Added market place specific feature

This commit is contained in:
2026-09-04 10:43:23 +05:30
parent b123a34e8f
commit 5e9b3f2122
31 changed files with 4978 additions and 214 deletions

View File

@@ -0,0 +1,48 @@
package com.kifi.api.controller.invoice;
import com.kifi.api.entity.invoice.CreditNote;
import com.kifi.api.service.invoice.CreditNoteService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/kifi-v2")
@RequiredArgsConstructor
public class CreditNoteController {
private final CreditNoteService creditNoteService;
@GetMapping("/credit-notes")
public Flux<CreditNote> getCreditNotes(Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return creditNoteService.getCreditNotes(userId);
}
@GetMapping("/credit-notes/{id}")
public Mono<CreditNote> getCreditNoteById(
@PathVariable Long id,
Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return creditNoteService.getCreditNoteById(id, userId);
}
@GetMapping("/invoices/{invoiceId}/credit-notes")
public Flux<CreditNote> getCreditNotesForInvoice(
@PathVariable Long invoiceId,
Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return creditNoteService.getCreditNotesForInvoice(invoiceId, userId);
}
@PostMapping("/invoices/{invoiceId}/credit-notes")
public Mono<CreditNote> createCreditNote(
@PathVariable Long invoiceId,
Authentication authentication,
@RequestBody CreditNote creditNote) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return creditNoteService.createCreditNote(userId, invoiceId, creditNote);
}
}

View File

@@ -0,0 +1,48 @@
package com.kifi.api.controller.invoice;
import com.kifi.api.entity.invoice.MarketplaceSettlement;
import com.kifi.api.service.invoice.MarketplaceSettlementService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/kifi-v2")
@RequiredArgsConstructor
public class MarketplaceSettlementController {
private final MarketplaceSettlementService settlementService;
@GetMapping("/marketplace-settlements")
public Flux<MarketplaceSettlement> getSettlements(Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return settlementService.getSettlements(userId);
}
@GetMapping("/marketplace-settlements/{id}")
public Mono<MarketplaceSettlement> getSettlementById(
@PathVariable Long id,
Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return settlementService.getSettlementById(id, userId);
}
@GetMapping("/invoices/{invoiceId}/settlements")
public Flux<MarketplaceSettlement> getSettlementsForInvoice(
@PathVariable Long invoiceId,
Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return settlementService.getSettlementsForInvoice(invoiceId, userId);
}
@PostMapping("/invoices/{invoiceId}/settlements")
public Mono<MarketplaceSettlement> createSettlement(
@PathVariable Long invoiceId,
Authentication authentication,
@RequestBody MarketplaceSettlement settlement) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return settlementService.createSettlement(userId, invoiceId, settlement);
}
}

View File

