diff --git a/backend/src/main/java/com/petshop/backend/controller/RefundController.java b/backend/src/main/java/com/petshop/backend/controller/RefundController.java new file mode 100644 index 00000000..ef527069 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/RefundController.java @@ -0,0 +1,25 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.refund.RefundRequest; +import com.petshop.backend.dto.refund.RefundResponse; +import com.petshop.backend.service.RefundService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/sales") +@RequiredArgsConstructor +public class RefundController { + + private final RefundService refundService; + + @PostMapping("/{saleId}/refunds") + public ResponseEntity createRefund( + @PathVariable Long saleId, + @Valid @RequestBody RefundRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(refundService.createRefund(saleId, request)); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/SaleController.java b/backend/src/main/java/com/petshop/backend/controller/SaleController.java new file mode 100644 index 00000000..6b77ba43 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/SaleController.java @@ -0,0 +1,35 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.sale.SaleRequest; +import com.petshop.backend.dto.sale.SaleResponse; +import com.petshop.backend.service.SaleService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/sales") +@RequiredArgsConstructor +public class SaleController { + + private final SaleService saleService; + + @GetMapping + public ResponseEntity> getAllSales(Pageable pageable) { + return ResponseEntity.ok(saleService.getAllSales(pageable)); + } + + @GetMapping("/{id}") + public ResponseEntity getSaleById(@PathVariable Long id) { + return ResponseEntity.ok(saleService.getSaleById(id)); + } + + @PostMapping + public ResponseEntity createSale(@Valid @RequestBody SaleRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(saleService.createSale(request)); + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/refund/RefundResponse.java b/backend/src/main/java/com/petshop/backend/dto/refund/RefundResponse.java index 50e22874..4ef1caa4 100644 --- a/backend/src/main/java/com/petshop/backend/dto/refund/RefundResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/refund/RefundResponse.java @@ -21,16 +21,16 @@ public class RefundResponse { private String processedByName; private List items; private LocalDateTime createdAt; -} -@Data -@NoArgsConstructor -@AllArgsConstructor -class RefundItemResponse { - private Long id; - private Long saleItemId; - private Long productId; - private String productName; - private Integer quantity; - private BigDecimal refundAmount; + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class RefundItemResponse { + private Long id; + private Long saleItemId; + private Long productId; + private String productName; + private Integer quantity; + private BigDecimal refundAmount; + } } 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 b44fd542..e05063ef 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 @@ -27,16 +27,16 @@ public class SaleResponse { private String notes; private List items; private LocalDateTime createdAt; -} -@Data -@NoArgsConstructor -@AllArgsConstructor -class SaleItemResponse { - private Long id; - private Long productId; - private String productName; - private Integer quantity; - private BigDecimal unitPrice; - private BigDecimal subtotal; + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class SaleItemResponse { + private Long id; + private Long productId; + private String productName; + private Integer quantity; + private BigDecimal unitPrice; + private BigDecimal subtotal; + } } diff --git a/backend/src/main/java/com/petshop/backend/repository/SaleItemRepository.java b/backend/src/main/java/com/petshop/backend/repository/SaleItemRepository.java new file mode 100644 index 00000000..0b67f95e --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/SaleItemRepository.java @@ -0,0 +1,9 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.SaleItem; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SaleItemRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/petshop/backend/service/RefundService.java b/backend/src/main/java/com/petshop/backend/service/RefundService.java new file mode 100644 index 00000000..8f2a4e6a --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/RefundService.java @@ -0,0 +1,115 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.refund.RefundRequest; +import com.petshop.backend.dto.refund.RefundResponse; +import com.petshop.backend.entity.*; +import com.petshop.backend.exception.BusinessException; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.*; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RefundService { + + private final RefundRepository refundRepository; + private final SaleRepository saleRepository; + private final SaleItemRepository saleItemRepository; + private final InventoryRepository inventoryRepository; + private final UserRepository userRepository; + + @Transactional + public RefundResponse createRefund(Long saleId, RefundRequest request) { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + User processedBy = userRepository.findByUsername(username) + .orElseThrow(() -> new ResourceNotFoundException("User not found: " + username)); + + Sale sale = saleRepository.findById(saleId) + .orElseThrow(() -> new ResourceNotFoundException("Sale not found with id: " + saleId)); + + Refund refund = new Refund(); + refund.setSale(sale); + refund.setRefundDate(LocalDateTime.now()); + refund.setRefundReason(request.getRefundReason()); + refund.setProcessedBy(processedBy); + + BigDecimal totalRefundAmount = BigDecimal.ZERO; + List refundItems = new ArrayList<>(); + + for (var itemRequest : request.getItems()) { + SaleItem saleItem = saleItemRepository.findById(itemRequest.getSaleItemId()) + .orElseThrow(() -> new ResourceNotFoundException("Sale item not found with id: " + itemRequest.getSaleItemId())); + + if (!saleItem.getSale().getId().equals(saleId)) { + throw new BusinessException("Sale item " + itemRequest.getSaleItemId() + " does not belong to sale " + saleId); + } + + if (itemRequest.getQuantity() > saleItem.getQuantity()) { + throw new BusinessException("Refund quantity (" + itemRequest.getQuantity() + + ") exceeds original sale quantity (" + saleItem.getQuantity() + ") for product: " + saleItem.getProduct().getProductName()); + } + + Inventory inventory = inventoryRepository.findByProductIdAndStoreId( + saleItem.getProduct().getId(), + sale.getStore().getId()) + .orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + + saleItem.getProduct().getId() + " at store " + sale.getStore().getId())); + + inventory.setQuantity(inventory.getQuantity() + itemRequest.getQuantity()); + inventory.setLastRestocked(LocalDateTime.now()); + inventoryRepository.save(inventory); + + BigDecimal itemRefundAmount = saleItem.getUnitPrice().multiply(BigDecimal.valueOf(itemRequest.getQuantity())); + + RefundItem refundItem = new RefundItem(); + refundItem.setRefund(refund); + refundItem.setSaleItem(saleItem); + refundItem.setQuantity(itemRequest.getQuantity()); + refundItem.setRefundAmount(itemRefundAmount); + + refundItems.add(refundItem); + totalRefundAmount = totalRefundAmount.add(itemRefundAmount); + } + + refund.setRefundAmount(totalRefundAmount); + refund.setItems(refundItems); + + Refund savedRefund = refundRepository.save(refund); + return mapToResponse(savedRefund); + } + + private RefundResponse mapToResponse(Refund refund) { + RefundResponse response = new RefundResponse(); + response.setId(refund.getId()); + response.setSaleId(refund.getSale().getId()); + response.setRefundDate(refund.getRefundDate()); + response.setRefundAmount(refund.getRefundAmount()); + response.setRefundReason(refund.getRefundReason()); + response.setProcessedBy(refund.getProcessedBy().getId()); + response.setProcessedByName(refund.getProcessedBy().getFullName()); + response.setCreatedAt(refund.getCreatedAt()); + + List itemResponses = new ArrayList<>(); + for (RefundItem item : refund.getItems()) { + RefundResponse.RefundItemResponse itemResponse = new RefundResponse.RefundItemResponse(); + itemResponse.setId(item.getId()); + itemResponse.setSaleItemId(item.getSaleItem().getId()); + itemResponse.setProductId(item.getSaleItem().getProduct().getId()); + itemResponse.setProductName(item.getSaleItem().getProduct().getProductName()); + itemResponse.setQuantity(item.getQuantity()); + itemResponse.setRefundAmount(item.getRefundAmount()); + itemResponses.add(itemResponse); + } + response.setItems(itemResponses); + + return response; + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java new file mode 100644 index 00000000..e3769adc --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -0,0 +1,145 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.sale.SaleRequest; +import com.petshop.backend.dto.sale.SaleResponse; +import com.petshop.backend.entity.*; +import com.petshop.backend.exception.BusinessException; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.*; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class SaleService { + + private final SaleRepository saleRepository; + private final ProductRepository productRepository; + private final CustomerRepository customerRepository; + private final StoreRepository storeRepository; + private final InventoryRepository inventoryRepository; + private final UserRepository userRepository; + + public Page getAllSales(Pageable pageable) { + return saleRepository.findAll(pageable).map(this::mapToResponse); + } + + public SaleResponse getSaleById(Long id) { + Sale sale = saleRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Sale not found with id: " + id)); + return mapToResponse(sale); + } + + @Transactional + public SaleResponse createSale(SaleRequest request) { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + User employee = userRepository.findByUsername(username) + .orElseThrow(() -> new ResourceNotFoundException("User not found: " + username)); + + Store store = storeRepository.findById(request.getStoreId()) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId())); + + Customer customer = null; + if (request.getCustomerId() != null) { + customer = customerRepository.findById(request.getCustomerId()) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); + } + + Sale sale = new Sale(); + sale.setSaleDate(LocalDateTime.now()); + sale.setEmployee(employee); + sale.setCustomer(customer); + sale.setStore(store); + sale.setPaymentMethod(request.getPaymentMethod()); + sale.setTax(request.getTax()); + sale.setNotes(request.getNotes()); + + BigDecimal subtotal = BigDecimal.ZERO; + List saleItems = new ArrayList<>(); + + for (var itemRequest : request.getItems()) { + Product product = productRepository.findById(itemRequest.getProductId()) + .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + itemRequest.getProductId())); + + Inventory inventory = inventoryRepository.findByProductIdAndStoreId(itemRequest.getProductId(), request.getStoreId()) + .orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProductId() + " at store " + request.getStoreId())); + + if (inventory.getQuantity() < itemRequest.getQuantity()) { + throw new BusinessException("Insufficient stock for product: " + product.getProductName() + + ". Available: " + inventory.getQuantity() + ", requested: " + itemRequest.getQuantity()); + } + + inventory.setQuantity(inventory.getQuantity() - itemRequest.getQuantity()); + inventoryRepository.save(inventory); + + BigDecimal unitPrice = product.getProductPrice(); + BigDecimal itemSubtotal = unitPrice.multiply(BigDecimal.valueOf(itemRequest.getQuantity())); + + SaleItem saleItem = new SaleItem(); + saleItem.setSale(sale); + saleItem.setProduct(product); + saleItem.setQuantity(itemRequest.getQuantity()); + saleItem.setUnitPrice(unitPrice); + saleItem.setSubtotal(itemSubtotal); + + saleItems.add(saleItem); + subtotal = subtotal.add(itemSubtotal); + } + + sale.setSubtotal(subtotal); + sale.setTotal(subtotal.add(sale.getTax())); + sale.setItems(saleItems); + + Sale savedSale = saleRepository.save(sale); + return mapToResponse(savedSale); + } + + private SaleResponse mapToResponse(Sale sale) { + SaleResponse response = new SaleResponse(); + response.setId(sale.getId()); + response.setSaleDate(sale.getSaleDate()); + response.setEmployeeId(sale.getEmployee().getId()); + response.setEmployeeName(sale.getEmployee().getFullName()); + + if (sale.getCustomer() != null) { + response.setCustomerId(sale.getCustomer().getId()); + response.setCustomerName(sale.getCustomer().getCustomerName()); + } + + if (sale.getStore() != null) { + response.setStoreId(sale.getStore().getId()); + response.setStoreName(sale.getStore().getStoreName()); + } + + response.setSubtotal(sale.getSubtotal()); + response.setTax(sale.getTax()); + response.setTotal(sale.getTotal()); + response.setPaymentMethod(sale.getPaymentMethod()); + response.setNotes(sale.getNotes()); + response.setCreatedAt(sale.getCreatedAt()); + + List itemResponses = new ArrayList<>(); + for (SaleItem item : sale.getItems()) { + SaleResponse.SaleItemResponse itemResponse = new SaleResponse.SaleItemResponse(); + itemResponse.setId(item.getId()); + itemResponse.setProductId(item.getProduct().getId()); + itemResponse.setProductName(item.getProduct().getProductName()); + itemResponse.setQuantity(item.getQuantity()); + itemResponse.setUnitPrice(item.getUnitPrice()); + itemResponse.setSubtotal(item.getSubtotal()); + itemResponses.add(itemResponse); + } + response.setItems(itemResponses); + + return response; + } +}