Fixed double entry issue

This commit is contained in:
2026-08-30 21:06:46 +05:30
parent eb39d5ff90
commit a0e7a898fa
7 changed files with 198 additions and 53 deletions

View File

@@ -72,11 +72,7 @@ public class PurchaseOrderController {
Long userId = Long.valueOf(authentication.getDetails().toString());
List<PurchaseOrderItem> 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")

View File

@@ -45,4 +45,59 @@ public class LedgerService {
public Mono<Wallet> getCustomerLedger(Long userId, Long customerId, String customerName) {
return getOrCreateSystemLedger(userId, "Customer: " + customerName, "RECEIVABLES", "CUSTOMER_" + customerId);
}
public Mono<Wallet> 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<Wallet> 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<Wallet> 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<Wallet> 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);
}
}
}

View File

@@ -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;
})
);
}

View File

@@ -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);
});
});
});
});

View File

@@ -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<Void> 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<Void> 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);
});
});
}

View File

@@ -240,12 +240,16 @@ class _ProductListScreenState extends ConsumerState<ProductListScreen> {
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,

View File

@@ -421,7 +421,10 @@ class _InvoiceBuilderScreenState extends ConsumerState<InvoiceBuilderScreen> {
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))