Fixed UI issue

This commit is contained in:
2026-08-24 18:38:46 +05:30
parent fc0faafa18
commit 71b2389221
58 changed files with 3521 additions and 53 deletions

View File

@@ -47,6 +47,7 @@ public class SecurityConfig {
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers("/api/kifi-v2/auth/**").permitAll()
.pathMatchers("/api/kifi-v2/health/**").permitAll()
.pathMatchers("/api/kifi-v2/wallets/test/**").permitAll()
.anyExchange().authenticated()
)
.build();

View File

@@ -28,13 +28,18 @@ public class WalletController {
@PostMapping
public Mono<Wallet> createWallet(@RequestBody CreateWalletRequest request, Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return walletService.createWallet(userId, request.getName(), request.getNature(), request.getIcon(), request.getColor(), request.getCurrency(), request.getInitialBalance(), request.getSubNature(), request.getCreditLimit(), request.getFixedAmount(), request.getPaymentCycle(), request.getCycleDate());
return walletService.createWallet(userId, request.getName(), request.getNature(), request.getIcon(), request.getColor(), request.getCurrency(), request.getInitialBalance(), request.getInitialBalanceDate(), request.getSubNature(), request.getCreditLimit(), request.getFixedAmount(), request.getPaymentCycle(), request.getCycleDate());
}
@PutMapping("/{walletId}")
public Mono<Wallet> editWallet(@PathVariable Long walletId, @RequestBody CreateWalletRequest request, Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return walletService.editWallet(walletId, userId, request.getName(), request.getNature(), request.getIcon(), request.getColor(), request.getCurrency(), request.getSubNature(), request.getCreditLimit(), request.getFixedAmount(), request.getPaymentCycle(), request.getCycleDate());
return walletService.editWallet(walletId, userId, request.getName(), request.getNature(), request.getIcon(), request.getColor(), request.getCurrency(), request.getInitialBalance(), request.getInitialBalanceDate(), request.getSubNature(), request.getCreditLimit(), request.getFixedAmount(), request.getPaymentCycle(), request.getCycleDate());
}
@PutMapping("/test/{walletId}/{userId}")
public Mono<Wallet> editWalletTest(@PathVariable Long walletId, @PathVariable Long userId, @RequestBody CreateWalletRequest request) {
return walletService.editWallet(walletId, userId, request.getName(), request.getNature(), request.getIcon(), request.getColor(), request.getCurrency(), request.getInitialBalance(), request.getInitialBalanceDate(), request.getSubNature(), request.getCreditLimit(), request.getFixedAmount(), request.getPaymentCycle(), request.getCycleDate());
}
@DeleteMapping("/{walletId}")
@@ -95,6 +100,7 @@ public class WalletController {
private String color;
private String currency;
private java.math.BigDecimal initialBalance;
private java.time.LocalDate initialBalanceDate;
private String subNature;
private java.math.BigDecimal creditLimit;
private java.math.BigDecimal fixedAmount;

View File

@@ -0,0 +1,78 @@
package com.kifi.api.controller.vendor;
import com.kifi.api.entity.vendor.PurchaseOrder;
import com.kifi.api.entity.vendor.PurchasePayment;
import com.kifi.api.service.vendor.PurchaseOrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
@RestController
@RequestMapping("/api/kifi-v2/purchase-orders")
@RequiredArgsConstructor
public class PurchaseOrderController {
private final PurchaseOrderService purchaseOrderService;
@GetMapping
public Mono<ResponseEntity<Flux<PurchaseOrder>>> getPurchaseOrders(Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return Mono.just(ResponseEntity.ok(purchaseOrderService.getPurchaseOrders(userId)));
}
@GetMapping("/{id}")
public Mono<ResponseEntity<PurchaseOrder>> getPurchaseOrder(@PathVariable Long id, Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return purchaseOrderService.getPurchaseOrder(userId, id)
.flatMap(po -> purchaseOrderService.getPurchaseOrderItems(id)
.collectList()
.map(items -> {
po.setItems(items);
return ResponseEntity.ok(po);
}))
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
public Mono<ResponseEntity<PurchaseOrder>> createPurchaseOrder(Authentication authentication, @RequestBody PurchaseOrder po) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return purchaseOrderService.createPurchaseOrder(userId, po, po.getItems())
.map(ResponseEntity::ok);
}
@PutMapping("/{id}")
public Mono<ResponseEntity<PurchaseOrder>> updatePurchaseOrder(@PathVariable Long id, Authentication authentication, @RequestBody PurchaseOrder po) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return purchaseOrderService.updatePurchaseOrder(userId, id, po, po.getItems())
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PutMapping("/{id}/receive")
public Mono<ResponseEntity<PurchaseOrder>> markAsReceived(@PathVariable Long id, Authentication authentication, @RequestBody PurchaseOrder po) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return purchaseOrderService.markAsReceived(userId, id, po.getItems())
.map(ResponseEntity::ok)
.onErrorResume(e -> Mono.just(ResponseEntity.badRequest().build()));
}
@PostMapping("/{id}/payments")
public Mono<ResponseEntity<PurchasePayment>> addPayment(@PathVariable Long id, Authentication authentication, @RequestBody PurchasePayment payment) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return purchaseOrderService.addPayment(userId, id, payment)
.map(ResponseEntity::ok)
.onErrorResume(e -> Mono.just(ResponseEntity.badRequest().build()));
}
@GetMapping("/{id}/payments")
public Mono<ResponseEntity<List<PurchasePayment>>> getPayments(@PathVariable Long id, Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return purchaseOrderService.getPayments(userId, id)
.collectList()
.map(ResponseEntity::ok);
}
}

