Tuned ui to support ecommerce product catalogue and purchases.

This commit is contained in:
2026-09-01 21:11:32 +05:30
parent 3a5e812947
commit b4772ccf19
16 changed files with 1798 additions and 600 deletions

View File

@@ -55,6 +55,7 @@ public class ProductCategoryController {
existing.setDefaultMakingCharge(category.getDefaultMakingCharge());
existing.setMakingChargeType(category.getMakingChargeType());
existing.setBaseUnit(category.getBaseUnit());
existing.setDefaultLengthUom(category.getDefaultLengthUom());
existing.setIsActive(category.getIsActive());
existing.setSortOrder(category.getSortOrder());

View File

@@ -39,6 +39,8 @@ public class InventoryItem {
private Long vendorId;
private Long branchId;
private String purchaseRef;
private String salesRef;
private BigDecimal saleRate;
private String photoUrl;
@Builder.Default
private String status = "AVAILABLE";

View File

@@ -33,6 +33,26 @@ public class Product {
private String dimensions;
private String color;
private String size;
private Double itemLength;
private Double itemWidth;
private Double itemHeight;
private String dimensionUom;
private Double packageLength;
private Double packageWidth;
private Double packageHeight;
private String packageDimensionUom;
private String manufacturerCode;
private String material;
private String brandName;
private String countryOfOrigin;
private Double weightInG;
private Double packageWeightInG;
private Double volumetricWeightInKg;
private String priceCalcRule;
private Double purityFactor;
private Double makingCharges;

View File

@@ -34,6 +34,8 @@ public class ProductCategory {
private String makingChargeType;
private String baseUnit;
@Builder.Default
private String defaultLengthUom = "cm";
@Builder.Default
private Boolean isActive = true;

View File

@@ -102,6 +102,26 @@ public class ProductService {
existingProduct.setColor(updatedProduct.getColor());
existingProduct.setSize(updatedProduct.getSize());
existingProduct.setDimensions(updatedProduct.getDimensions());
existingProduct.setItemLength(updatedProduct.getItemLength());
existingProduct.setItemWidth(updatedProduct.getItemWidth());
existingProduct.setItemHeight(updatedProduct.getItemHeight());
existingProduct.setDimensionUom(updatedProduct.getDimensionUom());
existingProduct.setPackageLength(updatedProduct.getPackageLength());
existingProduct.setPackageWidth(updatedProduct.getPackageWidth());
existingProduct.setPackageHeight(updatedProduct.getPackageHeight());
existingProduct.setPackageDimensionUom(updatedProduct.getPackageDimensionUom());
existingProduct.setManufacturerCode(updatedProduct.getManufacturerCode());
existingProduct.setMaterial(updatedProduct.getMaterial());
existingProduct.setBrandName(updatedProduct.getBrandName());
existingProduct.setCountryOfOrigin(updatedProduct.getCountryOfOrigin());
existingProduct.setWeightInG(updatedProduct.getWeightInG());
existingProduct.setPackageWeightInG(updatedProduct.getPackageWeightInG());
existingProduct.setVolumetricWeightInKg(updatedProduct.getVolumetricWeightInKg());
existingProduct.setPriceCalcRule(updatedProduct.getPriceCalcRule());
existingProduct.setPurityFactor(normalizePurity(updatedProduct.getPurityFactor()));
existingProduct.setMakingCharges(updatedProduct.getMakingCharges());

View File

@@ -146,15 +146,64 @@ public class InvoiceService {
if (shouldDeduct && invoice.getItems() != null && !invoice.getItems().isEmpty()) {
return Flux.fromIterable(invoice.getItems())
.concatMap(item -> {
BigDecimal soldQty = (item.getWeight() != null && item.getWeight().compareTo(BigDecimal.ZERO) > 0)
? item.getWeight()
: (item.getQuantity() != null && item.getQuantity().compareTo(BigDecimal.ZERO) > 0 ? item.getQuantity() : BigDecimal.ONE);
BigDecimal soldRate = item.getUnitPrice() != null ? item.getUnitPrice() : BigDecimal.ZERO;
if (item.getInventoryItemId() != null) {
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);
BigDecimal currentWeight = (invItem.getGrossWeight() != null && invItem.getGrossWeight().compareTo(BigDecimal.ZERO) > 0)
? invItem.getGrossWeight()
: ((invItem.getNetWeight() != null && invItem.getNetWeight().compareTo(BigDecimal.ZERO) > 0)
? invItem.getNetWeight()
: BigDecimal.ONE);
BigDecimal unitCost = invItem.getPurchaseCost() != null ? invItem.getPurchaseCost() : BigDecimal.ZERO;
BigDecimal cost = unitCost.multiply(soldQty);
if (currentWeight.compareTo(soldQty) > 0) {
// Partial sale: Reduce existing available item weight
BigDecimal remaining = currentWeight.subtract(soldQty);
invItem.setGrossWeight(remaining);
invItem.setNetWeight(remaining);
invItem.setUpdatedAt(LocalDateTime.now());
// Create new SOLD item record
com.kifi.api.entity.inventory.InventoryItem soldItem = com.kifi.api.entity.inventory.InventoryItem.builder()
.userId(userId)
.productId(invItem.getProductId())
.vendorId(invItem.getVendorId())
.purchaseRef(invItem.getPurchaseRef())
.salesRef(invoice.getInvoiceNumber())
.purchaseCost(invItem.getPurchaseCost())
.saleRate(soldRate)
.makingCharges(invItem.getMakingCharges())
.sku(invItem.getSku())
.huid(invItem.getHuid())
.grossWeight(soldQty)
.netWeight(soldQty)
.photoUrl(invItem.getPhotoUrl())
.status("SOLD")
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
return inventoryItemRepository.save(invItem)
.then(inventoryItemRepository.save(soldItem))
.thenReturn(cost);
} else {
// Full sale
invItem.setStatus("SOLD");
invItem.setSalesRef(invoice.getInvoiceNumber());
invItem.setSaleRate(soldRate);
invItem.setUpdatedAt(LocalDateTime.now());
return inventoryItemRepository.save(invItem).thenReturn(cost);
}
}).defaultIfEmpty(BigDecimal.ZERO);
} else if (item.getProductId() != null) {
// Product sale without explicit inventory item: adjust product stock & FIFO deduct inventory items
InventoryMovement movement = InventoryMovement.builder()
.userId(userId)
.type("REDUCTION")
@@ -167,8 +216,72 @@ public class InvoiceService {
movementItem.setQuantity(item.getQuantity());
movement.setItems(java.util.Collections.singletonList(movementItem));
return productService.adjustStock(userId, item.getProductId(), movement)
.thenReturn(BigDecimal.ZERO);
Mono<Void> adjustMono = productService.adjustStock(userId, item.getProductId(), movement).then();
Mono<BigDecimal> fifoMono = inventoryItemRepository.findByUserIdAndProductId(userId, item.getProductId())
.filter(i -> "AVAILABLE".equalsIgnoreCase(i.getStatus()))
.collectList()
.flatMap(availItems -> {
if (availItems.isEmpty()) {
return Mono.just(BigDecimal.ZERO);
}
BigDecimal needed = soldQty;
java.util.List<Mono<Void>> saves = new java.util.ArrayList<>();
BigDecimal totalCost = BigDecimal.ZERO;
for (com.kifi.api.entity.inventory.InventoryItem invItem : availItems) {
if (needed.compareTo(BigDecimal.ZERO) <= 0) break;
BigDecimal itemWeight = (invItem.getGrossWeight() != null && invItem.getGrossWeight().compareTo(BigDecimal.ZERO) > 0)
? invItem.getGrossWeight()
: ((invItem.getNetWeight() != null && invItem.getNetWeight().compareTo(BigDecimal.ZERO) > 0)
? invItem.getNetWeight()
: BigDecimal.ONE);
BigDecimal take = needed.min(itemWeight);
needed = needed.subtract(take);
BigDecimal unitPurchase = invItem.getPurchaseCost() != null ? invItem.getPurchaseCost() : BigDecimal.ZERO;
totalCost = totalCost.add(unitPurchase.multiply(take));
if (itemWeight.compareTo(take) > 0) {
BigDecimal remaining = itemWeight.subtract(take);
invItem.setGrossWeight(remaining);
invItem.setNetWeight(remaining);
invItem.setUpdatedAt(LocalDateTime.now());
saves.add(inventoryItemRepository.save(invItem).then());
com.kifi.api.entity.inventory.InventoryItem soldRecord = com.kifi.api.entity.inventory.InventoryItem.builder()
.userId(userId)
.productId(invItem.getProductId())
.vendorId(invItem.getVendorId())
.purchaseRef(invItem.getPurchaseRef())
.salesRef(invoice.getInvoiceNumber())
.purchaseCost(invItem.getPurchaseCost())
.saleRate(soldRate)
.makingCharges(invItem.getMakingCharges())
.sku(invItem.getSku())
.huid(invItem.getHuid())
.grossWeight(take)
.netWeight(take)
.photoUrl(invItem.getPhotoUrl())
.status("SOLD")
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
saves.add(inventoryItemRepository.save(soldRecord).then());
} else {
invItem.setStatus("SOLD");
invItem.setSalesRef(invoice.getInvoiceNumber());
invItem.setSaleRate(soldRate);
invItem.setUpdatedAt(LocalDateTime.now());
saves.add(inventoryItemRepository.save(invItem).then());
}
}
BigDecimal finalCost = totalCost;
return Flux.concat(saves).then(Mono.just(finalCost));
});
return adjustMono.then(fifoMono);
}
return Mono.just(BigDecimal.ZERO);
})

View File

@@ -208,6 +208,7 @@ CREATE TABLE IF NOT EXISTS product_categories (
commodity_code VARCHAR(10),
purity_factor DECIMAL(5, 4) DEFAULT 1.0,
base_unit VARCHAR(20) DEFAULT 'pcs',
default_length_uom VARCHAR(20) DEFAULT 'cm',
is_active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
@@ -249,6 +250,21 @@ CREATE TABLE IF NOT EXISTS products (
weight DECIMAL(10, 3),
color VARCHAR(50),
size VARCHAR(50),
item_length DECIMAL(10, 2),
item_width DECIMAL(10, 2),
item_height DECIMAL(10, 2),
dimension_uom VARCHAR(20) DEFAULT 'cm',
package_length DECIMAL(10, 2),
package_width DECIMAL(10, 2),
package_height DECIMAL(10, 2),
package_dimension_uom VARCHAR(20) DEFAULT 'cm',
manufacturer_code VARCHAR(100),
material VARCHAR(100),
brand_name VARCHAR(100),
country_of_origin VARCHAR(100),
weight_in_g DECIMAL(10, 3),
package_weight_in_g DECIMAL(10, 3),
volumetric_weight_in_kg DECIMAL(10, 3),
price_calc_rule VARCHAR(50) DEFAULT 'MANUAL',
purity_factor DECIMAL(5, 4),
making_charges DECIMAL(15, 2) DEFAULT 0.0,
@@ -383,6 +399,8 @@ CREATE TABLE IF NOT EXISTS inventory_items (
vendor_id INTEGER REFERENCES vendors(id) ON DELETE SET NULL,
branch_id INTEGER,
purchase_ref VARCHAR(100),
sales_ref VARCHAR(100),
sale_rate DECIMAL(15, 2),
photo_url VARCHAR(1024),
status VARCHAR(50) DEFAULT 'AVAILABLE',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,