Product Add Done

This commit is contained in:
2026-08-16 22:51:34 +05:30
parent 17c0526ed6
commit ba9a9fd8a4
60 changed files with 5098 additions and 22 deletions

20
kifi-api/build_n_push_v2.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
# Configuration
REGISTRY="hub.technobeesolutions.in"
USERNAME="technobee_admin"
PASSWORD='M@tr!x#149@dm!N'
IMAGE_NAME="kifi-api"
TAG="kifi-one"
FULL_IMAGE_NAME="$REGISTRY/$IMAGE_NAME:$TAG"
# Stop on any error
set -e
echo "Logging into Docker registry: $REGISTRY..."
echo "$PASSWORD" | docker login "$REGISTRY" -u "$USERNAME" --password-stdin
echo "Building and pushing Docker image for linux/amd64: $FULL_IMAGE_NAME..."
docker buildx build --platform linux/amd64 -t "$FULL_IMAGE_NAME" --push .
echo "Done! Image built and pushed successfully."

View File

@@ -45,8 +45,8 @@ public class SecurityConfig {
.securityContextRepository(securityContextRepository)
.authorizeExchange(exchange -> exchange
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers("/api/kifi/auth/**").permitAll()
.pathMatchers("/api/kifi/health/**").permitAll()
.pathMatchers("/api/kifi-v2/auth/**").permitAll()
.pathMatchers("/api/kifi-v2/health/**").permitAll()
.anyExchange().authenticated()
)
.build();

View File

@@ -13,7 +13,7 @@ import reactor.core.publisher.Mono;
import java.util.Map;
@RestController
@RequestMapping("/api/kifi/auth")
@RequestMapping("/api/kifi-v2/auth")
@RequiredArgsConstructor
public class AuthController {

View File

@@ -12,7 +12,7 @@ import reactor.core.publisher.Mono;
import java.util.List;
@RestController
@RequestMapping("/api/kifi/budgets")
@RequestMapping("/api/kifi-v2/budgets")
@RequiredArgsConstructor
public class BudgetController {
private final BudgetService budgetService;

View File

@@ -11,7 +11,7 @@ import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/kifi/categories")
@RequestMapping("/api/kifi-v2/categories")
@RequiredArgsConstructor
public class CategoryController {
private final CategoryService categoryService;

View File

@@ -8,7 +8,7 @@ import reactor.core.publisher.Mono;
import java.util.Map;
@RestController
@RequestMapping("/api/kifi/health")
@RequestMapping("/api/kifi-v2/health")
public class HealthController {
@GetMapping

View File

@@ -12,7 +12,7 @@ import java.util.List;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/kifi/recurring-transactions")
@RequestMapping("/api/kifi-v2/recurring-transactions")
@RequiredArgsConstructor
public class RecurringTransactionController {

View File

@@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/kifi/reports")
@RequestMapping("/api/kifi-v2/reports")
@RequiredArgsConstructor
public class ReportController {
private final ReportService reportService;

View File

@@ -15,7 +15,7 @@ import java.util.List;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/kifi/transactions")
@RequestMapping("/api/kifi-v2/transactions")
@RequiredArgsConstructor
public class TransactionController {
private final TransactionService transactionService;

View File

@@ -13,7 +13,7 @@ import reactor.core.publisher.Mono;
import java.util.List;
@RestController
@RequestMapping("/api/kifi/wallets")
@RequestMapping("/api/kifi-v2/wallets")
@RequiredArgsConstructor
public class WalletController {

View File

@@ -0,0 +1,32 @@
package com.kifi.api.controller.business;
import com.kifi.api.entity.business.BusinessProfile;
import com.kifi.api.service.business.BusinessService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/kifi-v2/business")
@RequiredArgsConstructor
public class BusinessController {
private final BusinessService businessService;
// TODO: Obtain user ID from auth context, using hardcoded for now or parameter
@GetMapping("/profile")
public Mono<ResponseEntity<BusinessProfile>> getProfile(Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return businessService.getProfileByUserId(userId)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping("/profile")
public Mono<ResponseEntity<BusinessProfile>> saveProfile(Authentication authentication, @RequestBody BusinessProfile profile) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return businessService.saveProfile(userId, profile)
.map(ResponseEntity::ok);
}
}

View File

@@ -0,0 +1,24 @@
package com.kifi.api.controller.inventory;
import com.kifi.api.dto.inventory.MovementRequestDto;
import com.kifi.api.entity.inventory.InventoryMovement;
import com.kifi.api.service.inventory.InventoryMovementService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/kifi-v2/inventory/movements")
@RequiredArgsConstructor
public class InventoryMovementController {
private final InventoryMovementService movementService;
@PostMapping
public Mono<ResponseEntity<InventoryMovement>> recordMovement(Authentication authentication, @RequestBody MovementRequestDto request) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return movementService.recordMovement(userId, request.getMovement(), request.getItems())
.map(ResponseEntity::ok);
}
}

View File

@@ -0,0 +1,48 @@
package com.kifi.api.controller.inventory;
import com.kifi.api.entity.inventory.ProductCategory;
import com.kifi.api.repository.inventory.ProductCategoryRepository;
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.time.LocalDateTime;
@RestController
@RequestMapping("/api/kifi-v2/inventory/categories")
@RequiredArgsConstructor
public class ProductCategoryController {
private final ProductCategoryRepository categoryRepository;
@GetMapping
public Flux<ProductCategory> getCategories(Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return categoryRepository.findByUserId(userId);
}
@PostMapping
public Mono<ResponseEntity<ProductCategory>> createCategory(Authentication authentication, @RequestBody ProductCategory category) {
Long userId = Long.valueOf(authentication.getDetails().toString());
category.setUserId(userId);
category.setCreatedAt(LocalDateTime.now());
return categoryRepository.save(category)
.map(ResponseEntity::ok);
}
@PutMapping("/{id}")
public Mono<ResponseEntity<ProductCategory>> updateCategory(@PathVariable Long id, @RequestBody ProductCategory category) {
return categoryRepository.findById(id)
.flatMap(existing -> {
existing.setName(category.getName());
existing.setParentCategoryId(category.getParentCategoryId());
existing.setIsCommodity(category.getIsCommodity());
existing.setDailyRate(category.getDailyRate());
return categoryRepository.save(existing);
})
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
}

View File

@@ -0,0 +1,81 @@
package com.kifi.api.controller.inventory;
import com.kifi.api.entity.inventory.Product;
import com.kifi.api.entity.inventory.ProductImage;
import com.kifi.api.service.inventory.ProductService;
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 org.springframework.http.codec.multipart.FilePart;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import java.util.Base64;
@RestController
@RequestMapping("/api/kifi-v2/inventory/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping
public Flux<Product> getProducts(
Authentication authentication,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return productService.getProductsByUserId(userId, page, size);
}
@PostMapping
public Mono<ResponseEntity<Product>> createProduct(Authentication authentication, @RequestBody Product product) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return productService.createProduct(userId, product)
.map(ResponseEntity::ok);
}
@PutMapping("/{id}")
public Mono<ResponseEntity<Product>> updateProduct(
Authentication authentication,
@PathVariable Long id,
@RequestBody Product product
) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return productService.updateProduct(userId, id, product)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping(value = "/{id}/images", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<ProductImage> addImage(@PathVariable Long id, @RequestPart("file") FilePart filePart) {
return DataBufferUtils.join(filePart.content())
.flatMap(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
String base64Content = Base64.getEncoder().encodeToString(bytes);
String fileName = filePart.filename();
String contentType = filePart.headers().getContentType() != null ? filePart.headers().getContentType().toString() : "application/octet-stream";
return productService.addProductImage(id, fileName, contentType, base64Content);
});
}
@GetMapping("/{id}/images/{imageId}/content")
public Mono<ResponseEntity<byte[]>> downloadImage(@PathVariable Long id, @PathVariable Long imageId) {
return productService.downloadProductImage(imageId)
.map(response -> {
byte[] decodedBytes = Base64.getDecoder().decode(response.getBase64Content());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, "image/jpeg")
.body(decodedBytes);
});
}
@DeleteMapping("/images/{imageId}")
public Mono<Void> deleteImage(@PathVariable Long imageId) {
return productService.deleteProductImage(imageId);
}
}

View File

@@ -0,0 +1,13 @@
package com.kifi.api.dto.inventory;
import com.kifi.api.entity.inventory.InventoryMovement;
import com.kifi.api.entity.inventory.InventoryMovementItem;
import lombok.Data;
import java.util.List;
@Data
public class MovementRequestDto {
private InventoryMovement movement;
private List<InventoryMovementItem> items;
}

View File

@@ -21,6 +21,8 @@ public class User {
private String password;
@Builder.Default
private Boolean enabled = false;
@Builder.Default
private String profileType = "INDIVIDUAL";
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,25 @@
package com.kifi.api.entity.business;
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("business_features")
public class BusinessFeature {
@Id
private Long id;
private Long userId;
private Boolean inventoryManagement;
private Boolean salesManagement;
private Boolean multiLocation;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,28 @@
package com.kifi.api.entity.business;
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("business_profiles")
public class BusinessProfile {
@Id
private Long id;
private Long userId;
private String businessName;
private String industry;
private String taxNumber;
private String currency;
private Boolean taxIncludedInPrice;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,25 @@
package com.kifi.api.entity.inventory;
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("inventory_balances")
public class InventoryBalance {
@Id
private Long id;
private Long productId;
private Long locationId;
private BigDecimal quantity;
private LocalDateTime lastUpdated;
}

View File

@@ -0,0 +1,25 @@
package com.kifi.api.entity.inventory;
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("inventory_locations")
public class InventoryLocation {
@Id
private Long id;
private Long userId;
private String name;
private String address;
private Boolean isPrimary;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,26 @@
package com.kifi.api.entity.inventory;
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("inventory_movements")
public class InventoryMovement {
@Id
private Long id;
private Long userId;
private Long locationId;
private String type;
private Long referenceTransactionId;
private String notes;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,26 @@
package com.kifi.api.entity.inventory;
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("inventory_movement_items")
public class InventoryMovementItem {
@Id
private Long id;
private Long movementId;
private Long productId;
private BigDecimal quantity;
private BigDecimal unitPrice;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,52 @@
package com.kifi.api.entity.inventory;
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;
import java.util.List;
import org.springframework.data.annotation.Transient;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table("products")
public class Product {
@Id
private Long id;
private Long userId;
private Long categoryId;
private Long uomId;
private String name;
private String sku;
private String barcode;
private String description;
private BigDecimal purchasePrice;
private BigDecimal sellingPrice;
private BigDecimal gstRate;
private String dimensions;
private BigDecimal weight;
private String color;
private String size;
private String priceCalcRule;
private Boolean autoCalculatePrice;
private Double purityFactor;
private Double makingCharges;
private String makingChargesType;
private Double wastagePercentage;
private BigDecimal minStock;
private BigDecimal reorderLevel;
private Boolean trackInventory;
private Boolean isActive;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@Transient
private List<ProductImage> images;
}

View File

@@ -0,0 +1,25 @@
package com.kifi.api.entity.inventory;
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("product_bom")
public class ProductBom {
@Id
private Long id;
private Long parentProductId;
private Long componentProductId;
private BigDecimal quantity;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,28 @@
package com.kifi.api.entity.inventory;
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("product_categories")
public class ProductCategory {
@Id
private Long id;
private Long userId;
private String name;
private Long parentCategoryId;
private Boolean isCommodity;
private String calculationMethod;
private String baseUnit;
private Double dailyRate;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,25 @@
package com.kifi.api.entity.inventory;
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("product_images")
public class ProductImage {
@Id
private Long id;
private Long productId;
private String fileName;
private String filePath;
private String contentType;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,24 @@
package com.kifi.api.entity.inventory;
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("units_of_measure")
public class UnitOfMeasure {
@Id
private Long id;
private Long userId;
private String name;
private String abbreviation;
private LocalDateTime createdAt;
}

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
package com.kifi.api.repository.inventory;
import com.kifi.api.entity.inventory.InventoryBalance;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;
public interface InventoryBalanceRepository extends ReactiveCrudRepository<InventoryBalance, Long> {
Mono<InventoryBalance> findByProductIdAndLocationId(Long productId, Long locationId);
Flux<InventoryBalance> findByLocationId(Long locationId);
}

View File

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

View File

@@ -0,0 +1,9 @@
package com.kifi.api.repository.inventory;
import com.kifi.api.entity.inventory.InventoryMovementItem;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
public interface InventoryMovementItemRepository extends ReactiveCrudRepository<InventoryMovementItem, Long> {
Flux<InventoryMovementItem> findByMovementId(Long movementId);
}

View File

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

View File

@@ -0,0 +1,9 @@
package com.kifi.api.repository.inventory;
import com.kifi.api.entity.inventory.ProductBom;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
public interface ProductBomRepository extends ReactiveCrudRepository<ProductBom, Long> {
Flux<ProductBom> findByParentProductId(Long parentProductId);
}

View File

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

View File

@@ -0,0 +1,9 @@
package com.kifi.api.repository.inventory;
import com.kifi.api.entity.inventory.ProductImage;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
public interface ProductImageRepository extends ReactiveCrudRepository<ProductImage, Long> {
Flux<ProductImage> findByProductId(Long productId);
}

View File

@@ -0,0 +1,10 @@
package com.kifi.api.repository.inventory;
import com.kifi.api.entity.inventory.Product;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
public interface ProductRepository extends ReactiveCrudRepository<Product, Long> {
Flux<Product> findByUserId(Long userId, Pageable pageable);
}

View File

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

View File

@@ -0,0 +1,37 @@
package com.kifi.api.service.business;
import com.kifi.api.entity.business.BusinessProfile;
import com.kifi.api.repository.business.BusinessProfileRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
public class BusinessService {
private final BusinessProfileRepository businessProfileRepository;
public Mono<BusinessProfile> getProfileByUserId(Long userId) {
return businessProfileRepository.findByUserId(userId);
}
public Mono<BusinessProfile> saveProfile(Long userId, BusinessProfile profile) {
return businessProfileRepository.findByUserId(userId)
.flatMap(existing -> {
existing.setBusinessName(profile.getBusinessName());
existing.setIndustry(profile.getIndustry());
existing.setTaxNumber(profile.getTaxNumber());
existing.setCurrency(profile.getCurrency() != null ? profile.getCurrency() : existing.getCurrency());
existing.setUpdatedAt(LocalDateTime.now());
return businessProfileRepository.save(existing);
})
.switchIfEmpty(Mono.defer(() -> {
profile.setUserId(userId);
profile.setCreatedAt(LocalDateTime.now());
profile.setUpdatedAt(LocalDateTime.now());
return businessProfileRepository.save(profile);
}));
}
}

View File

@@ -0,0 +1,69 @@
package com.kifi.api.service.inventory;
import com.kifi.api.entity.inventory.InventoryBalance;
import com.kifi.api.entity.inventory.InventoryMovement;
import com.kifi.api.entity.inventory.InventoryMovementItem;
import com.kifi.api.repository.inventory.InventoryBalanceRepository;
import com.kifi.api.repository.inventory.InventoryMovementItemRepository;
import com.kifi.api.repository.inventory.InventoryMovementRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Mono;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Service
@RequiredArgsConstructor
public class InventoryMovementService {
private final InventoryMovementRepository movementRepository;
private final InventoryMovementItemRepository itemRepository;
private final InventoryBalanceRepository balanceRepository;
@Transactional
public Mono<InventoryMovement> recordMovement(Long userId, InventoryMovement movement, List<InventoryMovementItem> items) {
movement.setUserId(userId);
movement.setCreatedAt(LocalDateTime.now());
return movementRepository.save(movement)
.flatMap(savedMovement -> {
return reactor.core.publisher.Flux.fromIterable(items)
.flatMap(item -> {
item.setMovementId(savedMovement.getId());
item.setCreatedAt(LocalDateTime.now());
return itemRepository.save(item)
.then(updateBalance(item.getProductId(), savedMovement.getLocationId(), item.getQuantity(), savedMovement.getType()));
})
.then(Mono.just(savedMovement));
});
}
private Mono<InventoryBalance> updateBalance(Long productId, Long locationId, BigDecimal quantity, String type) {
BigDecimal signedQuantity = quantity;
if ("REDUCTION".equals(type) || "ADJUSTMENT".equals(type)) { // Assuming adjustment is absolute or negative handled elsewhere, for now treat reduction as negative
// we'll refine this later
if("REDUCTION".equals(type)) {
signedQuantity = quantity.negate();
}
}
final BigDecimal finalQty = signedQuantity;
return balanceRepository.findByProductIdAndLocationId(productId, locationId)
.flatMap(balance -> {
balance.setQuantity(balance.getQuantity().add(finalQty));
balance.setLastUpdated(LocalDateTime.now());
return balanceRepository.save(balance);
})
.switchIfEmpty(Mono.defer(() -> {
InventoryBalance newBalance = new InventoryBalance();
newBalance.setProductId(productId);
newBalance.setLocationId(locationId);
newBalance.setQuantity(finalQty);
newBalance.setLastUpdated(LocalDateTime.now());
return balanceRepository.save(newBalance);
}));
}
}

View File

@@ -0,0 +1,109 @@
package com.kifi.api.service.inventory;
import com.kifi.api.entity.inventory.Product;
import com.kifi.api.entity.inventory.ProductImage;
import com.kifi.api.repository.inventory.ProductRepository;
import com.kifi.api.repository.inventory.ProductImageRepository;
import com.kifi.api.service.MinioServiceClient;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.data.domain.PageRequest;
import java.util.UUID;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
private final ProductImageRepository productImageRepository;
private final MinioServiceClient minioServiceClient;
public Flux<Product> getProductsByUserId(Long userId, int page, int size) {
return productRepository.findByUserId(userId, PageRequest.of(page, size))
.flatMap(product -> productImageRepository.findByProductId(product.getId()).collectList()
.map(images -> {
product.setImages(images);
return product;
})
);
}
public Mono<Product> createProduct(Long userId, Product product) {
product.setUserId(userId);
product.setCreatedAt(LocalDateTime.now());
product.setUpdatedAt(LocalDateTime.now());
if (product.getIsActive() == null) product.setIsActive(true);
if (product.getTrackInventory() == null) product.setTrackInventory(true);
if (product.getPriceCalcRule() == null) product.setPriceCalcRule("MANUAL");
if (product.getAutoCalculatePrice() == null) product.setAutoCalculatePrice(false);
return productRepository.save(product);
}
public Mono<Product> updateProduct(Long userId, Long productId, Product updatedProduct) {
return productRepository.findById(productId)
.filter(p -> p.getUserId().equals(userId))
.flatMap(existingProduct -> {
existingProduct.setName(updatedProduct.getName());
existingProduct.setSku(updatedProduct.getSku());
existingProduct.setCategoryId(updatedProduct.getCategoryId());
existingProduct.setPurchasePrice(updatedProduct.getPurchasePrice());
existingProduct.setSellingPrice(updatedProduct.getSellingPrice());
existingProduct.setGstRate(updatedProduct.getGstRate());
existingProduct.setWeight(updatedProduct.getWeight());
existingProduct.setColor(updatedProduct.getColor());
existingProduct.setSize(updatedProduct.getSize());
existingProduct.setDimensions(updatedProduct.getDimensions());
existingProduct.setPriceCalcRule(updatedProduct.getPriceCalcRule());
existingProduct.setAutoCalculatePrice(updatedProduct.getAutoCalculatePrice());
existingProduct.setPurityFactor(updatedProduct.getPurityFactor());
existingProduct.setMakingCharges(updatedProduct.getMakingCharges());
existingProduct.setMakingChargesType(updatedProduct.getMakingChargesType());
existingProduct.setWastagePercentage(updatedProduct.getWastagePercentage());
existingProduct.setMinStock(updatedProduct.getMinStock());
existingProduct.setReorderLevel(updatedProduct.getReorderLevel());
existingProduct.setTrackInventory(updatedProduct.getTrackInventory());
existingProduct.setIsActive(updatedProduct.getIsActive());
existingProduct.setUpdatedAt(LocalDateTime.now());
return productRepository.save(existingProduct);
});
}
public Mono<ProductImage> addProductImage(Long productId, String fileName, String contentType, String base64Content) {
String uniqueFileName = UUID.randomUUID().toString() + "_" + fileName;
String directoryPath = "products/" + productId;
return minioServiceClient.uploadFile(directoryPath, contentType, uniqueFileName, base64Content)
.flatMap(minioResponse -> {
if (minioResponse.isSuccess()) {
ProductImage image = ProductImage.builder()
.productId(productId)
.fileName(fileName)
.filePath(minioResponse.getFilePath())
.contentType(contentType)
.createdAt(LocalDateTime.now())
.build();
return productImageRepository.save(image);
} else {
return Mono.error(new RuntimeException("Failed to upload product image to MinIO"));
}
});
}
public Mono<Void> deleteProductImage(Long imageId) {
return productImageRepository.findById(imageId)
.flatMap(image ->
minioServiceClient.deleteFile(image.getContentType(), image.getFilePath())
.then(productImageRepository.delete(image))
);
}
public Mono<MinioServiceClient.MinioDownloadResponse> downloadProductImage(Long imageId) {
return productImageRepository.findById(imageId)
.switchIfEmpty(Mono.error(new RuntimeException("Product image not found")))
.flatMap(image ->
minioServiceClient.downloadFile(image.getContentType(), image.getFilePath())
);
}
}

View File

@@ -3,7 +3,7 @@ spring:
name: kifi-api
r2dbc:
url: r2dbc:postgresql://103.125.129.116:5333/kifi
url: r2dbc:postgresql://103.125.129.116:5333/kifi-v2
username: postgres
password: M@triXPostgr3s@6202
pool:

View File

@@ -132,3 +132,134 @@ CREATE TABLE IF NOT EXISTS wallet_invitations (
status VARCHAR(20) DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- KIFI V2 PHASE 1 MIGRATION: INVENTORY MANAGEMENT --
ALTER TABLE users ADD COLUMN IF NOT EXISTS profile_type VARCHAR(20) DEFAULT 'INDIVIDUAL';
CREATE TABLE IF NOT EXISTS business_profiles (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
business_name VARCHAR(255) NOT NULL,
industry VARCHAR(100),
tax_number VARCHAR(100),
currency VARCHAR(10) DEFAULT 'INR',
tax_included_in_price BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id)
);
CREATE TABLE IF NOT EXISTS business_features (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
inventory_management BOOLEAN DEFAULT FALSE,
sales_management BOOLEAN DEFAULT FALSE,
multi_location BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id)
);
CREATE TABLE IF NOT EXISTS product_categories (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
parent_category_id INTEGER REFERENCES product_categories(id),
is_commodity BOOLEAN DEFAULT FALSE,
calculation_method VARCHAR(50) DEFAULT 'UNIT',
base_unit VARCHAR(50) DEFAULT 'pcs',
daily_rate DECIMAL(15, 2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS units_of_measure (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
abbreviation VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS products (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
category_id INTEGER REFERENCES product_categories(id),
uom_id INTEGER REFERENCES units_of_measure(id),
name VARCHAR(255) NOT NULL,
sku VARCHAR(100),
barcode VARCHAR(100),
description TEXT,
purchase_price DECIMAL(15, 2),
selling_price DECIMAL(15, 2),
gst_rate DECIMAL(5, 2),
dimensions VARCHAR(100),
weight DECIMAL(10, 3),
color VARCHAR(50),
size VARCHAR(50),
price_calc_rule VARCHAR(50) DEFAULT 'MANUAL',
auto_calculate_price BOOLEAN DEFAULT FALSE,
purity_factor DECIMAL(5, 4) DEFAULT 1.0,
making_charges DECIMAL(15, 2) DEFAULT 0.0,
making_charges_type VARCHAR(50) DEFAULT 'FLAT',
wastage_percentage DECIMAL(5, 2) DEFAULT 0.0,
min_stock DECIMAL(10, 2) DEFAULT 0,
reorder_level DECIMAL(10, 2) DEFAULT 0,
track_inventory BOOLEAN DEFAULT TRUE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS product_images (
id SERIAL PRIMARY KEY,
product_id INTEGER REFERENCES products(id) ON DELETE CASCADE,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(1024) NOT NULL,
content_type VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS product_bom (
id SERIAL PRIMARY KEY,
parent_product_id INTEGER REFERENCES products(id) ON DELETE CASCADE,
component_product_id INTEGER REFERENCES products(id) ON DELETE RESTRICT,
quantity DECIMAL(10, 3) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS inventory_locations (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
address TEXT,
is_primary BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS inventory_movements (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
location_id INTEGER REFERENCES inventory_locations(id),
type VARCHAR(50) NOT NULL, -- ADDITION, REDUCTION, TRANSFER, ADJUSTMENT
reference_transaction_id INTEGER REFERENCES transactions(id),
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS inventory_movement_items (
id SERIAL PRIMARY KEY,
movement_id INTEGER REFERENCES inventory_movements(id) ON DELETE CASCADE,
product_id INTEGER REFERENCES products(id),
quantity DECIMAL(10, 2) NOT NULL,
unit_price DECIMAL(10, 2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS inventory_balances (
id SERIAL PRIMARY KEY,
product_id INTEGER REFERENCES products(id) ON DELETE CASCADE,
location_id INTEGER REFERENCES inventory_locations(id) ON DELETE CASCADE,
quantity DECIMAL(10, 2) DEFAULT 0,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(product_id, location_id)
);