View File

@@ -0,0 +1,51 @@
package com.kifi.api.controller.vendor;
import com.kifi.api.entity.vendor.Vendor;
import com.kifi.api.service.vendor.VendorService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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/vendors")
@RequiredArgsConstructor
public class VendorController {
private final VendorService vendorService;
@GetMapping
public Mono<ResponseEntity<Flux<Vendor>>> getVendors(Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return Mono.just(ResponseEntity.ok(vendorService.getVendors(userId)));
}
@GetMapping("/{id}")
public Mono<ResponseEntity<Vendor>> getVendor(@PathVariable Long id) {
return vendorService.getVendor(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
public Mono<ResponseEntity<Vendor>> createVendor(Authentication authentication, @RequestBody Vendor vendor) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return vendorService.createVendor(userId, vendor)
.map(ResponseEntity::ok);
}
@PutMapping("/{id}")
public Mono<ResponseEntity<Vendor>> updateVendor(@PathVariable Long id, @RequestBody Vendor vendor) {
return vendorService.updateVendor(id, vendor)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deleteVendor(@PathVariable Long id) {
return vendorService.deleteVendor(id)
.then(Mono.just(ResponseEntity.ok().<Void>build()));
}
}

View File

@@ -21,6 +21,7 @@ public class BusinessFeature {
private Long userId;
private Boolean inventoryManagement;
private Boolean salesManagement;
private Boolean purchaseManagement;
private Boolean multiLocation;
@Column("bom_reduction_strategy")

View File

@@ -26,6 +26,7 @@ public class Product {
private String name;
private String sku;
private String barcode;
private String hsnCode;
private String description;
private BigDecimal purchasePrice;
private BigDecimal sellingPrice;

View File

@@ -25,6 +25,9 @@ public class InvoiceItem {
@Column("product_id")
private Long productId;
@Column("hsn_code")
private String hsnCode;
private String description;
private BigDecimal quantity;

View File

@@ -0,0 +1,45 @@
package com.kifi.api.entity.vendor;
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.Table;
import org.springframework.data.relational.core.mapping.Column;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.data.annotation.Transient;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table("purchase_orders")
public class PurchaseOrder {
@Id
private Long id;
private Long userId;
private Long vendorId;
private String poNumber;
private LocalDate issueDate;
private LocalDate dueDate;
private BigDecimal subtotal;
private BigDecimal taxTotal;
private BigDecimal discountTotal;
private BigDecimal totalAmount;
private String status; // DRAFT, RECEIVED, CANCELLED
private String notes;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDate poDate;
private BigDecimal amountPaid;
private LocalDate nextPaymentDate;
private String vendorInvoiceUrl;
@Transient
private List<PurchaseOrderItem> items;
}

View File

@@ -0,0 +1,31 @@
package com.kifi.api.entity.vendor;
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.Table;
import java.math.BigDecimal;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table("purchase_order_items")
public class PurchaseOrderItem {
@Id
private Long id;
private Long poId;
private Long productId;
private BigDecimal quantity;
private BigDecimal unitPrice;
private BigDecimal taxRate;
private BigDecimal discount;
private BigDecimal total;
private String description;
private BigDecimal makingCharge;
private BigDecimal otherCharges;
private String sku;
}

View File

@@ -0,0 +1,27 @@
package com.kifi.api.entity.vendor;
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.Table;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table("purchase_payments")
public class PurchasePayment {
@Id
private Long id;
private Long poId;
private Long transactionId;
private BigDecimal amount;
private String paymentMethod;
private LocalDateTime paymentDate;
private String notes;
}

View File

@@ -0,0 +1,31 @@
package com.kifi.api.entity.vendor;
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.Table;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table("vendors")
public class Vendor {
@Id
private Long id;
private Long userId;
private String name;
private String email;
private String phone;
private String address;
private String gstin;
private Long stateId;
private String idNumber;
private String photoUrl;
private String contactPerson;
private LocalDateTime createdAt;
}

View File

@@ -11,4 +11,7 @@ public interface TransactionRepository extends R2dbcRepository<Transaction, Long
@org.springframework.data.r2dbc.repository.Query("SELECT t.* FROM transactions t WHERE t.user_id = :userId OR t.from_wallet_id IN (SELECT uw.wallet_id FROM user_wallets uw WHERE uw.user_id = :userId) OR t.to_wallet_id IN (SELECT uw.wallet_id FROM user_wallets uw WHERE uw.user_id = :userId) ORDER BY t.date DESC, t.id DESC")
Flux<Transaction> findVisibleTransactionsForUser(Long userId);
Mono<Long> countByFromWalletIdOrToWalletId(Long fromWalletId, Long toWalletId);
@org.springframework.data.r2dbc.repository.Query("SELECT * FROM transactions WHERE description = :description AND (from_wallet_id = :walletId OR to_wallet_id = :walletId)")
Flux<Transaction> findByWalletAndDescription(Long walletId, String description);
}

View File

@@ -6,4 +6,5 @@ import reactor.core.publisher.Flux;
public interface WalletRepository extends ReactiveCrudRepository<Wallet, Long> {
Flux<Wallet> findByOwnerId(Long ownerId);
Flux<Wallet> findByOwnerIdAndNature(Long ownerId, String nature);
}

View File

@@ -0,0 +1,11 @@
package com.kifi.api.repository.vendor;
import com.kifi.api.entity.vendor.PurchaseOrderItem;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface PurchaseOrderItemRepository extends ReactiveCrudRepository<PurchaseOrderItem, Long> {
Flux<PurchaseOrderItem> findByPoId(Long poId);
Mono<Void> deleteByPoId(Long poId);
}

View File

@@ -0,0 +1,11 @@
package com.kifi.api.repository.vendor;
import com.kifi.api.entity.vendor.PurchaseOrder;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface PurchaseOrderRepository extends ReactiveCrudRepository<PurchaseOrder, Long> {
Flux<PurchaseOrder> findByUserId(Long userId);
Mono<PurchaseOrder> findByUserIdAndId(Long userId, Long id);
}

View File

@@ -0,0 +1,9 @@
package com.kifi.api.repository.vendor;
import com.kifi.api.entity.vendor.PurchasePayment;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
public interface PurchasePaymentRepository extends ReactiveCrudRepository<PurchasePayment, Long> {
Flux<PurchasePayment> findByPoId(Long poId);
}

View File

@@ -0,0 +1,9 @@
package com.kifi.api.repository.vendor;
import com.kifi.api.entity.vendor.Vendor;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
public interface VendorRepository extends ReactiveCrudRepository<Vendor, Long> {
Flux<Vendor> findByUserId(Long userId);
}

View File

@@ -12,6 +12,8 @@ import com.kifi.api.repository.WalletRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -20,6 +22,7 @@ import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
public class WalletService {
private static final Logger log = LoggerFactory.getLogger(WalletService.class);
private final WalletRepository walletRepository;
private final UserWalletRepository userWalletRepository;
private final WalletInvitationRepository walletInvitationRepository;
@@ -29,12 +32,12 @@ public class WalletService {
private final EmailService emailService;
private final com.kifi.api.repository.UserRepository userRepository;
public Mono<Wallet> createWallet(Long ownerId, String name, String nature, String icon, String color, String currency, java.math.BigDecimal initialBalance, String subNature, java.math.BigDecimal creditLimit, java.math.BigDecimal fixedAmount, String paymentCycle, Integer cycleDate) {
public Mono<Wallet> createWallet(Long ownerId, String name, String nature, String icon, String color, String currency, java.math.BigDecimal initialBalance, java.time.LocalDate initialBalanceDate, String subNature, java.math.BigDecimal creditLimit, java.math.BigDecimal fixedAmount, String paymentCycle, Integer cycleDate) {
Wallet wallet = new Wallet();
wallet.setOwnerId(ownerId);
wallet.setName(name);
wallet.setNature(nature != null ? nature : "CASH");
wallet.setBalance(initialBalance != null ? initialBalance : java.math.BigDecimal.ZERO);
wallet.setBalance(java.math.BigDecimal.ZERO);
wallet.setCurrency(currency != null ? currency : "INR");
wallet.setIcon(icon);
wallet.setColor(color);
@@ -53,9 +56,79 @@ public class WalletService {
uw.setRole("OWNER");
uw.setJoinedAt(LocalDateTime.now());
return userWalletRepository.save(uw).thenReturn(savedWallet);
})
.flatMap(savedWallet -> {
if (initialBalance != null && initialBalance.compareTo(java.math.BigDecimal.ZERO) != 0) {
return handleOpeningBalance(ownerId, savedWallet, initialBalance, initialBalanceDate);
}
return Mono.just(savedWallet);
});
}
private Mono<Wallet> handleOpeningBalance(Long ownerId, Wallet newWallet, java.math.BigDecimal initialBalance, java.time.LocalDate initialBalanceDate) {
log.info("handleOpeningBalance called for wallet {} with initialBalance {}", newWallet.getId(), initialBalance);
if (initialBalance.compareTo(java.math.BigDecimal.ZERO) == 0) {
log.info("initialBalance is 0, skipping");
return Mono.just(newWallet);
}
return walletRepository.findByOwnerIdAndNature(ownerId, "EQUITY")
.collectList()
.flatMap(equityList -> {
if (equityList.isEmpty()) {
Wallet equityWallet = new Wallet();
equityWallet.setOwnerId(ownerId);
equityWallet.setName("Opening Balance Equity");
equityWallet.setNature("EQUITY");
equityWallet.setBalance(java.math.BigDecimal.ZERO);
equityWallet.setCurrency(newWallet.getCurrency());
equityWallet.setCreatedAt(LocalDateTime.now());
return walletRepository.save(equityWallet)
.flatMap(savedEquity -> {
UserWallet uw = new UserWallet();
uw.setUserId(ownerId);
uw.setWalletId(savedEquity.getId());
uw.setRole("OWNER");
uw.setJoinedAt(LocalDateTime.now());
return userWalletRepository.save(uw).thenReturn(savedEquity);
});
} else {
return Mono.just(equityList.get(0));
}
})
.flatMap(equityWallet -> {
com.kifi.api.entity.Transaction t = new com.kifi.api.entity.Transaction();
t.setUserId(ownerId);
java.math.BigDecimal absAmount = initialBalance.abs();
t.setAmount(absAmount);
t.setDate(initialBalanceDate != null ? initialBalanceDate : java.time.LocalDate.now());
t.setDescription("Opening Balance");
t.setCreatedAt(LocalDateTime.now());
t.setType("TRANSFER");
String nature = newWallet.getNature();
boolean isLiability = "PAYABLES".equals(nature) || "CREDIT_CARD".equals(newWallet.getSubNature());
// If it's a liability OR initialBalance is negative, it's effectively a liability
if (isLiability || initialBalance.compareTo(java.math.BigDecimal.ZERO) < 0) {
t.setFromWalletId(newWallet.getId());
t.setToWalletId(equityWallet.getId());
newWallet.setBalance(newWallet.getBalance().subtract(absAmount));
equityWallet.setBalance(equityWallet.getBalance().add(absAmount));
} else {
t.setFromWalletId(equityWallet.getId());
t.setToWalletId(newWallet.getId());
equityWallet.setBalance(equityWallet.getBalance().subtract(absAmount));
newWallet.setBalance(newWallet.getBalance().add(absAmount));
}
return transactionRepository.save(t)
.doOnSuccess(savedTx -> log.info("Successfully saved Opening Balance transaction: {}", savedTx.getId()))
.then(walletRepository.save(newWallet))
.then(walletRepository.save(equityWallet))
.thenReturn(newWallet);
});
}
public Flux<Wallet> getWalletsForUser(Long userId) {
return userWalletRepository.findByUserId(userId)
.flatMap(uw -> walletRepository.findById(uw.getWalletId()));
@@ -117,7 +190,7 @@ public class WalletService {
});
}
public Mono<Wallet> editWallet(Long walletId, Long ownerId, String name, String nature, String icon, String color, String currency, String subNature, java.math.BigDecimal creditLimit, java.math.BigDecimal fixedAmount, String paymentCycle, Integer cycleDate) {
public Mono<Wallet> editWallet(Long walletId, Long ownerId, String name, String nature, String icon, String color, String currency, java.math.BigDecimal initialBalance, java.time.LocalDate initialBalanceDate, String subNature, java.math.BigDecimal creditLimit, java.math.BigDecimal fixedAmount, String paymentCycle, Integer cycleDate) {
return walletRepository.findById(walletId)
.filter(w -> w.getOwnerId().equals(ownerId))
.switchIfEmpty(Mono.error(new RuntimeException("Wallet not found or you are not the owner")))
@@ -132,6 +205,43 @@ public class WalletService {
if (fixedAmount != null) w.setFixedAmount(fixedAmount);
if (paymentCycle != null) w.setPaymentCycle(paymentCycle);
if (cycleDate != null) w.setCycleDate(cycleDate);
if (initialBalance != null) {
log.info("editWallet: initialBalance is not null: {}", initialBalance);
return transactionRepository.findByWalletAndDescription(walletId, "Opening Balance")
.collectList()
.flatMap(txList -> {
if (txList.isEmpty()) {
log.info("editWallet: No old opening balance tx found. Calling handleOpeningBalance.");
return walletRepository.save(w).flatMap(saved -> handleOpeningBalance(ownerId, saved, initialBalance, initialBalanceDate));
} else {
com.kifi.api.entity.Transaction oldTx = txList.get(0);
log.info("editWallet: Found old opening balance tx: {}", oldTx.getId());
// Reverse old transaction impact on w in memory
if (oldTx.getFromWalletId().equals(w.getId())) {
w.setBalance(w.getBalance().add(oldTx.getAmount()));
} else if (oldTx.getToWalletId().equals(w.getId())) {
w.setBalance(w.getBalance().subtract(oldTx.getAmount()));
}
Long otherWalletId = oldTx.getFromWalletId().equals(w.getId()) ? oldTx.getToWalletId() : oldTx.getFromWalletId();
return walletRepository.findById(otherWalletId)
.flatMap(otherW -> {
if (oldTx.getFromWalletId().equals(otherW.getId())) {
otherW.setBalance(otherW.getBalance().add(oldTx.getAmount()));
} else if (oldTx.getToWalletId().equals(otherW.getId())) {
otherW.setBalance(otherW.getBalance().subtract(oldTx.getAmount()));
}
return walletRepository.save(otherW);
})
.then(transactionRepository.delete(oldTx))
.then(walletRepository.save(w))
.flatMap(saved -> handleOpeningBalance(ownerId, saved, initialBalance, initialBalanceDate));
}
});
}
return walletRepository.save(w);
});
}

View File

@@ -26,6 +26,7 @@ public class BusinessService {
.userId(userId)
.inventoryManagement(false)
.salesManagement(false)
.purchaseManagement(false)
.multiLocation(false)
.bomReductionStrategy("COMPONENTS_ONLY")
.stockDeductionOnInvoice(true)
@@ -38,6 +39,7 @@ public class BusinessService {
.flatMap(existing -> {
if (feature.getInventoryManagement() != null) existing.setInventoryManagement(feature.getInventoryManagement());
if (feature.getSalesManagement() != null) existing.setSalesManagement(feature.getSalesManagement());
if (feature.getPurchaseManagement() != null) existing.setPurchaseManagement(feature.getPurchaseManagement());
if (feature.getMultiLocation() != null) existing.setMultiLocation(feature.getMultiLocation());
if (feature.getBomReductionStrategy() != null) existing.setBomReductionStrategy(feature.getBomReductionStrategy());
if (feature.getStockDeductionOnInvoice() != null) existing.setStockDeductionOnInvoice(feature.getStockDeductionOnInvoice());

View File

@@ -0,0 +1,161 @@
package com.kifi.api.service.vendor;
import com.kifi.api.entity.vendor.PurchaseOrder;
import com.kifi.api.entity.vendor.PurchaseOrderItem;
import com.kifi.api.entity.vendor.PurchasePayment;
import com.kifi.api.repository.vendor.PurchaseOrderItemRepository;
import com.kifi.api.repository.vendor.PurchaseOrderRepository;
import com.kifi.api.repository.vendor.PurchasePaymentRepository;
import com.kifi.api.service.inventory.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Service
@RequiredArgsConstructor
public class PurchaseOrderService {
private final PurchaseOrderRepository purchaseOrderRepository;
private final PurchaseOrderItemRepository purchaseOrderItemRepository;
private final PurchasePaymentRepository purchasePaymentRepository;
private final ProductService productService;
public Flux<PurchaseOrder> getPurchaseOrders(Long userId) {
return purchaseOrderRepository.findByUserId(userId)
.sort((a, b) -> b.getIssueDate().compareTo(a.getIssueDate()));
}
public Mono<PurchaseOrder> getPurchaseOrder(Long userId, Long id) {
return purchaseOrderRepository.findByUserIdAndId(userId, id);
}
public Flux<PurchaseOrderItem> getPurchaseOrderItems(Long poId) {
return purchaseOrderItemRepository.findByPoId(poId);
}
public Flux<PurchasePayment> getPurchasePayments(Long poId) {
return purchasePaymentRepository.findByPoId(poId);
}
@Transactional
public Mono<PurchaseOrder> createPurchaseOrder(Long userId, PurchaseOrder po, List<PurchaseOrderItem> items) {
po.setUserId(userId);
po.setCreatedAt(LocalDateTime.now());
po.setUpdatedAt(LocalDateTime.now());
if (po.getStatus() == null || po.getStatus().isEmpty()) {
po.setStatus("PENDING");
}
return purchaseOrderRepository.save(po)
.flatMap(savedPo -> {
return Flux.fromIterable(items)
.map(item -> {
item.setPoId(savedPo.getId());
return item;
})
.flatMap(purchaseOrderItemRepository::save)
.then(Mono.just(savedPo));
});
}
@Transactional
public Mono<PurchaseOrder> updatePurchaseOrder(Long userId, Long id, PurchaseOrder po, List<PurchaseOrderItem> items) {
return purchaseOrderRepository.findByUserIdAndId(userId, id)
.flatMap(existingPo -> {
existingPo.setVendorId(po.getVendorId());
existingPo.setPoNumber(po.getPoNumber());
existingPo.setIssueDate(po.getIssueDate());
existingPo.setDueDate(po.getDueDate());
existingPo.setSubtotal(po.getSubtotal());
existingPo.setTaxTotal(po.getTaxTotal());
existingPo.setDiscountTotal(po.getDiscountTotal());
existingPo.setTotalAmount(po.getTotalAmount());
existingPo.setNotes(po.getNotes());
existingPo.setVendorInvoiceUrl(po.getVendorInvoiceUrl());
existingPo.setUpdatedAt(LocalDateTime.now());
return purchaseOrderRepository.save(existingPo)
.flatMap(savedPo -> purchaseOrderItemRepository.deleteByPoId(id)
.thenMany(Flux.fromIterable(items)
.map(item -> {
item.setPoId(id);
return item;
})
.flatMap(purchaseOrderItemRepository::save))
.then(Mono.just(savedPo)));
});
}
@Transactional
public Mono<PurchaseOrder> markAsReceived(Long userId, Long id, List<PurchaseOrderItem> receivedItems) {
return purchaseOrderRepository.findByUserIdAndId(userId, id)
.flatMap(po -> {
if ("RECEIVED".equals(po.getStatus())) {
return Mono.error(new RuntimeException("PO is already marked as received"));
}
po.setStatus("RECEIVED");
po.setUpdatedAt(LocalDateTime.now());
return purchaseOrderRepository.save(po)
.flatMap(savedPo -> purchaseOrderItemRepository.deleteByPoId(id)
.thenMany(Flux.fromIterable(receivedItems)
.map(item -> {
item.setPoId(id);
return item;
})
.flatMap(purchaseOrderItemRepository::save))
.collectList()
.flatMap(savedItems -> {
// Update inventory for received items
return Flux.fromIterable(savedItems)
.flatMap(item -> {
if (item.getProductId() != null) {
com.kifi.api.entity.inventory.InventoryMovement movement = com.kifi.api.entity.inventory.InventoryMovement.builder()
.type("IN")
.notes("PO Received: " + savedPo.getPoNumber())
.items(java.util.List.of(com.kifi.api.entity.inventory.InventoryMovementItem.builder()
.productId(item.getProductId())
.quantity(item.getQuantity())
.build()))
.build();
return productService.adjustStock(userId, item.getProductId(), movement);
}
return Mono.empty();
})
.then(Mono.just(savedPo));
}));
});
}
@Transactional
public Mono<PurchasePayment> addPayment(Long userId, Long poId, PurchasePayment payment) {
return purchaseOrderRepository.findByUserIdAndId(userId, poId)
.switchIfEmpty(Mono.error(new RuntimeException("PO not found")))
.flatMap(po -> {
payment.setPoId(poId);
if (payment.getPaymentDate() == null) {
payment.setPaymentDate(LocalDateTime.now());
}
return purchasePaymentRepository.save(payment)
.flatMap(savedPayment -> {
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);
});
});
}
public Flux<PurchasePayment> getPayments(Long userId, Long poId) {
return purchaseOrderRepository.findByUserIdAndId(userId, poId)
.switchIfEmpty(Mono.error(new RuntimeException("PO not found")))
.flatMapMany(po -> purchasePaymentRepository.findByPoId(poId));
}
}

View File

@@ -0,0 +1,52 @@
package com.kifi.api.service.vendor;
import com.kifi.api.entity.vendor.Vendor;
import com.kifi.api.repository.vendor.VendorRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
public class VendorService {
private final VendorRepository vendorRepository;
public Flux<Vendor> getVendors(Long userId) {
return vendorRepository.findByUserId(userId)
.sort((a, b) -> a.getName().compareToIgnoreCase(b.getName()));
}
public Mono<Vendor> getVendor(Long id) {
return vendorRepository.findById(id);
}
public Mono<Vendor> createVendor(Long userId, Vendor vendor) {
vendor.setUserId(userId);
vendor.setCreatedAt(LocalDateTime.now());
return vendorRepository.save(vendor);
}
public Mono<Vendor> updateVendor(Long id, Vendor vendor) {
return vendorRepository.findById(id)
.flatMap(existing -> {
existing.setName(vendor.getName());
existing.setEmail(vendor.getEmail());
existing.setPhone(vendor.getPhone());
existing.setAddress(vendor.getAddress());
existing.setGstin(vendor.getGstin());
existing.setStateId(vendor.getStateId());
existing.setIdNumber(vendor.getIdNumber());
existing.setPhotoUrl(vendor.getPhotoUrl());
existing.setContactPerson(vendor.getContactPerson());
return vendorRepository.save(existing);
});
}
public Mono<Void> deleteVendor(Long id) {
return vendorRepository.deleteById(id);
}
}