fix(back-end) calculator improvements
This commit is contained in:
@@ -18,10 +18,12 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -200,9 +202,8 @@ public class QuoteSessionController {
|
||||
|
||||
// Store breakdown
|
||||
Map<String, Object> breakdown = new HashMap<>();
|
||||
breakdown.put("machine_cost", result.getTotalPrice() - result.getSetupCost()); // Approximation?
|
||||
// Better: QuoteResult could expose detailed breakdown. For now just storing what we have.
|
||||
breakdown.put("setup_fee", result.getSetupCost());
|
||||
breakdown.put("machine_cost", result.getTotalPrice()); // Excludes setup fee which is at session level
|
||||
breakdown.put("setup_fee", 0);
|
||||
item.setPricingBreakdown(breakdown);
|
||||
|
||||
// Dimensions
|
||||
@@ -210,9 +211,9 @@ public class QuoteSessionController {
|
||||
// If GCodeParser doesn't return size, we might defaults or 0.
|
||||
// Stats has filament used.
|
||||
// Let's set dummy for now or upgrade parser later.
|
||||
item.setBoundingBoxXMm(BigDecimal.ZERO);
|
||||
item.setBoundingBoxYMm(BigDecimal.ZERO);
|
||||
item.setBoundingBoxZMm(BigDecimal.ZERO);
|
||||
item.setBoundingBoxXMm(settings.getBoundingBoxX() != null ? BigDecimal.valueOf(settings.getBoundingBoxX()) : BigDecimal.ZERO);
|
||||
item.setBoundingBoxYMm(settings.getBoundingBoxY() != null ? BigDecimal.valueOf(settings.getBoundingBoxY()) : BigDecimal.ZERO);
|
||||
item.setBoundingBoxZMm(settings.getBoundingBoxZ() != null ? BigDecimal.valueOf(settings.getBoundingBoxZ()) : BigDecimal.ZERO);
|
||||
|
||||
item.setCreatedAt(OffsetDateTime.now());
|
||||
item.setUpdatedAt(OffsetDateTime.now());
|
||||
@@ -245,7 +246,7 @@ public class QuoteSessionController {
|
||||
case "standard":
|
||||
default:
|
||||
settings.setLayerHeight(0.20);
|
||||
settings.setInfillDensity(20.0);
|
||||
settings.setInfillDensity(15.0);
|
||||
settings.setInfillPattern("grid");
|
||||
break;
|
||||
}
|
||||
@@ -308,20 +309,78 @@ public class QuoteSessionController {
|
||||
|
||||
List<QuoteLineItem> items = lineItemRepo.findByQuoteSessionId(id);
|
||||
|
||||
// Calculate Totals
|
||||
// Calculate Totals and global session hours
|
||||
BigDecimal itemsTotal = BigDecimal.ZERO;
|
||||
BigDecimal totalSeconds = BigDecimal.ZERO;
|
||||
|
||||
for (QuoteLineItem item : items) {
|
||||
BigDecimal lineTotal = item.getUnitPriceChf().multiply(BigDecimal.valueOf(item.getQuantity()));
|
||||
itemsTotal = itemsTotal.add(lineTotal);
|
||||
|
||||
if (item.getPrintTimeSeconds() != null) {
|
||||
totalSeconds = totalSeconds.add(BigDecimal.valueOf(item.getPrintTimeSeconds()).multiply(BigDecimal.valueOf(item.getQuantity())));
|
||||
}
|
||||
}
|
||||
|
||||
BigDecimal totalHours = totalSeconds.divide(BigDecimal.valueOf(3600), 4, RoundingMode.HALF_UP);
|
||||
com.printcalculator.entity.PricingPolicy policy = pricingRepo.findFirstByIsActiveTrueOrderByValidFromDesc();
|
||||
BigDecimal globalMachineCost = quoteCalculator.calculateSessionMachineCost(policy, totalHours);
|
||||
|
||||
itemsTotal = itemsTotal.add(globalMachineCost);
|
||||
|
||||
// Map items to DTO to embed distributed machine cost
|
||||
List<Map<String, Object>> itemsDto = new ArrayList<>();
|
||||
for (QuoteLineItem item : items) {
|
||||
Map<String, Object> dto = new HashMap<>();
|
||||
dto.put("id", item.getId());
|
||||
dto.put("originalFilename", item.getOriginalFilename());
|
||||
dto.put("quantity", item.getQuantity());
|
||||
dto.put("printTimeSeconds", item.getPrintTimeSeconds());
|
||||
dto.put("materialGrams", item.getMaterialGrams());
|
||||
dto.put("colorCode", item.getColorCode());
|
||||
dto.put("status", item.getStatus());
|
||||
|
||||
BigDecimal unitPrice = item.getUnitPriceChf();
|
||||
if (totalSeconds.compareTo(BigDecimal.ZERO) > 0 && item.getPrintTimeSeconds() != null) {
|
||||
BigDecimal itemSeconds = BigDecimal.valueOf(item.getPrintTimeSeconds()).multiply(BigDecimal.valueOf(item.getQuantity()));
|
||||
BigDecimal share = itemSeconds.divide(totalSeconds, 8, RoundingMode.HALF_UP);
|
||||
BigDecimal itemMachineCost = globalMachineCost.multiply(share);
|
||||
BigDecimal unitMachineCost = itemMachineCost.divide(BigDecimal.valueOf(item.getQuantity()), 2, RoundingMode.HALF_UP);
|
||||
unitPrice = unitPrice.add(unitMachineCost);
|
||||
}
|
||||
dto.put("unitPriceChf", unitPrice);
|
||||
itemsDto.add(dto);
|
||||
}
|
||||
|
||||
BigDecimal setupFee = session.getSetupCostChf() != null ? session.getSetupCostChf() : BigDecimal.ZERO;
|
||||
BigDecimal grandTotal = itemsTotal.add(setupFee);
|
||||
|
||||
// Calculate shipping cost based on dimensions
|
||||
boolean exceedsBaseSize = false;
|
||||
for (QuoteLineItem item : items) {
|
||||
BigDecimal x = item.getBoundingBoxXMm() != null ? item.getBoundingBoxXMm() : BigDecimal.ZERO;
|
||||
BigDecimal y = item.getBoundingBoxYMm() != null ? item.getBoundingBoxYMm() : BigDecimal.ZERO;
|
||||
BigDecimal z = item.getBoundingBoxZMm() != null ? item.getBoundingBoxZMm() : BigDecimal.ZERO;
|
||||
|
||||
BigDecimal[] dims = {x, y, z};
|
||||
java.util.Arrays.sort(dims);
|
||||
|
||||
if (dims[2].compareTo(BigDecimal.valueOf(250.0)) > 0 ||
|
||||
dims[1].compareTo(BigDecimal.valueOf(176.0)) > 0 ||
|
||||
dims[0].compareTo(BigDecimal.valueOf(20.0)) > 0) {
|
||||
exceedsBaseSize = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
BigDecimal shippingCostChf = exceedsBaseSize ? BigDecimal.valueOf(4.00) : BigDecimal.valueOf(2.00);
|
||||
|
||||
BigDecimal grandTotal = itemsTotal.add(setupFee).add(shippingCostChf);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("session", session);
|
||||
response.put("items", items);
|
||||
response.put("itemsTotalChf", itemsTotal);
|
||||
response.put("items", itemsDto);
|
||||
response.put("itemsTotalChf", itemsTotal); // Includes the base cost of all items + the global tiered machine cost
|
||||
response.put("shippingCostChf", shippingCostChf);
|
||||
response.put("globalMachineCostChf", globalMachineCost); // Provide it so frontend knows how much it was (optional now)
|
||||
response.put("grandTotalChf", grandTotal);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
@@ -21,4 +21,9 @@ public class PrintSettingsDto {
|
||||
private String infillPattern;
|
||||
private Boolean supportsEnabled;
|
||||
private String notes;
|
||||
|
||||
// Dimensions
|
||||
private Double boundingBoxX;
|
||||
private Double boundingBoxY;
|
||||
private Double boundingBoxZ;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,15 @@ public class OrderItem {
|
||||
@Column(name = "material_grams", precision = 12, scale = 2)
|
||||
private BigDecimal materialGrams;
|
||||
|
||||
@Column(name = "bounding_box_x_mm", precision = 10, scale = 3)
|
||||
private BigDecimal boundingBoxXMm;
|
||||
|
||||
@Column(name = "bounding_box_y_mm", precision = 10, scale = 3)
|
||||
private BigDecimal boundingBoxYMm;
|
||||
|
||||
@Column(name = "bounding_box_z_mm", precision = 10, scale = 3)
|
||||
private BigDecimal boundingBoxZMm;
|
||||
|
||||
@Column(name = "unit_price_chf", nullable = false, precision = 12, scale = 2)
|
||||
private BigDecimal unitPriceChf;
|
||||
|
||||
@@ -181,6 +190,30 @@ public class OrderItem {
|
||||
this.materialGrams = materialGrams;
|
||||
}
|
||||
|
||||
public BigDecimal getBoundingBoxXMm() {
|
||||
return boundingBoxXMm;
|
||||
}
|
||||
|
||||
public void setBoundingBoxXMm(BigDecimal boundingBoxXMm) {
|
||||
this.boundingBoxXMm = boundingBoxXMm;
|
||||
}
|
||||
|
||||
public BigDecimal getBoundingBoxYMm() {
|
||||
return boundingBoxYMm;
|
||||
}
|
||||
|
||||
public void setBoundingBoxYMm(BigDecimal boundingBoxYMm) {
|
||||
this.boundingBoxYMm = boundingBoxYMm;
|
||||
}
|
||||
|
||||
public BigDecimal getBoundingBoxZMm() {
|
||||
return boundingBoxZMm;
|
||||
}
|
||||
|
||||
public void setBoundingBoxZMm(BigDecimal boundingBoxZMm) {
|
||||
this.boundingBoxZMm = boundingBoxZMm;
|
||||
}
|
||||
|
||||
public BigDecimal getUnitPriceChf() {
|
||||
return unitPriceChf;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,10 @@ public class QuoteResult {
|
||||
private double totalPrice;
|
||||
private String currency;
|
||||
private PrintStats stats;
|
||||
private double setupCost;
|
||||
|
||||
public QuoteResult(double totalPrice, String currency, PrintStats stats, double setupCost) {
|
||||
public QuoteResult(double totalPrice, String currency, PrintStats stats) {
|
||||
this.totalPrice = totalPrice;
|
||||
this.currency = currency;
|
||||
this.stats = stats;
|
||||
this.setupCost = setupCost;
|
||||
}
|
||||
|
||||
public double getTotalPrice() {
|
||||
@@ -24,8 +21,4 @@ public class QuoteResult {
|
||||
public PrintStats getStats() {
|
||||
return stats;
|
||||
}
|
||||
|
||||
public double getSetupCost() {
|
||||
return setupCost;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import com.printcalculator.repository.OrderItemRepository;
|
||||
import com.printcalculator.repository.OrderRepository;
|
||||
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||
import com.printcalculator.repository.QuoteSessionRepository;
|
||||
import com.printcalculator.repository.PricingPolicyRepository;
|
||||
import com.printcalculator.service.QuoteCalculator;
|
||||
import com.printcalculator.event.OrderCreatedEvent;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -15,6 +17,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -37,6 +40,8 @@ public class OrderService {
|
||||
private final QrBillService qrBillService;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final PaymentService paymentService;
|
||||
private final QuoteCalculator quoteCalculator;
|
||||
private final PricingPolicyRepository pricingRepo;
|
||||
|
||||
public OrderService(OrderRepository orderRepo,
|
||||
OrderItemRepository orderItemRepo,
|
||||
@@ -47,7 +52,9 @@ public class OrderService {
|
||||
InvoicePdfRenderingService invoiceService,
|
||||
QrBillService qrBillService,
|
||||
ApplicationEventPublisher eventPublisher,
|
||||
PaymentService paymentService) {
|
||||
PaymentService paymentService,
|
||||
QuoteCalculator quoteCalculator,
|
||||
PricingPolicyRepository pricingRepo) {
|
||||
this.orderRepo = orderRepo;
|
||||
this.orderItemRepo = orderItemRepo;
|
||||
this.quoteSessionRepo = quoteSessionRepo;
|
||||
@@ -58,6 +65,8 @@ public class OrderService {
|
||||
this.qrBillService = qrBillService;
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.paymentService = paymentService;
|
||||
this.quoteCalculator = quoteCalculator;
|
||||
this.pricingRepo = pricingRepo;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -145,12 +154,41 @@ public class OrderService {
|
||||
order.setTotalChf(BigDecimal.ZERO);
|
||||
order.setDiscountChf(BigDecimal.ZERO);
|
||||
order.setSetupCostChf(session.getSetupCostChf() != null ? session.getSetupCostChf() : BigDecimal.ZERO);
|
||||
order.setShippingCostChf(BigDecimal.valueOf(9.00));
|
||||
|
||||
// Calculate shipping cost based on dimensions before initial save
|
||||
boolean exceedsBaseSize = false;
|
||||
for (QuoteLineItem item : quoteItems) {
|
||||
BigDecimal x = item.getBoundingBoxXMm() != null ? item.getBoundingBoxXMm() : BigDecimal.ZERO;
|
||||
BigDecimal y = item.getBoundingBoxYMm() != null ? item.getBoundingBoxYMm() : BigDecimal.ZERO;
|
||||
BigDecimal z = item.getBoundingBoxZMm() != null ? item.getBoundingBoxZMm() : BigDecimal.ZERO;
|
||||
|
||||
BigDecimal[] dims = {x, y, z};
|
||||
java.util.Arrays.sort(dims);
|
||||
|
||||
if (dims[2].compareTo(BigDecimal.valueOf(250.0)) > 0 ||
|
||||
dims[1].compareTo(BigDecimal.valueOf(176.0)) > 0 ||
|
||||
dims[0].compareTo(BigDecimal.valueOf(20.0)) > 0) {
|
||||
exceedsBaseSize = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
order.setShippingCostChf(exceedsBaseSize ? BigDecimal.valueOf(4.00) : BigDecimal.valueOf(2.00));
|
||||
|
||||
order = orderRepo.save(order);
|
||||
|
||||
List<OrderItem> savedItems = new ArrayList<>();
|
||||
|
||||
// Calculate global machine cost upfront
|
||||
BigDecimal totalSeconds = BigDecimal.ZERO;
|
||||
for (QuoteLineItem qItem : quoteItems) {
|
||||
if (qItem.getPrintTimeSeconds() != null) {
|
||||
totalSeconds = totalSeconds.add(BigDecimal.valueOf(qItem.getPrintTimeSeconds()).multiply(BigDecimal.valueOf(qItem.getQuantity())));
|
||||
}
|
||||
}
|
||||
BigDecimal totalHours = totalSeconds.divide(BigDecimal.valueOf(3600), 4, RoundingMode.HALF_UP);
|
||||
PricingPolicy policy = pricingRepo.findFirstByIsActiveTrueOrderByValidFromDesc();
|
||||
BigDecimal globalMachineCost = quoteCalculator.calculateSessionMachineCost(policy, totalHours);
|
||||
|
||||
for (QuoteLineItem qItem : quoteItems) {
|
||||
OrderItem oItem = new OrderItem();
|
||||
oItem.setOrder(order);
|
||||
@@ -159,10 +197,22 @@ public class OrderService {
|
||||
oItem.setColorCode(qItem.getColorCode());
|
||||
oItem.setMaterialCode(session.getMaterialCode());
|
||||
|
||||
oItem.setUnitPriceChf(qItem.getUnitPriceChf());
|
||||
oItem.setLineTotalChf(qItem.getUnitPriceChf().multiply(BigDecimal.valueOf(qItem.getQuantity())));
|
||||
BigDecimal distributedUnitPrice = qItem.getUnitPriceChf();
|
||||
if (totalSeconds.compareTo(BigDecimal.ZERO) > 0 && qItem.getPrintTimeSeconds() != null) {
|
||||
BigDecimal itemSeconds = BigDecimal.valueOf(qItem.getPrintTimeSeconds()).multiply(BigDecimal.valueOf(qItem.getQuantity()));
|
||||
BigDecimal share = itemSeconds.divide(totalSeconds, 8, RoundingMode.HALF_UP);
|
||||
BigDecimal itemMachineCost = globalMachineCost.multiply(share);
|
||||
BigDecimal unitMachineCost = itemMachineCost.divide(BigDecimal.valueOf(qItem.getQuantity()), 2, RoundingMode.HALF_UP);
|
||||
distributedUnitPrice = distributedUnitPrice.add(unitMachineCost);
|
||||
}
|
||||
|
||||
oItem.setUnitPriceChf(distributedUnitPrice);
|
||||
oItem.setLineTotalChf(distributedUnitPrice.multiply(BigDecimal.valueOf(qItem.getQuantity())));
|
||||
oItem.setPrintTimeSeconds(qItem.getPrintTimeSeconds());
|
||||
oItem.setMaterialGrams(qItem.getMaterialGrams());
|
||||
oItem.setBoundingBoxXMm(qItem.getBoundingBoxXMm());
|
||||
oItem.setBoundingBoxYMm(qItem.getBoundingBoxYMm());
|
||||
oItem.setBoundingBoxZMm(qItem.getBoundingBoxZMm());
|
||||
|
||||
UUID fileUuid = UUID.randomUUID();
|
||||
String ext = getExtension(qItem.getOriginalFilename());
|
||||
@@ -196,10 +246,7 @@ public class OrderService {
|
||||
}
|
||||
|
||||
order.setSubtotalChf(subtotal);
|
||||
if (order.getShippingCostChf() == null) {
|
||||
order.setShippingCostChf(BigDecimal.valueOf(9.00));
|
||||
}
|
||||
|
||||
|
||||
BigDecimal total = subtotal.add(order.getSetupCostChf()).add(order.getShippingCostChf()).subtract(order.getDiscountChf() != null ? order.getDiscountChf() : BigDecimal.ZERO);
|
||||
order.setTotalChf(total);
|
||||
|
||||
|
||||
@@ -79,25 +79,27 @@ public class QuoteCalculator {
|
||||
BigDecimal weightKg = BigDecimal.valueOf(stats.filamentWeightGrams()).divide(BigDecimal.valueOf(1000), 4, RoundingMode.HALF_UP);
|
||||
BigDecimal materialCost = weightKg.multiply(variant.getCostChfPerKg());
|
||||
|
||||
// Machine Cost: Tiered
|
||||
// We do NOT add tiered machine cost here anymore - it is calculated globally per session
|
||||
BigDecimal totalHours = BigDecimal.valueOf(stats.printTimeSeconds()).divide(BigDecimal.valueOf(3600), 4, RoundingMode.HALF_UP);
|
||||
BigDecimal machineCost = calculateMachineCost(policy, totalHours);
|
||||
|
||||
// Energy Cost: (watts / 1000) * hours * costPerKwh
|
||||
BigDecimal kw = BigDecimal.valueOf(machine.getPowerWatts()).divide(BigDecimal.valueOf(1000), 4, RoundingMode.HALF_UP);
|
||||
BigDecimal kwh = kw.multiply(totalHours);
|
||||
BigDecimal energyCost = kwh.multiply(policy.getElectricityCostChfPerKwh());
|
||||
|
||||
// Subtotal (Costs + Fixed Fees)
|
||||
BigDecimal fixedFee = policy.getFixedJobFeeChf();
|
||||
BigDecimal subtotal = materialCost.add(machineCost).add(energyCost).add(fixedFee);
|
||||
// Subtotal (Costs without Fixed Fees and without Machine Tiers)
|
||||
BigDecimal subtotal = materialCost.add(energyCost);
|
||||
|
||||
// Markup
|
||||
// Markup is percentage (e.g. 20.0)
|
||||
BigDecimal markupFactor = BigDecimal.ONE.add(policy.getMarkupPercent().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP));
|
||||
BigDecimal totalPrice = subtotal.multiply(markupFactor).setScale(2, RoundingMode.HALF_UP);
|
||||
|
||||
return new QuoteResult(totalPrice.doubleValue(), "CHF", stats, fixedFee.doubleValue());
|
||||
subtotal = subtotal.multiply(markupFactor);
|
||||
return new QuoteResult(subtotal.doubleValue(), "CHF", stats);
|
||||
}
|
||||
public BigDecimal calculateSessionMachineCost(PricingPolicy policy, BigDecimal hours) {
|
||||
BigDecimal rawCost = calculateMachineCost(policy, hours);
|
||||
BigDecimal markupFactor = BigDecimal.ONE.add(policy.getMarkupPercent().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP));
|
||||
return rawCost.multiply(markupFactor).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private BigDecimal calculateMachineCost(PricingPolicy policy, BigDecimal hours) {
|
||||
|
||||
Reference in New Issue
Block a user