diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java index 5208720f..f8752a16 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java @@ -201,7 +201,7 @@ public class SaleDetailFragment extends Fragment { if (sale.getItems() != null) { binding.llSaleItems.removeAllViews(); for (SaleDTO.SaleItemDTO item : sale.getItems()) { - addItemRow(item.getProductName(), Math.abs(item.getQuantity()), item.getUnitPrice()); + addItemRow(item.getProductName(), Math.abs(item.getQuantity()), item.getUnitPrice(), null); } } } @@ -308,15 +308,16 @@ public class SaleDetailFragment extends Fragment { break; } } - addItemRow(name, item.getQuantity(), price); + addItemRow(name, item.getQuantity(), price, item.getProdId()); } } - private void addItemRow(String name, int qty, BigDecimal price) { + private void addItemRow(String name, int qty, BigDecimal price, Long prodId) { if (getContext() == null) return; LinearLayout row = new LinearLayout(getContext()); row.setOrientation(LinearLayout.HORIZONTAL); row.setPadding(0, 8, 0, 8); + row.setGravity(android.view.Gravity.CENTER_VERTICAL); TextView tvName = new TextView(getContext()); tvName.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)); @@ -333,6 +334,20 @@ public class SaleDetailFragment extends Fragment { row.addView(tvName); row.addView(tvQty); row.addView(tvPrice); + + if (prodId != null) { + Button btnRemove = new Button(getContext()); + btnRemove.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + btnRemove.setText("✕"); + btnRemove.setTextSize(12f); + btnRemove.setBackgroundTintList(android.content.res.ColorStateList.valueOf(0xFFE53935)); + btnRemove.setTextColor(0xFFFFFFFF); + btnRemove.setPadding(16, 4, 16, 4); + btnRemove.setOnClickListener(v -> viewModel.removeFromCart(prodId)); + row.addView(btnRemove); + } + binding.llSaleItems.addView(row); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java index cb090feb..658264c9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java @@ -95,6 +95,12 @@ public class SaleDetailViewModel extends ViewModel { cartItems.setValue(currentCart); } + public void removeFromCart(Long prodId) { + List currentCart = new ArrayList<>(cartItems.getValue()); + currentCart.removeIf(item -> item.getProdId().equals(prodId)); + cartItems.setValue(currentCart); + } + public LiveData> getCartItems() { return cartItems; } public LiveData> lookupCoupon(String code) { diff --git a/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java b/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java index 6523505c..c1f2357f 100644 --- a/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java @@ -12,6 +12,8 @@ public class SaleResponse { private String employeeName; private Long storeId; private String storeName; + private Long customerId; + private String customerName; private BigDecimal totalAmount; private BigDecimal subtotalAmount; private BigDecimal couponDiscountAmount; @@ -77,6 +79,22 @@ public class SaleResponse { this.storeName = storeName; } + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + public BigDecimal getTotalAmount() { return totalAmount; } diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index fa5dd3c5..9d49972f 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -13,6 +13,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -20,6 +21,8 @@ import java.util.List; @Service public class SaleService { + private static final BigDecimal EMPLOYEE_DISCOUNT_PERCENT = new BigDecimal("0.10"); + private final SaleRepository saleRepository; private final ProductRepository productRepository; private final StoreRepository storeRepository; @@ -76,6 +79,7 @@ public class SaleService { if (request.getCouponId() != null) { Coupon coupon = couponRepository.findById(request.getCouponId()) .orElseThrow(() -> new ResourceNotFoundException("Coupon not found with id: " + request.getCouponId())); + validateCoupon(coupon); sale.setCoupon(coupon); } @@ -85,8 +89,9 @@ public class SaleService { sale.setCart(cart); } + User customer = null; if (request.getCustomerId() != null) { - User customer = userRepository.findById(request.getCustomerId()) + customer = userRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); sale.setCustomer(customer); } @@ -97,7 +102,7 @@ public class SaleService { sale.setOriginalSale(originalSale); } - BigDecimal totalAmount = BigDecimal.ZERO; + BigDecimal subtotalAmount = BigDecimal.ZERO; List saleItems = new ArrayList<>(); if (sale.getIsRefund() && sale.getOriginalSale() != null) { @@ -145,9 +150,11 @@ public class SaleService { saleItem.setUnitPrice(unitPrice); saleItems.add(saleItem); - totalAmount = totalAmount.add(itemTotal); + subtotalAmount = subtotalAmount.add(itemTotal); } - totalAmount = totalAmount.negate(); + subtotalAmount = subtotalAmount.negate(); + sale.setSubtotalAmount(subtotalAmount); + sale.setTotalAmount(subtotalAmount); } else { for (var itemRequest : request.getItems()) { Product product = productRepository.findById(itemRequest.getProdId()) @@ -174,17 +181,77 @@ public class SaleService { saleItem.setUnitPrice(unitPrice); saleItems.add(saleItem); - totalAmount = totalAmount.add(itemTotal); + subtotalAmount = subtotalAmount.add(itemTotal); + } + sale.setSubtotalAmount(subtotalAmount); + + BigDecimal couponDiscount = calculateCouponDiscount(sale.getCoupon(), subtotalAmount); + sale.setCouponDiscountAmount(couponDiscount); + + BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount)); + sale.setEmployeeDiscountAmount(employeeDiscount); + + BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount); + sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO)); + + sale.setPointsEarned(sale.getTotalAmount().setScale(0, RoundingMode.FLOOR).intValue()); + if (customer != null) { + customer.setLoyaltyPoints(customer.getLoyaltyPoints() + sale.getPointsEarned()); + userRepository.save(customer); } } - sale.setTotalAmount(totalAmount); sale.setItems(saleItems); Sale savedSale = saleRepository.save(sale); return mapToResponse(savedSale); } + private void validateCoupon(Coupon coupon) { + if (!Boolean.TRUE.equals(coupon.getActive())) { + throw new BusinessException("Coupon is not active"); + } + LocalDateTime now = LocalDateTime.now(); + if (coupon.getStartsAt() != null && now.isBefore(coupon.getStartsAt())) { + throw new BusinessException("Coupon has not started yet"); + } + if (coupon.getEndsAt() != null && now.isAfter(coupon.getEndsAt())) { + throw new BusinessException("Coupon has expired"); + } + } + + private BigDecimal calculateCouponDiscount(Coupon coupon, BigDecimal subtotal) { + if (coupon == null || subtotal.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + + if (coupon.getMinOrderAmount() != null && subtotal.compareTo(coupon.getMinOrderAmount()) < 0) { + return BigDecimal.ZERO; + } + + BigDecimal discount = BigDecimal.ZERO; + String type = coupon.getDiscountType().trim().toUpperCase(); + if ("PERCENTAGE".equals(type) || "PERCENT".equals(type)) { + discount = subtotal.multiply(coupon.getDiscountValue().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP)); + } else if ("FIXED".equals(type)) { + discount = coupon.getDiscountValue(); + } + + return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP); + } + + private BigDecimal calculateEmployeeDiscount(User customer, BigDecimal remainingAmount) { + if (customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + + if (customer.getRole() == User.Role.STAFF || customer.getRole() == User.Role.ADMIN) { + return remainingAmount.multiply(EMPLOYEE_DISCOUNT_PERCENT).setScale(2, RoundingMode.HALF_UP); + } + + return BigDecimal.ZERO; + } + private SaleResponse mapToResponse(Sale sale) { SaleResponse response = new SaleResponse(); response.setSaleId(sale.getSaleId()); @@ -197,6 +264,11 @@ public class SaleService { response.setStoreName(sale.getStore().getStoreName()); } + if (sale.getCustomer() != null) { + response.setCustomerId(sale.getCustomer().getId()); + response.setCustomerName(sale.getCustomer().getFirstName() + " " + sale.getCustomer().getLastName()); + } + response.setTotalAmount(sale.getTotalAmount()); response.setSubtotalAmount(sale.getSubtotalAmount()); response.setCouponDiscountAmount(sale.getCouponDiscountAmount());