diff --git a/kifi-api/src/main/java/com/kifi/api/controller/vendor/PurchaseOrderController.java b/kifi-api/src/main/java/com/kifi/api/controller/vendor/PurchaseOrderController.java index f7a37b4..b652dbe 100644 --- a/kifi-api/src/main/java/com/kifi/api/controller/vendor/PurchaseOrderController.java +++ b/kifi-api/src/main/java/com/kifi/api/controller/vendor/PurchaseOrderController.java @@ -72,11 +72,7 @@ public class PurchaseOrderController { Long userId = Long.valueOf(authentication.getDetails().toString()); List items = (po != null) ? po.getItems() : null; return purchaseOrderService.markAsReceived(userId, id, items) - .map(ResponseEntity::ok) - .onErrorResume(e -> { - e.printStackTrace(); - return Mono.just(ResponseEntity.internalServerError().build()); - }); + .map(ResponseEntity::ok); } @PostMapping("/{id}/payments") diff --git a/kifi-api/src/main/java/com/kifi/api/service/accounting/LedgerService.java b/kifi-api/src/main/java/com/kifi/api/service/accounting/LedgerService.java index b148d8e..596e556 100644 --- a/kifi-api/src/main/java/com/kifi/api/service/accounting/LedgerService.java +++ b/kifi-api/src/main/java/com/kifi/api/service/accounting/LedgerService.java @@ -45,4 +45,59 @@ public class LedgerService { public Mono getCustomerLedger(Long userId, Long customerId, String customerName) { return getOrCreateSystemLedger(userId, "Customer: " + customerName, "RECEIVABLES", "CUSTOMER_" + customerId); } + + public Mono getOrCreateUserCashWallet(Long userId) { + return walletRepository.findByOwnerIdAndNature(userId, "CASH") + .filter(w -> w.getSubNature() == null) + .next() + .switchIfEmpty(Mono.defer(() -> + walletService.createWallet( + userId, "Cash", "CASH", null, "#10B981", "INR", + BigDecimal.ZERO, null, null, null, null, null, null + ) + )); + } + + public Mono getOrCreateUserBankWallet(Long userId) { + return walletRepository.findByOwnerIdAndNature(userId, "SAVINGS") + .filter(w -> w.getSubNature() == null) + .next() + .switchIfEmpty(Mono.defer(() -> + walletService.createWallet( + userId, "Bank Account", "SAVINGS", null, "#3B82F6", "INR", + BigDecimal.ZERO, null, null, null, null, null, null + ) + )); + } + + public Mono getOrCreateWalletByNature(Long userId, String nature) { + String normalizedNature = nature != null ? nature.toUpperCase() : "CASH"; + String defaultName = normalizedNature.substring(0, 1).toUpperCase() + normalizedNature.substring(1).toLowerCase(); + if ("SAVINGS".equalsIgnoreCase(normalizedNature)) defaultName = "Bank Account"; + if ("CASH".equalsIgnoreCase(normalizedNature)) defaultName = "Cash"; + + String finalName = defaultName; + return walletRepository.findByOwnerIdAndNature(userId, normalizedNature) + .filter(w -> w.getSubNature() == null) + .next() + .switchIfEmpty(Mono.defer(() -> + walletService.createWallet( + userId, finalName, normalizedNature, null, "#3B82F6", "INR", + BigDecimal.ZERO, null, null, null, null, null, null + ) + )); + } + + public Mono resolvePaymentWallet(Long userId, Long walletId, String paymentMethod) { + if (walletId != null) { + return walletRepository.findById(walletId) + .filter(w -> !"INVENTORY".equals(w.getSubNature()) && !"COGS".equals(w.getSubNature()) && (w.getSubNature() == null || (!w.getSubNature().startsWith("VENDOR_") && !w.getSubNature().startsWith("CUSTOMER_")))) + .switchIfEmpty(Mono.defer(() -> resolvePaymentWallet(userId, null, paymentMethod))); + } + if (paymentMethod != null && ("CASH".equalsIgnoreCase(paymentMethod) || "Cash".equalsIgnoreCase(paymentMethod))) { + return getOrCreateUserCashWallet(userId); + } else { + return getOrCreateUserBankWallet(userId); + } + } } diff --git a/kifi-api/src/main/java/com/kifi/api/service/inventory/ProductService.java b/kifi-api/src/main/java/com/kifi/api/service/inventory/ProductService.java index e6f10ee..842a361 100644 --- a/kifi-api/src/main/java/com/kifi/api/service/inventory/ProductService.java +++ b/kifi-api/src/main/java/com/kifi/api/service/inventory/ProductService.java @@ -9,6 +9,7 @@ import com.kifi.api.repository.inventory.InventoryMovementRepository; import com.kifi.api.repository.inventory.InventoryMovementItemRepository; import com.kifi.api.repository.inventory.InventoryLocationRepository; import com.kifi.api.repository.inventory.ProductBomRepository; +import com.kifi.api.repository.inventory.InventoryItemRepository; import com.kifi.api.repository.business.BusinessFeatureRepository; import com.kifi.api.entity.inventory.InventoryBalance; import com.kifi.api.entity.inventory.InventoryLocation; @@ -30,6 +31,7 @@ import java.math.BigDecimal; public class ProductService { private final ProductRepository productRepository; private final ProductImageRepository productImageRepository; + private final InventoryItemRepository inventoryItemRepository; private final InventoryBalanceRepository inventoryBalanceRepository; private final InventoryMovementRepository inventoryMovementRepository; private final InventoryMovementItemRepository inventoryMovementItemRepository; @@ -51,14 +53,26 @@ public class ProductService { return product; }) ) - .flatMap(product -> inventoryBalanceRepository.findByProductId(product.getId()) - .map(InventoryBalance::getQuantity) + .flatMap(product -> inventoryItemRepository.findByProductId(product.getId()) + .filter(item -> "AVAILABLE".equalsIgnoreCase(item.getStatus()) || item.getStatus() == null) + .map(item -> { + if (item.getGrossWeight() != null && item.getGrossWeight().compareTo(BigDecimal.ZERO) > 0) { + return item.getGrossWeight(); + } + return BigDecimal.ONE; + }) .reduce(BigDecimal.ZERO, BigDecimal::add) .map(totalStock -> { product.setCurrentStock(totalStock); return product; }) .defaultIfEmpty(product) + .map(p -> { + if (p.getCurrentStock() == null) { + p.setCurrentStock(BigDecimal.ZERO); + } + return p; + }) ); } diff --git a/kifi-api/src/main/java/com/kifi/api/service/invoice/InvoiceService.java b/kifi-api/src/main/java/com/kifi/api/service/invoice/InvoiceService.java index f671d2d..295192f 100644 --- a/kifi-api/src/main/java/com/kifi/api/service/invoice/InvoiceService.java +++ b/kifi-api/src/main/java/com/kifi/api/service/invoice/InvoiceService.java @@ -7,6 +7,7 @@ import com.kifi.api.repository.business.BusinessFeatureRepository; import com.kifi.api.repository.invoice.InvoiceItemRepository; import com.kifi.api.repository.invoice.InvoicePaymentRepository; import com.kifi.api.repository.invoice.InvoiceRepository; +import com.kifi.api.repository.inventory.InventoryItemRepository; import com.kifi.api.service.inventory.ProductService; import com.kifi.api.service.inventory.InventoryItemService; import com.kifi.api.service.accounting.LedgerService; @@ -20,6 +21,7 @@ import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.math.BigDecimal; import java.time.LocalDateTime; import org.springframework.transaction.annotation.Transactional; @@ -31,6 +33,7 @@ public class InvoiceService { private final InvoiceRepository invoiceRepository; private final InvoiceItemRepository invoiceItemRepository; private final InvoicePaymentRepository invoicePaymentRepository; + private final InventoryItemRepository inventoryItemRepository; private final BusinessFeatureRepository businessFeatureRepository; private final ProductService productService; private final InventoryItemService inventoryItemService; @@ -112,20 +115,23 @@ public class InvoiceService { .walletId(invoice.getPaymentWalletId()) .build(); return invoicePaymentRepository.save(payment).flatMap(savedPayment -> { - if (savedPayment.getWalletId() != null) { - Transaction transaction = Transaction.builder() - .userId(userId) - .toWalletId(savedPayment.getWalletId()) - .type("INCOME") - .amount(savedPayment.getAmount()) - .date(savedPayment.getPaymentDate()) - .description("Initial payment for Invoice #" + savedInvoice.getInvoiceNumber()) - .notes(savedPayment.getPaymentMethod()) - .createdAt(LocalDateTime.now()) - .build(); - return transactionService.addTransaction(userId, transaction).thenReturn(savedInvoice); - } - return Mono.just(savedInvoice); + return ledgerService.resolvePaymentWallet(userId, savedPayment.getWalletId(), savedPayment.getPaymentMethod()) + .flatMap(targetWallet -> { + savedPayment.setWalletId(targetWallet.getId()); + Transaction transaction = Transaction.builder() + .userId(userId) + .toWalletId(targetWallet.getId()) + .type("INCOME") + .amount(savedPayment.getAmount()) + .date(savedPayment.getPaymentDate()) + .description("Initial payment for Invoice #" + savedInvoice.getInvoiceNumber()) + .notes(savedPayment.getPaymentMethod()) + .createdAt(LocalDateTime.now()) + .build(); + return transactionService.addTransaction(userId, transaction) + .then(invoicePaymentRepository.save(savedPayment)) + .thenReturn(savedInvoice); + }); }); } return Mono.just(savedInvoice); @@ -141,10 +147,13 @@ public class InvoiceService { return Flux.fromIterable(invoice.getItems()) .concatMap(item -> { if (item.getInventoryItemId() != null) { - return inventoryItemService.updateItem(userId, item.getInventoryItemId(), - com.kifi.api.entity.inventory.InventoryItem.builder() - .status("SOLD") - .build()); + return inventoryItemRepository.findById(item.getInventoryItemId()) + .flatMap(invItem -> { + invItem.setStatus("SOLD"); + invItem.setUpdatedAt(LocalDateTime.now()); + BigDecimal cost = invItem.getPurchaseCost() != null ? invItem.getPurchaseCost() : BigDecimal.ZERO; + return inventoryItemRepository.save(invItem).thenReturn(cost); + }).defaultIfEmpty(BigDecimal.ZERO); } else if (item.getProductId() != null) { InventoryMovement movement = InventoryMovement.builder() .userId(userId) @@ -158,11 +167,37 @@ public class InvoiceService { movementItem.setQuantity(item.getQuantity()); movement.setItems(java.util.Collections.singletonList(movementItem)); - return productService.adjustStock(userId, item.getProductId(), movement); + return productService.adjustStock(userId, item.getProductId(), movement) + .thenReturn(BigDecimal.ZERO); } - return Mono.just(item); + return Mono.just(BigDecimal.ZERO); }) - .then(Mono.just(invoice)); + .reduce(BigDecimal.ZERO, BigDecimal::add) + .flatMap(totalCogs -> { + if (totalCogs.compareTo(BigDecimal.ZERO) > 0) { + return Mono.zip( + ledgerService.getInventoryAssetLedger(userId), + ledgerService.getCogsLedger(userId) + ).flatMap(ledgers -> { + Transaction cogsTx = Transaction.builder() + .userId(userId) + .fromWalletId(ledgers.getT1().getId()) // Inventory Asset (Asset decreases) + .toWalletId(ledgers.getT2().getId()) // COGS (Expense increases) + .type("EXPENSE") + .amount(totalCogs) + .date(invoice.getIssueDate() != null ? invoice.getIssueDate() : java.time.LocalDate.now()) + .description("COGS for Invoice #" + invoice.getInvoiceNumber()) + .createdAt(LocalDateTime.now()) + .build(); + return transactionService.addTransaction(userId, cogsTx); + }).onErrorResume(err -> { + System.err.println("COGS relief transaction error: " + err.getMessage()); + return Mono.empty(); + }); + } + return Mono.empty(); + }) + .thenReturn(invoice); } return Mono.just(invoice); }); @@ -248,20 +283,23 @@ public class InvoiceService { } } return processMono.flatMap(processedInv -> { - if (savedPayment.getWalletId() != null) { - Transaction transaction = Transaction.builder() - .userId(userId) - .toWalletId(savedPayment.getWalletId()) - .type("INCOME") - .amount(savedPayment.getAmount()) - .date(savedPayment.getPaymentDate()) - .description("Payment for Invoice #" + invoice.getInvoiceNumber()) - .notes(savedPayment.getPaymentMethod()) - .createdAt(LocalDateTime.now()) - .build(); - return transactionService.addTransaction(userId, transaction).thenReturn(savedPayment); - } - return Mono.just(savedPayment); + return ledgerService.resolvePaymentWallet(userId, savedPayment.getWalletId(), savedPayment.getPaymentMethod()) + .flatMap(targetWallet -> { + savedPayment.setWalletId(targetWallet.getId()); + Transaction transaction = Transaction.builder() + .userId(userId) + .toWalletId(targetWallet.getId()) + .type("INCOME") + .amount(savedPayment.getAmount()) + .date(savedPayment.getPaymentDate()) + .description("Payment for Invoice #" + invoice.getInvoiceNumber()) + .notes(savedPayment.getPaymentMethod()) + .createdAt(LocalDateTime.now()) + .build(); + return transactionService.addTransaction(userId, transaction) + .then(invoicePaymentRepository.save(savedPayment)) + .thenReturn(savedPayment); + }); }); }); }); diff --git a/kifi-api/src/main/java/com/kifi/api/service/vendor/PurchaseOrderService.java b/kifi-api/src/main/java/com/kifi/api/service/vendor/PurchaseOrderService.java index afe0cfe..59bf2aa 100644 --- a/kifi-api/src/main/java/com/kifi/api/service/vendor/PurchaseOrderService.java +++ b/kifi-api/src/main/java/com/kifi/api/service/vendor/PurchaseOrderService.java @@ -90,7 +90,7 @@ public class PurchaseOrderService { item.setPoId(savedPo.getId()); return item; }) - .flatMap(purchaseOrderItemRepository::save) + .concatMap(purchaseOrderItemRepository::save) .collectList() .map(savedItems -> { savedPo.setItems(savedItems); @@ -126,7 +126,7 @@ public class PurchaseOrderService { item.setPoId(id); return item; }) - .flatMap(purchaseOrderItemRepository::save)) + .concatMap(purchaseOrderItemRepository::save)) .collectList() .map(savedItems -> { savedPo.setItems(savedItems); @@ -160,7 +160,7 @@ public class PurchaseOrderService { item.setPoId(id); return item; }) - .flatMap(purchaseOrderItemRepository::save)) + .concatMap(purchaseOrderItemRepository::save)) .collectList(); } else { itemsProcess = purchaseOrderItemRepository.findByPoId(id).collectList(); @@ -179,10 +179,10 @@ public class PurchaseOrderService { .build()) .map(InventoryLocation::getId))); - // 2. Update stock & inventory items for all received items + // 2. Update stock & inventory items for all received items sequentially Mono stockUpdates = locationIdMono.flatMap(locId -> Flux.fromIterable(savedItems) - .flatMap(item -> { + .concatMap(item -> { if (item.getProductId() != null) { InventoryItem invItem = InventoryItem.builder() .userId(userId) @@ -195,6 +195,7 @@ public class PurchaseOrderService { .huid(item.getHuid()) .grossWeight(item.getWeight()) .netWeight(item.getWeight()) + .status("AVAILABLE") .build(); BigDecimal itemQty = (item.getWeight() != null && item.getWeight().compareTo(BigDecimal.ZERO) > 0) @@ -279,7 +280,41 @@ public class PurchaseOrderService { BigDecimal newPaid = po.getAmountPaid() != null ? po.getAmountPaid().add(savedPayment.getAmount()) : savedPayment.getAmount(); po.setAmountPaid(newPaid); po.setUpdatedAt(LocalDateTime.now()); - return purchaseOrderRepository.save(po).thenReturn(savedPayment); + + Mono txMono = Mono.empty(); + if (po.getVendorId() != null && savedPayment.getAmount() != null && savedPayment.getAmount().compareTo(BigDecimal.ZERO) > 0) { + txMono = vendorRepository.findById(po.getVendorId()) + .flatMap(vendor -> Mono.zip( + ledgerService.resolvePaymentWallet(userId, null, savedPayment.getPaymentMethod()), + ledgerService.getVendorLedger(userId, vendor.getId(), vendor.getName()) + )) + .flatMap(wallets -> { + com.kifi.api.entity.Transaction tx = com.kifi.api.entity.Transaction.builder() + .userId(userId) + .fromWalletId(wallets.getT1().getId()) // Cash / Bank (Asset decreases) + .toWalletId(wallets.getT2().getId()) // Vendor Payable (Liability decreases / is settled) + .type("EXPENSE") + .amount(savedPayment.getAmount()) + .date(savedPayment.getPaymentDate() != null ? savedPayment.getPaymentDate().toLocalDate() : java.time.LocalDate.now()) + .description("Payment for PO #" + po.getPoNumber()) + .notes(savedPayment.getPaymentMethod()) + .createdAt(LocalDateTime.now()) + .build(); + return transactionService.addTransaction(userId, tx) + .flatMap(txSaved -> { + savedPayment.setTransactionId(txSaved.getId()); + return purchasePaymentRepository.save(savedPayment); + }).then(); + }) + .onErrorResume(err -> { + System.err.println("Failed to post vendor payment transaction: " + err.getMessage()); + return Mono.empty(); + }); + } + + return purchaseOrderRepository.save(po) + .then(txMono) + .thenReturn(savedPayment); }); }); } diff --git a/kifi-app/lib/features/inventory/presentation/product_list_screen.dart b/kifi-app/lib/features/inventory/presentation/product_list_screen.dart index 881e3fd..0bd678e 100644 --- a/kifi-app/lib/features/inventory/presentation/product_list_screen.dart +++ b/kifi-app/lib/features/inventory/presentation/product_list_screen.dart @@ -240,12 +240,16 @@ class _ProductListScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - (p.currentStock != null) - ? 'Stock: ${p.currentStock ?? 0}' + p.currentStock != null + ? (p.currentStock! > 0 + ? 'Stock: ${p.currentStock! % 1 == 0 ? p.currentStock!.toInt() : p.currentStock}' + : 'Out of Stock') : 'Untracked', style: TextStyle( - color: (p.currentStock != null) - ? Colors.orange + color: p.currentStock != null + ? (p.currentStock! > 0 + ? const Color(0xFF10B981) + : Colors.red) : Colors.grey, fontSize: 12, fontWeight: FontWeight.bold, diff --git a/kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart b/kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart index 999b008..3a262ea 100644 --- a/kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart +++ b/kifi-app/lib/features/sales/presentation/invoice_builder_screen.dart @@ -421,7 +421,10 @@ class _InvoiceBuilderScreenState extends ConsumerState { totalAmount: grandTotal, amountPaid: validAmountPaid, paymentMethod: validAmountPaid > 0 ? _paymentMethod : null, - paymentWalletId: validAmountPaid > 0 ? (_selectedWalletId ?? ref.read(walletProvider).value?.firstOrNull?.id) : null, + paymentWalletId: validAmountPaid > 0 + ? (_selectedWalletId ?? + ref.read(walletProvider).value?.where((w) => w.nature == 'CASH' || w.nature == 'SAVINGS').firstOrNull?.id) + : null, nextPaymentDate: balanceDue > 0.01 ? (_nextPaymentDate ?? DateTime.now().add(const Duration(days: 30))) : null, status: widget.existingInvoice != null ? (isFullyPaid ? 'PAID' : (validAmountPaid > 0.01 ? 'PARTIAL' : widget.existingInvoice!.status))