@@ -5,6 +5,7 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.math.BigDecimal;
@@ -58,6 +59,19 @@ public class Product {
private Double makingCharges;
private String makingChargesType;
private Double wastagePercentage;
@Column("min_stock")
private BigDecimal minStock;
@Column("reorder_level")
private BigDecimal reorderLevel;
@Column("reorder_quantity")
private BigDecimal reorderQuantity;
@Column("track_inventory")
private Boolean trackInventory;
private Boolean isActive;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;

View File

@@ -0,0 +1,86 @@
package com.kifi.api.entity.invoice;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table("credit_notes")
public class CreditNote {
@Id
private Long id;
@Column("user_id")
private Long userId;
@Column("invoice_id")
private Long invoiceId;
@Column("credit_note_number")
private String creditNoteNumber;
@Column("customer_id")
private Long customerId;
@Column("return_date")
private LocalDate returnDate;
@Column("reason")
private String reason; // CUSTOMER_RETURN, RTO_UNDELIVERED, DEFECTIVE, EXCHANGE, OTHER
@Column("restock_items")
@Builder.Default
private Boolean restockItems = true;
@Column("subtotal")
private BigDecimal subtotal;
@Column("tax_total")
private BigDecimal taxTotal;
@Column("cgst_total")
private BigDecimal cgstTotal;
@Column("sgst_total")
private BigDecimal sgstTotal;
@Column("igst_total")
private BigDecimal igstTotal;
@Column("discount_total")
private BigDecimal discountTotal;
@Column("total_amount")
private BigDecimal totalAmount;
@Column("refund_status")
private String refundStatus; // ADJUSTED_TO_AR, REFUNDED_TO_WALLET, STORE_CREDIT, PENDING
@Column("refund_wallet_id")
private Long refundWalletId;
@Column("notes")
private String notes;
@Column("created_at")
private LocalDateTime createdAt;
@Column("updated_at")
private LocalDateTime updatedAt;
@Transient
private List<CreditNoteItem> items;
}

View File

@@ -0,0 +1,78 @@
package com.kifi.api.entity.invoice;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.math.BigDecimal;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table("credit_note_items")
public class CreditNoteItem {
@Id
private Long id;
@Column("credit_note_id")
private Long creditNoteId;
@Column("invoice_item_id")
private Long invoiceItemId;
@Column("product_id")
private Long productId;
@Column("inventory_item_id")
private Long inventoryItemId;
@Column("product_name")
private String productName;
@Column("sku")
private String sku;
@Column("huid")
private String huid;
@Column("hsn_code")
private String hsnCode;
@Column("quantity")
private BigDecimal quantity;
@Column("weight")
private BigDecimal weight;
@Column("unit_price")
private BigDecimal unitPrice;
@Column("tax_rate")
private BigDecimal taxRate;
@Column("cgst")
private BigDecimal cgst;
@Column("sgst")
private BigDecimal sgst;
@Column("igst")
private BigDecimal igst;
@Column("discount")
private BigDecimal discount;
@Column("making_charge")
private BigDecimal makingCharge;
@Column("other_charges")
private BigDecimal otherCharges;
@Column("total")
private BigDecimal total;
}

View File

@@ -89,6 +89,24 @@ public class Invoice {
@Column("marketplace_order_id")
private String marketplaceOrderId;
@Column("courier_partner")
private String courierPartner;
@Column("tracking_number")
private String trackingNumber;
@Column("dispatch_status")
private String dispatchStatus; // PENDING, PACKED, SHIPPED, IN_TRANSIT, DELIVERED, RTO
@Column("shipped_at")
private LocalDateTime shippedAt;
@Column("shipping_address")
private String shippingAddress;
@Column("shipping_pincode")
private String shippingPincode;
@Column("place_of_supply_state_id")
private Long placeOfSupplyStateId;

View File

@@ -0,0 +1,90 @@
package com.kifi.api.entity.invoice;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table("marketplace_settlements")
public class MarketplaceSettlement {
@Id
private Long id;
@Column("user_id")
private Long userId;
@Column("invoice_id")
private Long invoiceId;
@Column("sales_channel_id")
private Long salesChannelId;
@Column("channel_name")
private String channelName;
@Column("settlement_ref")
private String settlementRef;
@Column("settlement_date")
private LocalDate settlementDate;
@Column("gross_amount")
private BigDecimal grossAmount;
@Column("commission_fee")
@Builder.Default
private BigDecimal commissionFee = BigDecimal.ZERO;
@Column("shipping_fee")
@Builder.Default
private BigDecimal shippingFee = BigDecimal.ZERO;
@Column("other_fees")
@Builder.Default
private BigDecimal otherFees = BigDecimal.ZERO;
@Column("fee_gst")
@Builder.Default
private BigDecimal feeGst = BigDecimal.ZERO;
@Column("tcs_gst_amount")
@Builder.Default
private BigDecimal tcsGstAmount = BigDecimal.ZERO;
@Column("tds_amount")
@Builder.Default
private BigDecimal tdsAmount = BigDecimal.ZERO;
@Column("total_deductions")
private BigDecimal totalDeductions;
@Column("net_payout_amount")
private BigDecimal netPayoutAmount;
@Column("payout_wallet_id")
private Long payoutWalletId;
@Column("status")
@Builder.Default
private String status = "SETTLED";
@Column("notes")
private String notes;
@Column("created_at")
private LocalDateTime createdAt;
@Column("updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,9 @@
package com.kifi.api.repository.invoice;
import com.kifi.api.entity.invoice.CreditNoteItem;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
public interface CreditNoteItemRepository extends ReactiveCrudRepository<CreditNoteItem, Long> {
Flux<CreditNoteItem> findByCreditNoteId(Long creditNoteId);
}

View File

@@ -0,0 +1,13 @@
package com.kifi.api.repository.invoice;
import com.kifi.api.entity.invoice.CreditNote;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface CreditNoteRepository extends ReactiveCrudRepository<CreditNote, Long> {
Flux<CreditNote> findByUserId(Long userId);
Flux<CreditNote> findByUserIdAndInvoiceId(Long userId, Long invoiceId);
Flux<CreditNote> findByUserIdAndCustomerId(Long userId, Long customerId);
Mono<CreditNote> findByUserIdAndCreditNoteNumber(Long userId, String creditNoteNumber);
}

View File

@@ -0,0 +1,13 @@
package com.kifi.api.repository.invoice;
import com.kifi.api.entity.invoice.MarketplaceSettlement;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface MarketplaceSettlementRepository extends ReactiveCrudRepository<MarketplaceSettlement, Long> {
Flux<MarketplaceSettlement> findByUserId(Long userId);
Flux<MarketplaceSettlement> findByUserIdAndInvoiceId(Long userId, Long invoiceId);
Flux<MarketplaceSettlement> findByUserIdAndSalesChannelId(Long userId, Long salesChannelId);
Mono<MarketplaceSettlement> findByUserIdAndSettlementRef(Long userId, String settlementRef);
}

View File

@@ -127,6 +127,12 @@ public class ProductService {
existingProduct.setMakingCharges(updatedProduct.getMakingCharges());
existingProduct.setMakingChargesType(updatedProduct.getMakingChargesType());
existingProduct.setWastagePercentage(updatedProduct.getWastagePercentage());
existingProduct.setMinStock(updatedProduct.getMinStock());
existingProduct.setReorderLevel(updatedProduct.getReorderLevel());
existingProduct.setReorderQuantity(updatedProduct.getReorderQuantity());
existingProduct.setTrackInventory(updatedProduct.getTrackInventory());
existingProduct.setIsActive(updatedProduct.getIsActive());
existingProduct.setUpdatedAt(LocalDateTime.now());
return productRepository.save(existingProduct);

View File

@@ -0,0 +1,239 @@
package com.kifi.api.service.invoice;
import com.kifi.api.entity.Transaction;
import com.kifi.api.entity.inventory.InventoryItem;
import com.kifi.api.entity.invoice.CreditNote;
import com.kifi.api.entity.invoice.CreditNoteItem;
import com.kifi.api.entity.invoice.Invoice;
import com.kifi.api.repository.customer.CustomerRepository;
import com.kifi.api.repository.inventory.InventoryItemRepository;
import com.kifi.api.repository.invoice.CreditNoteItemRepository;
import com.kifi.api.repository.invoice.CreditNoteRepository;
import com.kifi.api.repository.invoice.InvoiceItemRepository;
import com.kifi.api.repository.invoice.InvoiceRepository;
import com.kifi.api.service.TransactionService;
import com.kifi.api.service.accounting.LedgerService;
import com.kifi.api.service.inventory.InventoryItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional
public class CreditNoteService {
private final CreditNoteRepository creditNoteRepository;
private final CreditNoteItemRepository creditNoteItemRepository;
private final InvoiceRepository invoiceRepository;
private final InvoiceItemRepository invoiceItemRepository;
private final InventoryItemRepository inventoryItemRepository;
private final InventoryItemService inventoryItemService;
private final LedgerService ledgerService;
private final CustomerRepository customerRepository;
private final TransactionService transactionService;
public Flux<CreditNote> getCreditNotes(Long userId) {
return creditNoteRepository.findByUserId(userId)
.flatMap(this::populateItems);
}
public Flux<CreditNote> getCreditNotesForInvoice(Long invoiceId, Long userId) {
return creditNoteRepository.findByUserIdAndInvoiceId(userId, invoiceId)
.flatMap(this::populateItems);
}
public Mono<CreditNote> getCreditNoteById(Long id, Long userId) {
return creditNoteRepository.findById(id)
.filter(cn -> cn.getUserId().equals(userId))
.switchIfEmpty(Mono.error(new RuntimeException("Credit note not found or unauthorized")))
.flatMap(this::populateItems);
}
private Mono<CreditNote> populateItems(CreditNote cn) {
return creditNoteItemRepository.findByCreditNoteId(cn.getId())
.collectList()
.map(items -> {
cn.setItems(items);
return cn;
});
}
public Mono<CreditNote> createCreditNote(Long userId, Long invoiceId, CreditNote creditNote) {
return invoiceRepository.findById(invoiceId)
.filter(inv -> inv.getUserId().equals(userId))
.switchIfEmpty(Mono.error(new RuntimeException("Invoice not found or unauthorized")))
.flatMap(invoice -> {
creditNote.setUserId(userId);
creditNote.setInvoiceId(invoiceId);
creditNote.setCustomerId(invoice.getCustomerId());
if (creditNote.getReturnDate() == null) {
creditNote.setReturnDate(LocalDate.now());
}
if (creditNote.getCreditNoteNumber() == null || creditNote.getCreditNoteNumber().trim().isEmpty()) {
creditNote.setCreditNoteNumber("CN-" + System.currentTimeMillis() % 1000000);
}
creditNote.setCreatedAt(LocalDateTime.now());
creditNote.setUpdatedAt(LocalDateTime.now());
if (creditNote.getRefundStatus() == null || creditNote.getRefundStatus().trim().isEmpty()) {
creditNote.setRefundStatus("ADJUSTED_TO_AR");
}
// Calculate totals from items if not provided
List<CreditNoteItem> items = creditNote.getItems() != null ? creditNote.getItems() : new ArrayList<>();
BigDecimal subtotal = BigDecimal.ZERO;
BigDecimal taxTotal = BigDecimal.ZERO;
BigDecimal cgstTotal = BigDecimal.ZERO;
BigDecimal sgstTotal = BigDecimal.ZERO;
BigDecimal igstTotal = BigDecimal.ZERO;
BigDecimal totalAmount = BigDecimal.ZERO;
for (CreditNoteItem itm : items) {
BigDecimal qty = itm.getWeight() != null && itm.getWeight().compareTo(BigDecimal.ZERO) > 0
? itm.getWeight()
: (itm.getQuantity() != null ? itm.getQuantity() : BigDecimal.ONE);
BigDecimal unitPrice = itm.getUnitPrice() != null ? itm.getUnitPrice() : BigDecimal.ZERO;
BigDecimal baseAmt = qty.multiply(unitPrice);
BigDecimal making = itm.getMakingCharge() != null ? itm.getMakingCharge() : BigDecimal.ZERO;
BigDecimal disc = itm.getDiscount() != null ? itm.getDiscount() : BigDecimal.ZERO;
BigDecimal other = itm.getOtherCharges() != null ? itm.getOtherCharges() : BigDecimal.ZERO;
BigDecimal cgst = itm.getCgst() != null ? itm.getCgst() : BigDecimal.ZERO;
BigDecimal sgst = itm.getSgst() != null ? itm.getSgst() : BigDecimal.ZERO;
BigDecimal igst = itm.getIgst() != null ? itm.getIgst() : BigDecimal.ZERO;
BigDecimal tax = cgst.add(sgst).add(igst);
BigDecimal lineTotal = baseAmt.add(making).add(other).subtract(disc).add(tax);
itm.setTotal(lineTotal);
subtotal = subtotal.add(baseAmt);
taxTotal = taxTotal.add(tax);
cgstTotal = cgstTotal.add(cgst);
sgstTotal = sgstTotal.add(sgst);
igstTotal = igstTotal.add(igst);
totalAmount = totalAmount.add(lineTotal);
}
if (creditNote.getSubtotal() == null) creditNote.setSubtotal(subtotal);
if (creditNote.getTaxTotal() == null) creditNote.setTaxTotal(taxTotal);
if (creditNote.getCgstTotal() == null) creditNote.setCgstTotal(cgstTotal);
if (creditNote.getSgstTotal() == null) creditNote.setSgstTotal(sgstTotal);
if (creditNote.getIgstTotal() == null) creditNote.setIgstTotal(igstTotal);
if (creditNote.getTotalAmount() == null) creditNote.setTotalAmount(totalAmount);
return creditNoteRepository.save(creditNote)
.flatMap(savedCn -> {
Mono<List<CreditNoteItem>> itemsMono = Flux.fromIterable(items)
.flatMap(item -> {
item.setCreditNoteId(savedCn.getId());
return creditNoteItemRepository.save(item);
})
.collectList();
return itemsMono.flatMap(savedItems -> {
savedCn.setItems(savedItems);
// 1. Stock Restoration (if restockItems is true)
Mono<Void> restockMono = Mono.empty();
if (Boolean.TRUE.equals(savedCn.getRestockItems())) {
restockMono = Flux.fromIterable(savedItems)
.flatMap(item -> {
// Inward restock back into inventory
if (item.getInventoryItemId() != null) {
return inventoryItemRepository.findById(item.getInventoryItemId())
.flatMap(invItem -> {
invItem.setStatus("AVAILABLE");
invItem.setSalesRef(null);
invItem.setUpdatedAt(LocalDateTime.now());
return inventoryItemRepository.save(invItem).then();
});
} else if (item.getProductId() != null) {
BigDecimal restockQty = item.getWeight() != null && item.getWeight().compareTo(BigDecimal.ZERO) > 0
? item.getWeight()
: (item.getQuantity() != null ? item.getQuantity() : BigDecimal.ONE);
InventoryItem restockRecord = InventoryItem.builder()
.userId(userId)
.productId(item.getProductId())
.sku(item.getSku())
.huid(item.getHuid())
.grossWeight(restockQty)
.netWeight(restockQty)
.purchaseCost(item.getUnitPrice())
.purchaseRef("RETURN-" + savedCn.getCreditNoteNumber())
.status("AVAILABLE")
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
return inventoryItemRepository.save(restockRecord).then();
}
return Mono.empty();
})
.then();
}
// 2. Financial / Ledger Accounting Adjustment
Mono<Void> accountingMono = Mono.empty();
if ("REFUNDED_TO_WALLET".equalsIgnoreCase(savedCn.getRefundStatus()) && savedCn.getRefundWalletId() != null) {
Transaction refundTx = Transaction.builder()
.userId(userId)
.fromWalletId(savedCn.getRefundWalletId()) // Cash/Bank decreases
.type("EXPENSE")
.amount(savedCn.getTotalAmount())
.date(savedCn.getReturnDate())
.description("Refund for Return #" + savedCn.getCreditNoteNumber() + " (Invoice #" + invoice.getInvoiceNumber() + ")")
.createdAt(LocalDateTime.now())
.build();
accountingMono = transactionService.addTransaction(userId, refundTx).then();
} else {
// Customer AR adjustment
if (invoice.getCustomerId() != null) {
accountingMono = customerRepository.findById(invoice.getCustomerId())
.flatMap(customer -> Mono.zip(
ledgerService.getCustomerLedger(userId, customer.getId(), customer.getName()),
ledgerService.getSalesRevenueLedger(userId)
).flatMap(ledgers -> {
Transaction returnTx = Transaction.builder()
.userId(userId)
.fromWalletId(ledgers.getT1().getId()) // Customer AR (Asset decreases)
.toWalletId(ledgers.getT2().getId()) // Sales Revenue (Income decreases)
.type("SALES_RETURN")
.amount(savedCn.getTotalAmount())
.date(savedCn.getReturnDate())
.description("Credit Note #" + savedCn.getCreditNoteNumber() + " for Invoice #" + invoice.getInvoiceNumber())
.createdAt(LocalDateTime.now())
.build();
return transactionService.addTransaction(userId, returnTx).then();
})).onErrorResume(err -> {
System.err.println("AR adjustment error: " + err.getMessage());
return Mono.empty();
});
}
}
// 3. Update Invoice Status
invoice.setStatus(savedCn.getTotalAmount().compareTo(invoice.getTotalAmount()) >= 0 ? "RETURNED" : "PARTIALLY_RETURNED");
if ("RTO_UNDELIVERED".equalsIgnoreCase(savedCn.getReason())) {
invoice.setDispatchStatus("RTO");
}
invoice.setUpdatedAt(LocalDateTime.now());
Mono<Invoice> updateInvoiceMono = invoiceRepository.save(invoice);
return restockMono
.then(accountingMono)
.then(updateInvoiceMono)
.thenReturn(savedCn);
});
});
});
}
}

View File

@@ -0,0 +1,128 @@
package com.kifi.api.service.invoice;
import com.kifi.api.entity.Transaction;
import com.kifi.api.entity.invoice.Invoice;
import com.kifi.api.entity.invoice.InvoicePayment;
import com.kifi.api.entity.invoice.MarketplaceSettlement;
import com.kifi.api.repository.customer.CustomerRepository;
import com.kifi.api.repository.invoice.InvoicePaymentRepository;
import com.kifi.api.repository.invoice.InvoiceRepository;
import com.kifi.api.repository.invoice.MarketplaceSettlementRepository;
import com.kifi.api.service.TransactionService;
import com.kifi.api.service.accounting.LedgerService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
@Transactional
public class MarketplaceSettlementService {
private final MarketplaceSettlementRepository settlementRepository;
private final InvoiceRepository invoiceRepository;
private final InvoicePaymentRepository invoicePaymentRepository;
private final CustomerRepository customerRepository;
private final LedgerService ledgerService;
private final TransactionService transactionService;
public Flux<MarketplaceSettlement> getSettlements(Long userId) {
return settlementRepository.findByUserId(userId);
}
public Flux<MarketplaceSettlement> getSettlementsForInvoice(Long invoiceId, Long userId) {
return settlementRepository.findByUserIdAndInvoiceId(userId, invoiceId);
}
public Mono<MarketplaceSettlement> getSettlementById(Long id, Long userId) {
return settlementRepository.findById(id)
.filter(s -> s.getUserId().equals(userId))
.switchIfEmpty(Mono.error(new RuntimeException("Settlement not found or unauthorized")));
}
public Mono<MarketplaceSettlement> createSettlement(Long userId, Long invoiceId, MarketplaceSettlement settlement) {
return invoiceRepository.findById(invoiceId)
.filter(inv -> inv.getUserId().equals(userId))
.switchIfEmpty(Mono.error(new RuntimeException("Invoice not found or unauthorized")))
.flatMap(invoice -> {
settlement.setUserId(userId);
settlement.setInvoiceId(invoiceId);
if (settlement.getSalesChannelId() == null) {
settlement.setSalesChannelId(invoice.getSalesChannelId());
}
if (settlement.getChannelName() == null || settlement.getChannelName().trim().isEmpty()) {
settlement.setChannelName(invoice.getSalesChannel() != null ? invoice.getSalesChannel() : "Marketplace");
}
if (settlement.getSettlementDate() == null) {
settlement.setSettlementDate(LocalDate.now());
}
if (settlement.getGrossAmount() == null) {
settlement.setGrossAmount(invoice.getTotalAmount());
}
if (settlement.getCommissionFee() == null) settlement.setCommissionFee(BigDecimal.ZERO);
if (settlement.getShippingFee() == null) settlement.setShippingFee(BigDecimal.ZERO);
if (settlement.getOtherFees() == null) settlement.setOtherFees(BigDecimal.ZERO);
if (settlement.getFeeGst() == null) settlement.setFeeGst(BigDecimal.ZERO);
if (settlement.getTcsGstAmount() == null) settlement.setTcsGstAmount(BigDecimal.ZERO);
if (settlement.getTdsAmount() == null) settlement.setTdsAmount(BigDecimal.ZERO);
BigDecimal totalDeductions = settlement.getCommissionFee()
.add(settlement.getShippingFee())
.add(settlement.getOtherFees())
.add(settlement.getFeeGst())
.add(settlement.getTcsGstAmount())
.add(settlement.getTdsAmount());
settlement.setTotalDeductions(totalDeductions);
BigDecimal netPayout = settlement.getGrossAmount().subtract(totalDeductions);
settlement.setNetPayoutAmount(netPayout);
settlement.setStatus("SETTLED");
settlement.setCreatedAt(LocalDateTime.now());
settlement.setUpdatedAt(LocalDateTime.now());
return settlementRepository.save(settlement)
.flatMap(savedSettlement -> {
// 1. Record Bank Inflow Transaction
Mono<Void> txMono = Mono.empty();
if (savedSettlement.getPayoutWalletId() != null && netPayout.compareTo(BigDecimal.ZERO) > 0) {
Transaction netDepositTx = Transaction.builder()
.userId(userId)
.toWalletId(savedSettlement.getPayoutWalletId()) // Cash/Bank increases
.type("INCOME")
.amount(netPayout)
.date(savedSettlement.getSettlementDate())
.description("Payout for " + savedSettlement.getChannelName() + " Order #" + (invoice.getMarketplaceOrderId() != null ? invoice.getMarketplaceOrderId() : invoice.getInvoiceNumber()) + " (Ref: " + (savedSettlement.getSettlementRef() != null ? savedSettlement.getSettlementRef() : "Settlement") + ")")
.createdAt(LocalDateTime.now())
.build();
txMono = transactionService.addTransaction(userId, netDepositTx).then();
}
// 2. Record Invoice Payment (Clears Invoice Receivables)
InvoicePayment payment = InvoicePayment.builder()
.invoiceId(invoiceId)
.amount(invoice.getTotalAmount())
.paymentDate(savedSettlement.getSettlementDate())
.createdAt(LocalDateTime.now())
.build();
Mono<InvoicePayment> paymentMono = invoicePaymentRepository.save(payment);
// 3. Mark Invoice as PAID / SETTLED
invoice.setStatus("PAID");
invoice.setUpdatedAt(LocalDateTime.now());
Mono<Invoice> updateInvoiceMono = invoiceRepository.save(invoice);
return txMono
.then(paymentMono)
.then(updateInvoiceMono)
.thenReturn(savedSettlement);
});
});
}
}

View File

@@ -272,6 +272,7 @@ CREATE TABLE IF NOT EXISTS products (
wastage_percentage DECIMAL(5, 2) DEFAULT 0.0,
min_stock DECIMAL(10, 2) DEFAULT 0,
reorder_level DECIMAL(10, 2) DEFAULT 0,
reorder_quantity DECIMAL(10, 2) DEFAULT 0,
track_inventory BOOLEAN DEFAULT TRUE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@@ -495,6 +496,76 @@ CREATE TABLE IF NOT EXISTS invoice_payments (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS credit_notes (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
invoice_id INTEGER REFERENCES invoices(id) ON DELETE CASCADE,
credit_note_number VARCHAR(100) NOT NULL,
customer_id INTEGER REFERENCES customers(id) ON DELETE SET NULL,
return_date DATE NOT NULL,
reason VARCHAR(100) NOT NULL,
restock_items BOOLEAN DEFAULT TRUE,
subtotal DECIMAL(15, 2) NOT NULL,
tax_total DECIMAL(15, 2) DEFAULT 0.0,
cgst_total DECIMAL(15, 2) DEFAULT 0.0,
sgst_total DECIMAL(15, 2) DEFAULT 0.0,
igst_total DECIMAL(15, 2) DEFAULT 0.0,
discount_total DECIMAL(15, 2) DEFAULT 0.0,
total_amount DECIMAL(15, 2) NOT NULL,
refund_status VARCHAR(50) DEFAULT 'ADJUSTED_TO_AR',
refund_wallet_id INTEGER REFERENCES wallets(id) ON DELETE SET NULL,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS credit_note_items (
id SERIAL PRIMARY KEY,
credit_note_id INTEGER REFERENCES credit_notes(id) ON DELETE CASCADE,
invoice_item_id INTEGER REFERENCES invoice_items(id) ON DELETE SET NULL,
product_id INTEGER REFERENCES products(id) ON DELETE SET NULL,
inventory_item_id INTEGER REFERENCES inventory_items(id) ON DELETE SET NULL,
product_name VARCHAR(255),
sku VARCHAR(100),
huid VARCHAR(50),
hsn_code VARCHAR(50),
quantity DECIMAL(10, 3) NOT NULL,
weight DECIMAL(10, 3),
unit_price DECIMAL(15, 2) NOT NULL,
tax_rate DECIMAL(5, 2) DEFAULT 0.0,
cgst DECIMAL(15, 2) DEFAULT 0.0,
sgst DECIMAL(15, 2) DEFAULT 0.0,
igst DECIMAL(15, 2) DEFAULT 0.0,
discount DECIMAL(15, 2) DEFAULT 0.0,
making_charge DECIMAL(15, 2) DEFAULT 0.0,
other_charges DECIMAL(15, 2) DEFAULT 0.0,
total DECIMAL(15, 2) NOT NULL
);
CREATE TABLE IF NOT EXISTS marketplace_settlements (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
invoice_id INTEGER REFERENCES invoices(id) ON DELETE CASCADE,
sales_channel_id INTEGER REFERENCES sales_channels(id) ON DELETE SET NULL,
channel_name VARCHAR(100),
settlement_ref VARCHAR(100),
settlement_date DATE NOT NULL,
gross_amount DECIMAL(15, 2) NOT NULL,
commission_fee DECIMAL(15, 2) DEFAULT 0.0,
shipping_fee DECIMAL(15, 2) DEFAULT 0.0,
other_fees DECIMAL(15, 2) DEFAULT 0.0,
fee_gst DECIMAL(15, 2) DEFAULT 0.0,
tcs_gst_amount DECIMAL(15, 2) DEFAULT 0.0,
tds_amount DECIMAL(15, 2) DEFAULT 0.0,
total_deductions DECIMAL(15, 2) NOT NULL,
net_payout_amount DECIMAL(15, 2) NOT NULL,
payout_wallet_id INTEGER REFERENCES wallets(id) ON DELETE SET NULL,
status VARCHAR(50) DEFAULT 'SETTLED',
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 9. Projects & Task Management
CREATE TABLE IF NOT EXISTS projects (
id SERIAL PRIMARY KEY,