Inventory Management | Customer Management | Invoice Management Done | Commodity Rate Sync Done
This commit is contained in:
@@ -9,6 +9,10 @@ public class WebClientConfig {
|
||||
|
||||
@Bean
|
||||
public WebClient.Builder webClientBuilder() {
|
||||
return WebClient.builder();
|
||||
return WebClient
|
||||
.builder().codecs(configurer ->
|
||||
configurer.defaultCodecs()
|
||||
.maxInMemorySize(10 * 1024 * 1024)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +28,13 @@ 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());
|
||||
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());
|
||||
}
|
||||
|
||||
@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());
|
||||
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());
|
||||
}
|
||||
|
||||
@DeleteMapping("/{walletId}")
|
||||
@@ -95,6 +95,11 @@ public class WalletController {
|
||||
private String color;
|
||||
private String currency;
|
||||
private java.math.BigDecimal initialBalance;
|
||||
private String subNature;
|
||||
private java.math.BigDecimal creditLimit;
|
||||
private java.math.BigDecimal fixedAmount;
|
||||
private String paymentCycle;
|
||||
private Integer cycleDate;
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.kifi.api.controller.business;
|
||||
|
||||
import com.kifi.api.entity.business.BusinessProfile;
|
||||
import com.kifi.api.entity.business.BusinessFeature;
|
||||
import com.kifi.api.service.business.BusinessService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -29,4 +30,18 @@ public class BusinessController {
|
||||
return businessService.saveProfile(userId, profile)
|
||||
.map(ResponseEntity::ok);
|
||||
}
|
||||
|
||||
@GetMapping("/features")
|
||||
public Mono<ResponseEntity<BusinessFeature>> getFeatures(Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return businessService.getFeaturesByUserId(userId)
|
||||
.map(ResponseEntity::ok);
|
||||
}
|
||||
|
||||
@PostMapping("/features")
|
||||
public Mono<ResponseEntity<BusinessFeature>> saveFeatures(Authentication authentication, @RequestBody BusinessFeature feature) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return businessService.saveFeatures(userId, feature)
|
||||
.map(ResponseEntity::ok);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.kifi.api.controller.business;
|
||||
|
||||
import com.kifi.api.entity.business.IndianState;
|
||||
import com.kifi.api.repository.business.IndianStateRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/kifi-v2/master")
|
||||
@RequiredArgsConstructor
|
||||
public class MasterDataController {
|
||||
private final IndianStateRepository indianStateRepository;
|
||||
|
||||
@GetMapping("/states")
|
||||
public Flux<IndianState> getIndianStates() {
|
||||
return indianStateRepository.findAllByOrderByNameAsc();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.kifi.api.controller.customer;
|
||||
|
||||
import com.kifi.api.entity.customer.Customer;
|
||||
import com.kifi.api.service.customer.CustomerService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.codec.multipart.FilePart;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
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.Base64;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/kifi-v2/customers")
|
||||
@RequiredArgsConstructor
|
||||
public class CustomerController {
|
||||
|
||||
private final CustomerService customerService;
|
||||
|
||||
@GetMapping
|
||||
public Flux<Customer> getCustomers(
|
||||
Authentication authentication,
|
||||
@RequestParam(required = false) String search) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return customerService.getCustomers(userId, search);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<Customer> getCustomerById(
|
||||
@PathVariable Long id,
|
||||
Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return customerService.getCustomerById(id, userId);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Mono<Customer> createCustomer(
|
||||
Authentication authentication,
|
||||
@RequestBody Customer customer) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return customerService.createCustomer(userId, customer);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Mono<Customer> updateCustomer(
|
||||
@PathVariable Long id,
|
||||
Authentication authentication,
|
||||
@RequestBody Customer customer) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return customerService.updateCustomer(id, userId, customer);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<Void> deleteCustomer(
|
||||
@PathVariable Long id,
|
||||
Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return customerService.deleteCustomer(id, userId);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/{id}/photo", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public Mono<Customer> uploadPhoto(
|
||||
@PathVariable Long id,
|
||||
Authentication authentication,
|
||||
@RequestPart("file") FilePart filePart) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
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 contentType = filePart.headers().getContentType() != null ? filePart.headers().getContentType().toString() : "image/jpeg";
|
||||
return customerService.uploadPhoto(id, userId, contentType, base64Content);
|
||||
});
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/photo/content")
|
||||
public Mono<ResponseEntity<byte[]>> downloadPhoto(
|
||||
@PathVariable Long id,
|
||||
Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return customerService.downloadPhoto(id, userId)
|
||||
.map(base64Content -> {
|
||||
byte[] decodedBytes = Base64.getDecoder().decode(base64Content);
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_TYPE, "image/jpeg")
|
||||
.body(decodedBytes);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.kifi.api.controller.inventory;
|
||||
|
||||
import com.kifi.api.entity.inventory.ProductBom;
|
||||
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;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/kifi-v2/inventory/products")
|
||||
@RequiredArgsConstructor
|
||||
public class ProductBomController {
|
||||
|
||||
private final ProductService productService;
|
||||
|
||||
@GetMapping("/{id}/bom")
|
||||
public Flux<ProductBom> getProductBom(Authentication authentication, @PathVariable Long id) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
// Validation could be added to ensure the user owns the parent product
|
||||
return productService.getProductBom(id);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/bom")
|
||||
public Mono<ResponseEntity<ProductBom>> addOrUpdateBomItem(
|
||||
Authentication authentication,
|
||||
@PathVariable Long id,
|
||||
@RequestBody ProductBom bomItem) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
bomItem.setParentProductId(id);
|
||||
return productService.addOrUpdateBomItem(userId, bomItem)
|
||||
.map(ResponseEntity::ok);
|
||||
}
|
||||
|
||||
@DeleteMapping("/bom/{bomId}")
|
||||
public Mono<ResponseEntity<Void>> deleteBomItem(
|
||||
Authentication authentication,
|
||||
@PathVariable Long bomId) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return productService.deleteBomItem(userId, bomId)
|
||||
.then(Mono.just(ResponseEntity.noContent().<Void>build()));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@ package com.kifi.api.controller.inventory;
|
||||
|
||||
import com.kifi.api.entity.inventory.ProductCategory;
|
||||
import com.kifi.api.repository.inventory.ProductCategoryRepository;
|
||||
import com.kifi.api.entity.inventory.CategoryRateHistory;
|
||||
import com.kifi.api.repository.inventory.CategoryRateHistoryRepository;
|
||||
import com.kifi.api.service.inventory.ProductService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -9,13 +12,18 @@ import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/kifi-v2/inventory/categories")
|
||||
@RequiredArgsConstructor
|
||||
public class ProductCategoryController {
|
||||
private final ProductCategoryRepository categoryRepository;
|
||||
private final CategoryRateHistoryRepository rateHistoryRepository;
|
||||
private final ProductService productService;
|
||||
|
||||
@GetMapping
|
||||
public Flux<ProductCategory> getCategories(Authentication authentication) {
|
||||
@@ -36,13 +44,55 @@ public class ProductCategoryController {
|
||||
public Mono<ResponseEntity<ProductCategory>> updateCategory(@PathVariable Long id, @RequestBody ProductCategory category) {
|
||||
return categoryRepository.findById(id)
|
||||
.flatMap(existing -> {
|
||||
boolean rateChanged = category.getDailyRate() != null && !category.getDailyRate().equals(existing.getDailyRate());
|
||||
existing.setName(category.getName());
|
||||
existing.setParentCategoryId(category.getParentCategoryId());
|
||||
existing.setIsCommodity(category.getIsCommodity());
|
||||
existing.setDailyRate(category.getDailyRate());
|
||||
return categoryRepository.save(existing);
|
||||
|
||||
Mono<ProductCategory> saveMono = categoryRepository.save(existing);
|
||||
|
||||
if (rateChanged && category.getDailyRate() != null) {
|
||||
return saveMono.flatMap(saved ->
|
||||
rateHistoryRepository.findByCategoryIdAndDate(id, LocalDate.now())
|
||||
.defaultIfEmpty(CategoryRateHistory.builder()
|
||||
.categoryId(id)
|
||||
.date(LocalDate.now())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build())
|
||||
.flatMap(history -> {
|
||||
history.setRate(BigDecimal.valueOf(category.getDailyRate()));
|
||||
history.setUpdatedAt(LocalDateTime.now());
|
||||
return rateHistoryRepository.save(history);
|
||||
})
|
||||
.thenReturn(saved)
|
||||
);
|
||||
}
|
||||
return saveMono;
|
||||
})
|
||||
.map(ResponseEntity::ok)
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/rate-history")
|
||||
public Flux<CategoryRateHistory> getRateHistory(@PathVariable Long id) {
|
||||
return rateHistoryRepository.findRecentHistoryByCategoryId(id);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/sync-rates")
|
||||
public Mono<ResponseEntity<Map<String, Object>>> syncRates(@PathVariable Long id) {
|
||||
return categoryRepository.findById(id)
|
||||
.flatMap(category ->
|
||||
productService.syncCategoryRates(id, category.getDailyRate())
|
||||
.count()
|
||||
.map(count -> {
|
||||
Map<String, Object> response = new java.util.HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("syncedCount", count);
|
||||
response.put("message", "Successfully synced rates for " + count + " products.");
|
||||
return ResponseEntity.ok(response);
|
||||
})
|
||||
)
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@ public class ProductController {
|
||||
@GetMapping
|
||||
public Flux<Product> getProducts(
|
||||
Authentication authentication,
|
||||
@RequestParam(required = false) String search,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size
|
||||
) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return productService.getProductsByUserId(userId, page, size);
|
||||
return productService.getProductsByUserId(userId, search, page, size)
|
||||
.doOnNext(p -> System.out.println("Returning product: " + p.getName() + ", currentStock: " + p.getCurrentStock()));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@@ -78,4 +80,24 @@ public class ProductController {
|
||||
public Mono<Void> deleteImage(@PathVariable Long imageId) {
|
||||
return productService.deleteProductImage(imageId);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/movements")
|
||||
public Mono<ResponseEntity<com.kifi.api.entity.inventory.InventoryMovement>> adjustStock(
|
||||
Authentication authentication,
|
||||
@PathVariable Long id,
|
||||
@RequestBody com.kifi.api.entity.inventory.InventoryMovement movement
|
||||
) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return productService.adjustStock(userId, id, movement)
|
||||
.map(ResponseEntity::ok);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/movements")
|
||||
public Flux<com.kifi.api.entity.inventory.InventoryMovement> getStockLedger(
|
||||
Authentication authentication,
|
||||
@PathVariable Long id
|
||||
) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return productService.getProductStockLedger(userId, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.kifi.api.controller.inventory;
|
||||
|
||||
import com.kifi.api.entity.inventory.UnitOfMeasure;
|
||||
import com.kifi.api.service.inventory.UnitOfMeasureService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
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/inventory/uom")
|
||||
@RequiredArgsConstructor
|
||||
public class UnitOfMeasureController {
|
||||
|
||||
private final UnitOfMeasureService uomService;
|
||||
|
||||
@GetMapping
|
||||
public Flux<UnitOfMeasure> getUoms(Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return uomService.getUomsByUserId(userId);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<UnitOfMeasure> getUomById(@PathVariable Long id, Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return uomService.getUomById(id, userId);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public Mono<UnitOfMeasure> createUom(@RequestBody UnitOfMeasure uom, Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
uom.setUserId(userId);
|
||||
return uomService.createUom(uom);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Mono<UnitOfMeasure> updateUom(@PathVariable Long id, @RequestBody UnitOfMeasure uom, Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return uomService.updateUom(id, uom, userId);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public Mono<Void> deleteUom(@PathVariable Long id, Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return uomService.deleteUom(id, userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.kifi.api.controller.invoice;
|
||||
|
||||
import com.kifi.api.entity.invoice.Invoice;
|
||||
import com.kifi.api.service.invoice.InvoiceService;
|
||||
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/invoices")
|
||||
@RequiredArgsConstructor
|
||||
public class InvoiceController {
|
||||
|
||||
private final InvoiceService invoiceService;
|
||||
|
||||
@GetMapping
|
||||
public Flux<Invoice> getInvoices(Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return invoiceService.getInvoices(userId);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<Invoice> getInvoiceById(
|
||||
@PathVariable Long id,
|
||||
Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return invoiceService.getInvoiceById(id, userId);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Mono<Invoice> createInvoice(
|
||||
Authentication authentication,
|
||||
@RequestBody Invoice invoice) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return invoiceService.createInvoice(userId, invoice);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/finalize")
|
||||
public Mono<Invoice> finalizeInvoice(
|
||||
@PathVariable Long id,
|
||||
Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return invoiceService.finalizeInvoice(id, userId);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/payments")
|
||||
public Mono<com.kifi.api.entity.invoice.InvoicePayment> addPayment(
|
||||
@PathVariable Long id,
|
||||
Authentication authentication,
|
||||
@RequestBody com.kifi.api.entity.invoice.InvoicePayment payment) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return invoiceService.addPaymentToInvoice(id, userId, payment);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/payments")
|
||||
public Flux<com.kifi.api.entity.invoice.InvoicePayment> getPayments(
|
||||
@PathVariable Long id,
|
||||
Authentication authentication) {
|
||||
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||
return invoiceService.getPaymentsForInvoice(id, userId);
|
||||
}
|
||||
}
|
||||
@@ -18,5 +18,10 @@ public class Wallet {
|
||||
private String currency;
|
||||
private String icon;
|
||||
private String color;
|
||||
private String subNature;
|
||||
private java.math.BigDecimal creditLimit;
|
||||
private java.math.BigDecimal fixedAmount;
|
||||
private String paymentCycle;
|
||||
private Integer cycleDate;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
@@ -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.time.LocalDateTime;
|
||||
@@ -21,5 +22,16 @@ public class BusinessFeature {
|
||||
private Boolean inventoryManagement;
|
||||
private Boolean salesManagement;
|
||||
private Boolean multiLocation;
|
||||
|
||||
@Column("bom_reduction_strategy")
|
||||
private String bomReductionStrategy; // "COMPONENTS_ONLY" or "PARENT_AND_COMPONENTS"
|
||||
|
||||
@Column("stock_deduction_on_invoice")
|
||||
private Boolean stockDeductionOnInvoice;
|
||||
|
||||
@Column("barcode_source")
|
||||
private String barcodeSource;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,13 @@ public class BusinessProfile {
|
||||
private String taxNumber;
|
||||
private String currency;
|
||||
private Boolean taxIncludedInPrice;
|
||||
private String address;
|
||||
private Long stateId;
|
||||
private String contactPerson;
|
||||
private String contactNumber;
|
||||
private String emailId;
|
||||
private String panNumber;
|
||||
private String gstin;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Table("indian_states")
|
||||
public class IndianState {
|
||||
@Id
|
||||
private Long id;
|
||||
private String name;
|
||||
private String gstCode;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.kifi.api.entity.customer;
|
||||
|
||||
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.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Table("customers")
|
||||
public class Customer {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Column("user_id")
|
||||
private Long userId;
|
||||
|
||||
private String name;
|
||||
private String email;
|
||||
private String phone;
|
||||
private String address;
|
||||
private String gstin;
|
||||
|
||||
@Column("father_name")
|
||||
private String fatherName;
|
||||
|
||||
private String gender;
|
||||
private Integer age;
|
||||
|
||||
@Column("id_number")
|
||||
private String idNumber;
|
||||
|
||||
@Column("state_id")
|
||||
private Integer stateId;
|
||||
|
||||
@Column("photo_url")
|
||||
private String photoUrl;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Table("category_rate_history")
|
||||
public class CategoryRateHistory {
|
||||
@Id
|
||||
private Long id;
|
||||
private Long categoryId;
|
||||
private BigDecimal rate;
|
||||
private LocalDate date;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -23,4 +23,7 @@ public class InventoryMovement {
|
||||
private Long referenceTransactionId;
|
||||
private String notes;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@org.springframework.data.annotation.Transient
|
||||
private java.util.List<InventoryMovementItem> items;
|
||||
}
|
||||
|
||||
@@ -49,4 +49,7 @@ public class Product {
|
||||
|
||||
@Transient
|
||||
private List<ProductImage> images;
|
||||
|
||||
@Transient
|
||||
private BigDecimal currentStock;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
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("invoices")
|
||||
public class Invoice {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Column("user_id")
|
||||
private Long userId;
|
||||
|
||||
@Column("customer_id")
|
||||
private Long customerId;
|
||||
|
||||
@Column("invoice_number")
|
||||
private String invoiceNumber;
|
||||
|
||||
@Column("issue_date")
|
||||
private LocalDate issueDate;
|
||||
|
||||
@Column("due_date")
|
||||
private LocalDate dueDate;
|
||||
|
||||
private BigDecimal subtotal;
|
||||
|
||||
@Column("tax_total")
|
||||
private BigDecimal taxTotal;
|
||||
|
||||
@Column("discount_total")
|
||||
private BigDecimal discountTotal;
|
||||
|
||||
@Column("total_amount")
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
private String status; // DRAFT, SENT, PAID, PARTIAL, OVERDUE, CANCELLED
|
||||
private String notes;
|
||||
|
||||
@Column("amount_paid")
|
||||
private BigDecimal amountPaid;
|
||||
|
||||
@Column("next_payment_date")
|
||||
private LocalDate nextPaymentDate;
|
||||
|
||||
// EMI fields
|
||||
@Column("is_emi")
|
||||
private Boolean isEmi;
|
||||
|
||||
@Column("emi_amount")
|
||||
private BigDecimal emiAmount;
|
||||
|
||||
@Column("emi_cycle")
|
||||
private String emiCycle; // MONTHLY, WEEKLY
|
||||
|
||||
@Column("emi_start_date")
|
||||
private LocalDate emiStartDate;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Transient
|
||||
private List<InvoiceItem> items;
|
||||
|
||||
@Transient
|
||||
private String paymentMethod;
|
||||
|
||||
@Transient
|
||||
private Long paymentWalletId;
|
||||
|
||||
@Transient
|
||||
private List<InvoicePayment> payments;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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("invoice_items")
|
||||
public class InvoiceItem {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Column("invoice_id")
|
||||
private Long invoiceId;
|
||||
|
||||
@Column("product_id")
|
||||
private Long productId;
|
||||
|
||||
private String description;
|
||||
|
||||
private BigDecimal quantity;
|
||||
|
||||
@Column("unit_price")
|
||||
private BigDecimal unitPrice;
|
||||
|
||||
@Column("tax_rate")
|
||||
private BigDecimal taxRate;
|
||||
|
||||
private BigDecimal discount;
|
||||
|
||||
@Column("making_charge")
|
||||
private BigDecimal makingCharge;
|
||||
|
||||
@Column("other_charges")
|
||||
private BigDecimal otherCharges;
|
||||
|
||||
private BigDecimal total;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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("invoice_payments")
|
||||
public class InvoicePayment {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Column("invoice_id")
|
||||
private Long invoiceId;
|
||||
|
||||
@Column("transaction_id")
|
||||
private Long transactionId;
|
||||
|
||||
@Column("wallet_id")
|
||||
private Long walletId;
|
||||
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column("payment_date")
|
||||
private LocalDate paymentDate;
|
||||
|
||||
@Column("emi_installment_number")
|
||||
private Integer emiInstallmentNumber;
|
||||
|
||||
@Column("payment_method")
|
||||
private String paymentMethod;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import reactor.core.publisher.Mono;
|
||||
|
||||
@Repository
|
||||
public interface TransactionRepository extends R2dbcRepository<Transaction, Long>, TransactionRepositoryCustom {
|
||||
@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")
|
||||
@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);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public class TransactionRepositoryImpl implements TransactionRepositoryCustom {
|
||||
baseQuery.append(" AND t.description ILIKE :search");
|
||||
}
|
||||
|
||||
String dataQueryStr = "SELECT t.* " + baseQuery.toString() + " ORDER BY t.date DESC LIMIT " + pageable.getPageSize() + " OFFSET " + pageable.getOffset();
|
||||
String dataQueryStr = "SELECT t.* " + baseQuery.toString() + " ORDER BY t.date DESC, t.id DESC LIMIT " + pageable.getPageSize() + " OFFSET " + pageable.getOffset();
|
||||
String countQueryStr = "SELECT COUNT(t.id) " + baseQuery.toString();
|
||||
|
||||
DatabaseClient.GenericExecuteSpec dataSpec = databaseClient.sql(dataQueryStr).bind("userId", userId);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.kifi.api.repository.business;
|
||||
|
||||
import com.kifi.api.entity.business.IndianState;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@Repository
|
||||
public interface IndianStateRepository extends ReactiveCrudRepository<IndianState, Long> {
|
||||
Flux<IndianState> findAllByOrderByNameAsc();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.kifi.api.repository.customer;
|
||||
|
||||
import com.kifi.api.entity.customer.Customer;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public interface CustomerRepository extends ReactiveCrudRepository<Customer, Long> {
|
||||
Flux<Customer> findByUserId(Long userId);
|
||||
Flux<Customer> findByUserIdAndNameContainingIgnoreCase(Long userId, String name);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.kifi.api.repository.inventory;
|
||||
|
||||
import com.kifi.api.entity.inventory.CategoryRateHistory;
|
||||
import org.springframework.data.r2dbc.repository.Query;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import java.time.LocalDate;
|
||||
|
||||
public interface CategoryRateHistoryRepository extends ReactiveCrudRepository<CategoryRateHistory, Long> {
|
||||
Flux<CategoryRateHistory> findByCategoryIdOrderByDateDesc(Long categoryId);
|
||||
Mono<CategoryRateHistory> findByCategoryIdAndDate(Long categoryId, LocalDate date);
|
||||
|
||||
@Query("SELECT * FROM category_rate_history WHERE category_id = :categoryId ORDER BY date DESC LIMIT 30")
|
||||
Flux<CategoryRateHistory> findRecentHistoryByCategoryId(Long categoryId);
|
||||
}
|
||||
@@ -8,4 +8,5 @@ import reactor.core.publisher.Flux;
|
||||
public interface InventoryBalanceRepository extends ReactiveCrudRepository<InventoryBalance, Long> {
|
||||
Mono<InventoryBalance> findByProductIdAndLocationId(Long productId, Long locationId);
|
||||
Flux<InventoryBalance> findByLocationId(Long locationId);
|
||||
Flux<InventoryBalance> findByProductId(Long productId);
|
||||
}
|
||||
|
||||
@@ -7,4 +7,9 @@ import reactor.core.publisher.Flux;
|
||||
|
||||
public interface ProductRepository extends ReactiveCrudRepository<Product, Long> {
|
||||
Flux<Product> findByUserId(Long userId, Pageable pageable);
|
||||
|
||||
@org.springframework.data.r2dbc.repository.Query("SELECT * FROM products WHERE user_id = :userId AND (:keyword IS NULL OR :keyword = '' OR LOWER(name) LIKE LOWER(CONCAT('%', :keyword, '%')) OR LOWER(sku) LIKE LOWER(CONCAT('%', :keyword, '%'))) OFFSET :offset LIMIT :limit")
|
||||
Flux<Product> searchProducts(Long userId, String keyword, long offset, int limit);
|
||||
|
||||
Flux<Product> findByCategoryId(Long categoryId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.kifi.api.repository.invoice;
|
||||
|
||||
import com.kifi.api.entity.invoice.InvoiceItem;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface InvoiceItemRepository extends ReactiveCrudRepository<InvoiceItem, Long> {
|
||||
Flux<InvoiceItem> findByInvoiceId(Long invoiceId);
|
||||
Mono<Void> deleteByInvoiceId(Long invoiceId);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kifi.api.repository.invoice;
|
||||
|
||||
import com.kifi.api.entity.invoice.InvoicePayment;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public interface InvoicePaymentRepository extends ReactiveCrudRepository<InvoicePayment, Long> {
|
||||
Flux<InvoicePayment> findByInvoiceId(Long invoiceId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.kifi.api.repository.invoice;
|
||||
|
||||
import com.kifi.api.entity.invoice.Invoice;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface InvoiceRepository extends ReactiveCrudRepository<Invoice, Long> {
|
||||
Flux<Invoice> findByUserId(Long userId);
|
||||
Flux<Invoice> findByUserIdAndCustomerId(Long userId, Long customerId);
|
||||
Mono<Invoice> findByUserIdAndInvoiceNumber(Long userId, String invoiceNumber);
|
||||
}
|
||||
@@ -11,10 +11,10 @@ public class MinioServiceClient {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
@Value("${minio.service.url:http://minio-service:1500}")
|
||||
@Value("${spring.minio.service.url:http://minio-service:1500}")
|
||||
private String minioServiceUrl;
|
||||
|
||||
@Value("${minio.service.bucket:kifi}")
|
||||
@Value("${spring.minio.service.bucket:kifi}")
|
||||
private String bucketName;
|
||||
|
||||
public MinioServiceClient(WebClient.Builder webClientBuilder) {
|
||||
|
||||
@@ -29,7 +29,7 @@ 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) {
|
||||
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) {
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setOwnerId(ownerId);
|
||||
wallet.setName(name);
|
||||
@@ -38,6 +38,11 @@ public class WalletService {
|
||||
wallet.setCurrency(currency != null ? currency : "INR");
|
||||
wallet.setIcon(icon);
|
||||
wallet.setColor(color);
|
||||
wallet.setSubNature(subNature);
|
||||
wallet.setCreditLimit(creditLimit);
|
||||
wallet.setFixedAmount(fixedAmount);
|
||||
wallet.setPaymentCycle(paymentCycle);
|
||||
wallet.setCycleDate(cycleDate);
|
||||
wallet.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
return walletRepository.save(wallet)
|
||||
@@ -112,7 +117,7 @@ public class WalletService {
|
||||
});
|
||||
}
|
||||
|
||||
public Mono<Wallet> editWallet(Long walletId, Long ownerId, String name, String nature, String icon, String color, String currency) {
|
||||
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) {
|
||||
return walletRepository.findById(walletId)
|
||||
.filter(w -> w.getOwnerId().equals(ownerId))
|
||||
.switchIfEmpty(Mono.error(new RuntimeException("Wallet not found or you are not the owner")))
|
||||
@@ -122,6 +127,11 @@ public class WalletService {
|
||||
if (icon != null) w.setIcon(icon);
|
||||
if (color != null) w.setColor(color);
|
||||
if (currency != null) w.setCurrency(currency);
|
||||
if (subNature != null) w.setSubNature(subNature);
|
||||
if (creditLimit != null) w.setCreditLimit(creditLimit);
|
||||
if (fixedAmount != null) w.setFixedAmount(fixedAmount);
|
||||
if (paymentCycle != null) w.setPaymentCycle(paymentCycle);
|
||||
if (cycleDate != null) w.setCycleDate(cycleDate);
|
||||
return walletRepository.save(w);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.kifi.api.service.business;
|
||||
|
||||
import com.kifi.api.entity.business.BusinessProfile;
|
||||
import com.kifi.api.entity.business.BusinessFeature;
|
||||
import com.kifi.api.repository.business.BusinessProfileRepository;
|
||||
import com.kifi.api.repository.business.BusinessFeatureRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -12,10 +14,44 @@ import java.time.LocalDateTime;
|
||||
@RequiredArgsConstructor
|
||||
public class BusinessService {
|
||||
private final BusinessProfileRepository businessProfileRepository;
|
||||
private final BusinessFeatureRepository businessFeatureRepository;
|
||||
|
||||
public Mono<BusinessProfile> getProfileByUserId(Long userId) {
|
||||
return businessProfileRepository.findByUserId(userId);
|
||||
}
|
||||
|
||||
public Mono<BusinessFeature> getFeaturesByUserId(Long userId) {
|
||||
return businessFeatureRepository.findByUserId(userId)
|
||||
.defaultIfEmpty(BusinessFeature.builder()
|
||||
.userId(userId)
|
||||
.inventoryManagement(false)
|
||||
.salesManagement(false)
|
||||
.multiLocation(false)
|
||||
.bomReductionStrategy("COMPONENTS_ONLY")
|
||||
.stockDeductionOnInvoice(true)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build());
|
||||
}
|
||||
|
||||
public Mono<BusinessFeature> saveFeatures(Long userId, BusinessFeature feature) {
|
||||
return businessFeatureRepository.findByUserId(userId)
|
||||
.flatMap(existing -> {
|
||||
if (feature.getInventoryManagement() != null) existing.setInventoryManagement(feature.getInventoryManagement());
|
||||
if (feature.getSalesManagement() != null) existing.setSalesManagement(feature.getSalesManagement());
|
||||
if (feature.getMultiLocation() != null) existing.setMultiLocation(feature.getMultiLocation());
|
||||
if (feature.getBomReductionStrategy() != null) existing.setBomReductionStrategy(feature.getBomReductionStrategy());
|
||||
if (feature.getStockDeductionOnInvoice() != null) existing.setStockDeductionOnInvoice(feature.getStockDeductionOnInvoice());
|
||||
if (feature.getBarcodeSource() != null) existing.setBarcodeSource(feature.getBarcodeSource());
|
||||
return businessFeatureRepository.save(existing);
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
feature.setUserId(userId);
|
||||
feature.setCreatedAt(LocalDateTime.now());
|
||||
if (feature.getBomReductionStrategy() == null) feature.setBomReductionStrategy("COMPONENTS_ONLY");
|
||||
if (feature.getStockDeductionOnInvoice() == null) feature.setStockDeductionOnInvoice(true);
|
||||
return businessFeatureRepository.save(feature);
|
||||
}));
|
||||
}
|
||||
|
||||
public Mono<BusinessProfile> saveProfile(Long userId, BusinessProfile profile) {
|
||||
return businessProfileRepository.findByUserId(userId)
|
||||
@@ -23,6 +59,13 @@ public class BusinessService {
|
||||
existing.setBusinessName(profile.getBusinessName());
|
||||
existing.setIndustry(profile.getIndustry());
|
||||
existing.setTaxNumber(profile.getTaxNumber());
|
||||
existing.setAddress(profile.getAddress());
|
||||
existing.setStateId(profile.getStateId());
|
||||
existing.setContactPerson(profile.getContactPerson());
|
||||
existing.setContactNumber(profile.getContactNumber());
|
||||
existing.setEmailId(profile.getEmailId());
|
||||
existing.setPanNumber(profile.getPanNumber());
|
||||
existing.setGstin(profile.getGstin());
|
||||
existing.setCurrency(profile.getCurrency() != null ? profile.getCurrency() : existing.getCurrency());
|
||||
existing.setUpdatedAt(LocalDateTime.now());
|
||||
return businessProfileRepository.save(existing);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.kifi.api.service.customer;
|
||||
|
||||
import com.kifi.api.entity.customer.Customer;
|
||||
import com.kifi.api.repository.customer.CustomerRepository;
|
||||
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 java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CustomerService {
|
||||
|
||||
private final CustomerRepository customerRepository;
|
||||
private final MinioServiceClient minioServiceClient;
|
||||
|
||||
public Flux<Customer> getCustomers(Long userId, String search) {
|
||||
if (search != null && !search.isEmpty()) {
|
||||
return customerRepository.findByUserIdAndNameContainingIgnoreCase(userId, search);
|
||||
}
|
||||
return customerRepository.findByUserId(userId);
|
||||
}
|
||||
|
||||
public Mono<Customer> getCustomerById(Long id, Long userId) {
|
||||
return customerRepository.findById(id)
|
||||
.filter(customer -> customer.getUserId().equals(userId))
|
||||
.switchIfEmpty(Mono.error(new RuntimeException("Customer not found or unauthorized")));
|
||||
}
|
||||
|
||||
public Mono<Customer> createCustomer(Long userId, Customer customer) {
|
||||
customer.setUserId(userId);
|
||||
customer.setCreatedAt(LocalDateTime.now());
|
||||
return customerRepository.save(customer);
|
||||
}
|
||||
|
||||
public Mono<Customer> updateCustomer(Long id, Long userId, Customer customer) {
|
||||
return getCustomerById(id, userId)
|
||||
.flatMap(existing -> {
|
||||
if (customer.getName() != null) existing.setName(customer.getName());
|
||||
if (customer.getEmail() != null) existing.setEmail(customer.getEmail());
|
||||
if (customer.getPhone() != null) existing.setPhone(customer.getPhone());
|
||||
if (customer.getAddress() != null) existing.setAddress(customer.getAddress());
|
||||
if (customer.getGstin() != null) existing.setGstin(customer.getGstin());
|
||||
if (customer.getIdNumber() != null) existing.setIdNumber(customer.getIdNumber());
|
||||
if (customer.getStateId() != null) existing.setStateId(customer.getStateId());
|
||||
if (customer.getPhotoUrl() != null) existing.setPhotoUrl(customer.getPhotoUrl());
|
||||
return customerRepository.save(existing);
|
||||
});
|
||||
}
|
||||
|
||||
public Mono<Void> deleteCustomer(Long id, Long userId) {
|
||||
return getCustomerById(id, userId)
|
||||
.flatMap(customerRepository::delete);
|
||||
}
|
||||
|
||||
public Mono<Customer> uploadPhoto(Long id, Long userId, String contentType, String base64Content) {
|
||||
return getCustomerById(id, userId)
|
||||
.flatMap(customer -> {
|
||||
String directoryPath = "users/" + userId + "/customers/" + id;
|
||||
String fileName = "photo.jpg";
|
||||
return minioServiceClient.uploadFile(directoryPath, contentType, fileName, base64Content)
|
||||
.flatMap(response -> {
|
||||
if (response.isSuccess()) {
|
||||
customer.setPhotoUrl(response.getFilePath());
|
||||
return customerRepository.save(customer);
|
||||
} else {
|
||||
return Mono.error(new RuntimeException("Failed to upload photo to Minio"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public Mono<String> downloadPhoto(Long id, Long userId) {
|
||||
return getCustomerById(id, userId)
|
||||
.flatMap(customer -> {
|
||||
if (customer.getPhotoUrl() == null) return Mono.empty();
|
||||
return minioServiceClient.downloadFile("photo", customer.getPhotoUrl())
|
||||
.map(com.kifi.api.service.MinioServiceClient.MinioDownloadResponse::getBase64Content);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,17 @@ 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.repository.inventory.InventoryBalanceRepository;
|
||||
import com.kifi.api.repository.inventory.InventoryMovementRepository;
|
||||
import com.kifi.api.repository.inventory.InventoryMovementItemRepository;
|
||||
import com.kifi.api.repository.inventory.InventoryLocationRepository;
|
||||
import com.kifi.api.repository.inventory.ProductBomRepository;
|
||||
import com.kifi.api.repository.business.BusinessFeatureRepository;
|
||||
import com.kifi.api.entity.inventory.InventoryBalance;
|
||||
import com.kifi.api.entity.inventory.InventoryLocation;
|
||||
import com.kifi.api.entity.inventory.InventoryMovement;
|
||||
import com.kifi.api.entity.inventory.InventoryMovementItem;
|
||||
import com.kifi.api.entity.inventory.ProductBom;
|
||||
import com.kifi.api.service.MinioServiceClient;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -12,21 +23,42 @@ import reactor.core.publisher.Mono;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import java.util.UUID;
|
||||
import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ProductService {
|
||||
private final ProductRepository productRepository;
|
||||
private final ProductImageRepository productImageRepository;
|
||||
private final InventoryBalanceRepository inventoryBalanceRepository;
|
||||
private final InventoryMovementRepository inventoryMovementRepository;
|
||||
private final InventoryMovementItemRepository inventoryMovementItemRepository;
|
||||
private final InventoryLocationRepository inventoryLocationRepository;
|
||||
private final ProductBomRepository productBomRepository;
|
||||
private final BusinessFeatureRepository businessFeatureRepository;
|
||||
private final MinioServiceClient minioServiceClient;
|
||||
|
||||
public Flux<Product> getProductsByUserId(Long userId, int page, int size) {
|
||||
return productRepository.findByUserId(userId, PageRequest.of(page, size))
|
||||
public Flux<Product> getProductsByUserId(Long userId, String search, int page, int size) {
|
||||
long offset = (long) page * size;
|
||||
Flux<Product> productSource = (search != null && !search.trim().isEmpty())
|
||||
? productRepository.searchProducts(userId, search, offset, size)
|
||||
: productRepository.findByUserId(userId, PageRequest.of(page, size));
|
||||
|
||||
return productSource
|
||||
.flatMap(product -> productImageRepository.findByProductId(product.getId()).collectList()
|
||||
.map(images -> {
|
||||
product.setImages(images);
|
||||
return product;
|
||||
})
|
||||
)
|
||||
.flatMap(product -> inventoryBalanceRepository.findByProductId(product.getId())
|
||||
.map(InventoryBalance::getQuantity)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.map(totalStock -> {
|
||||
product.setCurrentStock(totalStock);
|
||||
return product;
|
||||
})
|
||||
.defaultIfEmpty(product)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +80,7 @@ public class ProductService {
|
||||
existingProduct.setName(updatedProduct.getName());
|
||||
existingProduct.setSku(updatedProduct.getSku());
|
||||
existingProduct.setCategoryId(updatedProduct.getCategoryId());
|
||||
existingProduct.setUomId(updatedProduct.getUomId());
|
||||
existingProduct.setPurchasePrice(updatedProduct.getPurchasePrice());
|
||||
existingProduct.setSellingPrice(updatedProduct.getSellingPrice());
|
||||
existingProduct.setGstRate(updatedProduct.getGstRate());
|
||||
@@ -106,4 +139,191 @@ public class ProductService {
|
||||
minioServiceClient.downloadFile(image.getContentType(), image.getFilePath())
|
||||
);
|
||||
}
|
||||
|
||||
public Flux<Product> syncCategoryRates(Long categoryId, Double dailyRate) {
|
||||
if (dailyRate == null) return Flux.empty();
|
||||
|
||||
return productRepository.findByCategoryId(categoryId)
|
||||
.filter(p -> Boolean.TRUE.equals(p.getAutoCalculatePrice()))
|
||||
.flatMap(product -> {
|
||||
double weight = product.getWeight() != null ? product.getWeight().doubleValue() : 0.0;
|
||||
double baseQuantity = (weight > 0) ? weight : 1.0;
|
||||
|
||||
double wastage = product.getWastagePercentage() != null ? product.getWastagePercentage() : 0.0;
|
||||
double purity = product.getPurityFactor() != null ? product.getPurityFactor() : 1.0;
|
||||
|
||||
double materialQuantity = baseQuantity + (baseQuantity * (wastage / 100.0));
|
||||
double materialCost = materialQuantity * dailyRate * purity;
|
||||
|
||||
double makingCharges = product.getMakingCharges() != null ? product.getMakingCharges() : 0.0;
|
||||
String makingType = product.getMakingChargesType() != null ? product.getMakingChargesType() : "FLAT";
|
||||
|
||||
double making = 0.0;
|
||||
if ("FLAT".equals(makingType)) {
|
||||
making = makingCharges;
|
||||
} else if ("PER_UNIT".equals(makingType)) {
|
||||
making = makingCharges * baseQuantity;
|
||||
} else if ("PERCENTAGE".equals(makingType)) {
|
||||
making = materialCost * (makingCharges / 100.0);
|
||||
}
|
||||
|
||||
double newPrice = materialCost + making;
|
||||
product.setSellingPrice(BigDecimal.valueOf(newPrice));
|
||||
product.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
return productRepository.save(product);
|
||||
});
|
||||
}
|
||||
|
||||
public Mono<InventoryMovement> adjustStock(Long userId, Long productId, InventoryMovement movement) {
|
||||
if (movement.getItems() == null || movement.getItems().isEmpty()) {
|
||||
return Mono.error(new IllegalArgumentException("Movement items are required"));
|
||||
}
|
||||
|
||||
movement.setUserId(userId);
|
||||
movement.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
Mono<Long> locationIdMono = movement.getLocationId() != null
|
||||
? Mono.just(movement.getLocationId())
|
||||
: inventoryLocationRepository.findByUserId(userId)
|
||||
.next()
|
||||
.map(InventoryLocation::getId)
|
||||
.switchIfEmpty(inventoryLocationRepository.save(InventoryLocation.builder()
|
||||
.userId(userId)
|
||||
.name("Main Store")
|
||||
.isPrimary(true)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build())
|
||||
.map(InventoryLocation::getId));
|
||||
|
||||
return productRepository.findById(productId)
|
||||
.filter(p -> p.getUserId().equals(userId))
|
||||
.switchIfEmpty(Mono.error(new RuntimeException("Product not found or access denied")))
|
||||
.flatMap(product -> locationIdMono.flatMap(locId -> {
|
||||
movement.setLocationId(locId);
|
||||
return inventoryBalanceRepository.findByProductIdAndLocationId(productId, locId)
|
||||
.defaultIfEmpty(InventoryBalance.builder()
|
||||
.productId(productId)
|
||||
.locationId(locId)
|
||||
.quantity(BigDecimal.ZERO)
|
||||
.lastUpdated(LocalDateTime.now())
|
||||
.build());
|
||||
}))
|
||||
.flatMap(balance -> {
|
||||
BigDecimal qtyChange = movement.getItems().get(0).getQuantity();
|
||||
if ("REDUCTION".equals(movement.getType()) || "DAMAGE".equals(movement.getType())) {
|
||||
qtyChange = qtyChange.negate();
|
||||
}
|
||||
|
||||
final BigDecimal finalQtyChange = qtyChange;
|
||||
return businessFeatureRepository.findByUserId(userId)
|
||||
.map(f -> f.getBomReductionStrategy() != null ? f.getBomReductionStrategy() : "COMPONENTS_ONLY")
|
||||
.defaultIfEmpty("COMPONENTS_ONLY")
|
||||
.flatMap(strategy -> {
|
||||
Mono<InventoryBalance> parentProcess = Mono.just(balance);
|
||||
if (!"COMPONENTS_ONLY".equals(strategy) || finalQtyChange.compareTo(BigDecimal.ZERO) >= 0 || !"REDUCTION".equals(movement.getType())) {
|
||||
balance.setQuantity(balance.getQuantity().add(finalQtyChange));
|
||||
balance.setLastUpdated(LocalDateTime.now());
|
||||
parentProcess = inventoryBalanceRepository.save(balance);
|
||||
}
|
||||
|
||||
return parentProcess;
|
||||
})
|
||||
.flatMap(savedBalance -> inventoryMovementRepository.save(movement))
|
||||
.flatMap(savedMovement -> {
|
||||
InventoryMovementItem item = movement.getItems().get(0);
|
||||
item.setMovementId(savedMovement.getId());
|
||||
item.setProductId(productId);
|
||||
item.setCreatedAt(LocalDateTime.now());
|
||||
return inventoryMovementItemRepository.save(item)
|
||||
.flatMap(savedItem -> {
|
||||
java.util.List<InventoryMovementItem> items = new java.util.ArrayList<>();
|
||||
items.add(savedItem);
|
||||
savedMovement.setItems(items);
|
||||
|
||||
if (finalQtyChange.compareTo(BigDecimal.ZERO) < 0 && "REDUCTION".equals(movement.getType())) {
|
||||
return reduceBomComponentsWithItems(userId, productId, savedMovement.getId(), movement.getLocationId(), finalQtyChange)
|
||||
.map(componentItems -> {
|
||||
savedMovement.getItems().addAll(componentItems);
|
||||
return savedMovement;
|
||||
});
|
||||
}
|
||||
return Mono.just(savedMovement);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<java.util.List<InventoryMovementItem>> reduceBomComponentsWithItems(Long userId, Long parentProductId, Long movementId, Long locationId, BigDecimal parentQtyChange) {
|
||||
return productBomRepository.findByParentProductId(parentProductId)
|
||||
.flatMap(bomItem -> {
|
||||
BigDecimal componentQtyChange = bomItem.getQuantity().multiply(parentQtyChange);
|
||||
return inventoryBalanceRepository.findByProductIdAndLocationId(bomItem.getComponentProductId(), locationId)
|
||||
.defaultIfEmpty(InventoryBalance.builder()
|
||||
.productId(bomItem.getComponentProductId())
|
||||
.locationId(locationId)
|
||||
.quantity(BigDecimal.ZERO)
|
||||
.lastUpdated(LocalDateTime.now())
|
||||
.build())
|
||||
.flatMap(balance -> {
|
||||
balance.setQuantity(balance.getQuantity().add(componentQtyChange));
|
||||
balance.setLastUpdated(LocalDateTime.now());
|
||||
return inventoryBalanceRepository.save(balance);
|
||||
})
|
||||
.flatMap(savedBalance -> {
|
||||
InventoryMovementItem compItem = new InventoryMovementItem();
|
||||
compItem.setMovementId(movementId);
|
||||
compItem.setProductId(bomItem.getComponentProductId());
|
||||
compItem.setQuantity(componentQtyChange.abs());
|
||||
compItem.setCreatedAt(LocalDateTime.now());
|
||||
return inventoryMovementItemRepository.save(compItem);
|
||||
});
|
||||
})
|
||||
.collectList();
|
||||
}
|
||||
|
||||
public Flux<InventoryMovement> getProductStockLedger(Long userId, Long productId) {
|
||||
// Find all movement items for the given product, then fetch their parent movements
|
||||
return inventoryMovementItemRepository.findAll()
|
||||
.filter(item -> productId.equals(item.getProductId()))
|
||||
.flatMap(item -> inventoryMovementRepository.findById(item.getMovementId())
|
||||
.map(movement -> {
|
||||
movement.setItems(java.util.Collections.singletonList(item));
|
||||
return movement;
|
||||
})
|
||||
)
|
||||
.filter(movement -> userId.equals(movement.getUserId()))
|
||||
.sort((m1, m2) -> m2.getCreatedAt().compareTo(m1.getCreatedAt())); // Descending order
|
||||
}
|
||||
|
||||
public Flux<ProductBom> getProductBom(Long parentProductId) {
|
||||
return productBomRepository.findByParentProductId(parentProductId);
|
||||
}
|
||||
|
||||
public Mono<ProductBom> addOrUpdateBomItem(Long userId, ProductBom bomItem) {
|
||||
return productRepository.findById(bomItem.getParentProductId())
|
||||
.filter(p -> p.getUserId().equals(userId))
|
||||
.switchIfEmpty(Mono.error(new RuntimeException("Parent product not found or access denied")))
|
||||
.flatMap(p -> {
|
||||
if (bomItem.getId() != null) {
|
||||
return productBomRepository.findById(bomItem.getId())
|
||||
.flatMap(existing -> {
|
||||
existing.setComponentProductId(bomItem.getComponentProductId());
|
||||
existing.setQuantity(bomItem.getQuantity());
|
||||
return productBomRepository.save(existing);
|
||||
});
|
||||
} else {
|
||||
bomItem.setCreatedAt(LocalDateTime.now());
|
||||
return productBomRepository.save(bomItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Mono<Void> deleteBomItem(Long userId, Long bomId) {
|
||||
return productBomRepository.findById(bomId)
|
||||
.flatMap(bom -> productRepository.findById(bom.getParentProductId())
|
||||
.filter(p -> p.getUserId().equals(userId))
|
||||
.switchIfEmpty(Mono.error(new RuntimeException("Access denied")))
|
||||
.flatMap(p -> productBomRepository.delete(bom)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.kifi.api.service.inventory;
|
||||
|
||||
import com.kifi.api.entity.inventory.UnitOfMeasure;
|
||||
import com.kifi.api.repository.inventory.UnitOfMeasureRepository;
|
||||
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 UnitOfMeasureService {
|
||||
|
||||
private final UnitOfMeasureRepository uomRepository;
|
||||
|
||||
public Flux<UnitOfMeasure> getUomsByUserId(Long userId) {
|
||||
return uomRepository.findByUserId(userId);
|
||||
}
|
||||
|
||||
public Mono<UnitOfMeasure> getUomById(Long id, Long userId) {
|
||||
return uomRepository.findById(id)
|
||||
.filter(uom -> uom.getUserId().equals(userId));
|
||||
}
|
||||
|
||||
public Mono<UnitOfMeasure> createUom(UnitOfMeasure uom) {
|
||||
uom.setCreatedAt(LocalDateTime.now());
|
||||
return uomRepository.save(uom);
|
||||
}
|
||||
|
||||
public Mono<UnitOfMeasure> updateUom(Long id, UnitOfMeasure updatedUom, Long userId) {
|
||||
return uomRepository.findById(id)
|
||||
.filter(uom -> uom.getUserId().equals(userId))
|
||||
.flatMap(existingUom -> {
|
||||
existingUom.setName(updatedUom.getName());
|
||||
existingUom.setAbbreviation(updatedUom.getAbbreviation());
|
||||
return uomRepository.save(existingUom);
|
||||
});
|
||||
}
|
||||
|
||||
public Mono<Void> deleteUom(Long id, Long userId) {
|
||||
return uomRepository.findById(id)
|
||||
.filter(uom -> uom.getUserId().equals(userId))
|
||||
.flatMap(uomRepository::delete);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package com.kifi.api.service.invoice;
|
||||
|
||||
import com.kifi.api.entity.invoice.Invoice;
|
||||
import com.kifi.api.entity.invoice.InvoiceItem;
|
||||
import com.kifi.api.entity.invoice.InvoicePayment;
|
||||
import com.kifi.api.repository.business.BusinessFeatureRepository;
|
||||
import com.kifi.api.repository.invoice.InvoiceItemRepository;
|
||||
import com.kifi.api.repository.invoice.InvoicePaymentRepository;
|
||||
import com.kifi.api.repository.invoice.InvoiceRepository;
|
||||
import com.kifi.api.service.inventory.ProductService;
|
||||
import com.kifi.api.entity.inventory.InventoryMovement;
|
||||
import com.kifi.api.entity.inventory.InventoryMovementItem;
|
||||
import com.kifi.api.entity.Transaction;
|
||||
import com.kifi.api.repository.TransactionRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class InvoiceService {
|
||||
|
||||
private final InvoiceRepository invoiceRepository;
|
||||
private final InvoiceItemRepository invoiceItemRepository;
|
||||
private final InvoicePaymentRepository invoicePaymentRepository;
|
||||
private final BusinessFeatureRepository businessFeatureRepository;
|
||||
private final ProductService productService;
|
||||
private final com.kifi.api.service.TransactionService transactionService;
|
||||
|
||||
public Flux<Invoice> getInvoices(Long userId) {
|
||||
return invoiceRepository.findByUserId(userId)
|
||||
.flatMap(invoice -> Mono.zip(
|
||||
invoiceItemRepository.findByInvoiceId(invoice.getId()).collectList(),
|
||||
invoicePaymentRepository.findByInvoiceId(invoice.getId()).collectList()
|
||||
).map(tuple -> {
|
||||
invoice.setItems(tuple.getT1());
|
||||
invoice.setPayments(tuple.getT2());
|
||||
return invoice;
|
||||
}));
|
||||
}
|
||||
|
||||
public Mono<Invoice> getInvoiceById(Long id, Long userId) {
|
||||
return invoiceRepository.findById(id)
|
||||
.filter(inv -> inv.getUserId().equals(userId))
|
||||
.switchIfEmpty(Mono.error(new RuntimeException("Invoice not found or unauthorized")))
|
||||
.flatMap(invoice -> invoiceItemRepository.findByInvoiceId(invoice.getId())
|
||||
.collectList()
|
||||
.map(items -> {
|
||||
invoice.setItems(items);
|
||||
return invoice;
|
||||
}));
|
||||
}
|
||||
|
||||
public Mono<Invoice> createInvoice(Long userId, Invoice invoice) {
|
||||
invoice.setUserId(userId);
|
||||
invoice.setCreatedAt(LocalDateTime.now());
|
||||
invoice.setUpdatedAt(LocalDateTime.now());
|
||||
if (invoice.getStatus() == null || "DRAFT".equals(invoice.getStatus())) {
|
||||
java.math.BigDecimal amountPaid = invoice.getAmountPaid() != null ? invoice.getAmountPaid() : java.math.BigDecimal.ZERO;
|
||||
java.math.BigDecimal total = invoice.getTotalAmount() != null ? invoice.getTotalAmount() : java.math.BigDecimal.ZERO;
|
||||
|
||||
if (amountPaid.compareTo(total) >= 0 && total.compareTo(java.math.BigDecimal.ZERO) > 0) {
|
||||
invoice.setStatus("PAID");
|
||||
} else if (amountPaid.compareTo(java.math.BigDecimal.ZERO) > 0) {
|
||||
invoice.setStatus("PARTIAL");
|
||||
} else {
|
||||
invoice.setStatus("DRAFT");
|
||||
}
|
||||
}
|
||||
|
||||
return invoiceRepository.save(invoice)
|
||||
.flatMap(savedInvoice -> {
|
||||
Mono<Invoice> itemsMono = Mono.just(savedInvoice);
|
||||
if (invoice.getItems() != null && !invoice.getItems().isEmpty()) {
|
||||
itemsMono = Flux.fromIterable(invoice.getItems())
|
||||
.flatMap(item -> {
|
||||
item.setInvoiceId(savedInvoice.getId());
|
||||
return invoiceItemRepository.save(item);
|
||||
})
|
||||
.collectList()
|
||||
.map(savedItems -> {
|
||||
savedInvoice.setItems(savedItems);
|
||||
return savedInvoice;
|
||||
});
|
||||
}
|
||||
return itemsMono;
|
||||
})
|
||||
.flatMap(savedInvoice -> {
|
||||
if (!"DRAFT".equals(savedInvoice.getStatus())) {
|
||||
return processStockDeduction(savedInvoice, userId);
|
||||
}
|
||||
return Mono.just(savedInvoice);
|
||||
})
|
||||
.flatMap(savedInvoice -> {
|
||||
if (invoice.getAmountPaid() != null && invoice.getAmountPaid().compareTo(java.math.BigDecimal.ZERO) > 0) {
|
||||
InvoicePayment payment = InvoicePayment.builder()
|
||||
.invoiceId(savedInvoice.getId())
|
||||
.amount(invoice.getAmountPaid())
|
||||
.paymentDate(java.time.LocalDate.now())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.paymentMethod(invoice.getPaymentMethod() != null ? invoice.getPaymentMethod() : "Cash")
|
||||
.walletId(invoice.getPaymentWalletId())
|
||||
.build();
|
||||
return invoicePaymentRepository.save(payment).flatMap(savedPayment -> {
|
||||
if (savedPayment.getWalletId() != null) {
|
||||
Transaction transaction = Transaction.builder()
|
||||
.userId(userId)
|
||||
.toWalletId(savedPayment.getWalletId())
|
||||
.type("INCOME")
|
||||
.amount(savedPayment.getAmount())
|
||||
.date(savedPayment.getPaymentDate())
|
||||
.description("Initial payment for Invoice #" + savedInvoice.getInvoiceNumber())
|
||||
.notes(savedPayment.getPaymentMethod())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
return transactionService.addTransaction(userId, transaction).thenReturn(savedInvoice);
|
||||
}
|
||||
return Mono.just(savedInvoice);
|
||||
});
|
||||
}
|
||||
return Mono.just(savedInvoice);
|
||||
});
|
||||
}
|
||||
|
||||
public Mono<Invoice> processStockDeduction(Invoice invoice, Long userId) {
|
||||
return businessFeatureRepository.findByUserId(userId)
|
||||
.map(feature -> feature.getStockDeductionOnInvoice() != null ? feature.getStockDeductionOnInvoice() : true)
|
||||
.defaultIfEmpty(true)
|
||||
.flatMap(shouldDeduct -> {
|
||||
if (shouldDeduct && invoice.getItems() != null && !invoice.getItems().isEmpty()) {
|
||||
return Flux.fromIterable(invoice.getItems())
|
||||
.concatMap(item -> {
|
||||
if (item.getProductId() != null) {
|
||||
InventoryMovement movement = InventoryMovement.builder()
|
||||
.userId(userId)
|
||||
.type("REDUCTION")
|
||||
.notes("Sales Invoice " + invoice.getInvoiceNumber())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
InventoryMovementItem movementItem = new InventoryMovementItem();
|
||||
movementItem.setProductId(item.getProductId());
|
||||
movementItem.setQuantity(item.getQuantity());
|
||||
movement.setItems(java.util.Collections.singletonList(movementItem));
|
||||
|
||||
return productService.adjustStock(userId, item.getProductId(), movement);
|
||||
}
|
||||
return Mono.just(item);
|
||||
})
|
||||
.then(Mono.just(invoice));
|
||||
}
|
||||
return Mono.just(invoice);
|
||||
});
|
||||
}
|
||||
|
||||
public Mono<Invoice> finalizeInvoice(Long invoiceId, Long userId) {
|
||||
return getInvoiceById(invoiceId, userId)
|
||||
.flatMap(invoice -> {
|
||||
if (!"DRAFT".equals(invoice.getStatus())) {
|
||||
return Mono.error(new RuntimeException("Only DRAFT invoices can be finalized."));
|
||||
}
|
||||
|
||||
return processStockDeduction(invoice, userId)
|
||||
.flatMap(inv -> {
|
||||
inv.setStatus("FINALIZED");
|
||||
inv.setUpdatedAt(LocalDateTime.now());
|
||||
return invoiceRepository.save(inv);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public Mono<InvoicePayment> addPaymentToInvoice(Long invoiceId, Long userId, InvoicePayment payment) {
|
||||
return getInvoiceById(invoiceId, userId)
|
||||
.flatMap(invoice -> {
|
||||
String oldStatus = invoice.getStatus();
|
||||
payment.setInvoiceId(invoiceId);
|
||||
if (payment.getPaymentDate() == null) {
|
||||
payment.setPaymentDate(java.time.LocalDate.now());
|
||||
}
|
||||
payment.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
return invoicePaymentRepository.save(payment)
|
||||
.flatMap(savedPayment -> {
|
||||
// Update invoice amount_paid and status
|
||||
java.math.BigDecimal currentPaid = invoice.getAmountPaid() != null ? invoice.getAmountPaid() : java.math.BigDecimal.ZERO;
|
||||
java.math.BigDecimal newPaid = currentPaid.add(savedPayment.getAmount());
|
||||
invoice.setAmountPaid(newPaid);
|
||||
|
||||
java.math.BigDecimal total = invoice.getTotalAmount() != null ? invoice.getTotalAmount() : java.math.BigDecimal.ZERO;
|
||||
if (newPaid.compareTo(total) >= 0) {
|
||||
invoice.setStatus("PAID");
|
||||
} else if (newPaid.compareTo(java.math.BigDecimal.ZERO) > 0) {
|
||||
invoice.setStatus("PARTIAL");
|
||||
}
|
||||
return invoiceRepository.save(invoice)
|
||||
.flatMap(inv -> {
|
||||
Mono<Invoice> processMono = Mono.just(inv);
|
||||
if (("DRAFT".equals(oldStatus) || oldStatus == null) && !"DRAFT".equals(inv.getStatus())) {
|
||||
// Fetch items first if not present
|
||||
if (inv.getItems() == null || inv.getItems().isEmpty()) {
|
||||
processMono = invoiceItemRepository.findByInvoiceId(inv.getId()).collectList().flatMap(items -> {
|
||||
inv.setItems(items);
|
||||
return processStockDeduction(inv, userId);
|
||||
});
|
||||
} else {
|
||||
processMono = processStockDeduction(inv, userId);
|
||||
}
|
||||
}
|
||||
return processMono.flatMap(processedInv -> {
|
||||
if (savedPayment.getWalletId() != null) {
|
||||
Transaction transaction = Transaction.builder()
|
||||
.userId(userId)
|
||||
.toWalletId(savedPayment.getWalletId())
|
||||
.type("INCOME")
|
||||
.amount(savedPayment.getAmount())
|
||||
.date(savedPayment.getPaymentDate())
|
||||
.description("Payment for Invoice #" + invoice.getInvoiceNumber())
|
||||
.notes(savedPayment.getPaymentMethod())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
return transactionService.addTransaction(userId, transaction).thenReturn(savedPayment);
|
||||
}
|
||||
return Mono.just(savedPayment);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public Flux<InvoicePayment> getPaymentsForInvoice(Long invoiceId, Long userId) {
|
||||
return getInvoiceById(invoiceId, userId)
|
||||
.flatMapMany(invoice -> invoicePaymentRepository.findByInvoiceId(invoiceId));
|
||||
}
|
||||
}
|
||||
@@ -145,17 +145,48 @@ CREATE TABLE IF NOT EXISTS business_profiles (
|
||||
tax_number VARCHAR(100),
|
||||
currency VARCHAR(10) DEFAULT 'INR',
|
||||
tax_included_in_price BOOLEAN DEFAULT FALSE,
|
||||
address TEXT,
|
||||
state_id INTEGER,
|
||||
contact_person VARCHAR(100),
|
||||
contact_number VARCHAR(20),
|
||||
email_id VARCHAR(100),
|
||||
pan_number VARCHAR(20),
|
||||
gstin VARCHAR(20),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS indian_states (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
gst_code VARCHAR(2) NOT NULL
|
||||
);
|
||||
|
||||
-- Seed Indian States if empty
|
||||
INSERT INTO indian_states (name, gst_code) VALUES
|
||||
('Jammu & Kashmir', '01'), ('Himachal Pradesh', '02'), ('Punjab', '03'),
|
||||
('Chandigarh', '04'), ('Uttarakhand', '05'), ('Haryana', '06'),
|
||||
('Delhi', '07'), ('Rajasthan', '08'), ('Uttar Pradesh', '09'),
|
||||
('Bihar', '10'), ('Sikkim', '11'), ('Arunachal Pradesh', '12'),
|
||||
('Nagaland', '13'), ('Manipur', '14'), ('Mizoram', '15'),
|
||||
('Tripura', '16'), ('Meghalaya', '17'), ('Assam', '18'),
|
||||
('West Bengal', '19'), ('Jharkhand', '20'), ('Odisha', '21'),
|
||||
('Chhattisgarh', '22'), ('Madhya Pradesh', '23'), ('Gujarat', '24'),
|
||||
('Daman & Diu', '25'), ('Dadra & Nagar Haveli and Daman & Diu', '26'),
|
||||
('Maharashtra', '27'), ('Karnataka', '29'), ('Goa', '30'),
|
||||
('Lakshadweep', '31'), ('Kerala', '32'), ('Tamil Nadu', '33'),
|
||||
('Puducherry', '34'), ('Andaman & Nicobar Islands', '35'),
|
||||
('Telangana', '36'), ('Andhra Pradesh', '37'), ('Ladakh', '38')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
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,
|
||||
bom_reduction_strategy VARCHAR(50) DEFAULT 'COMPONENTS_ONLY',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id)
|
||||
);
|
||||
@@ -172,6 +203,16 @@ CREATE TABLE IF NOT EXISTS product_categories (
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS category_rate_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
category_id INTEGER REFERENCES product_categories(id) ON DELETE CASCADE,
|
||||
rate DECIMAL(15, 2) NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(category_id, date)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS units_of_measure (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
@@ -263,3 +304,63 @@ CREATE TABLE IF NOT EXISTS inventory_balances (
|
||||
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(product_id, location_id)
|
||||
);
|
||||
|
||||
-- SALES & INVOICES (Phase 2)
|
||||
ALTER TABLE business_features ADD COLUMN IF NOT EXISTS stock_deduction_on_invoice BOOLEAN DEFAULT TRUE;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS customers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255),
|
||||
phone VARCHAR(50),
|
||||
address TEXT,
|
||||
gstin VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS invoices (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
customer_id INTEGER REFERENCES customers(id),
|
||||
invoice_number VARCHAR(100) NOT NULL,
|
||||
issue_date DATE NOT NULL,
|
||||
due_date DATE,
|
||||
subtotal DECIMAL(15,2) NOT NULL,
|
||||
tax_total DECIMAL(15,2) DEFAULT 0.0,
|
||||
discount_total DECIMAL(15,2) DEFAULT 0.0,
|
||||
total_amount DECIMAL(15,2) NOT NULL,
|
||||
status VARCHAR(50) DEFAULT 'DRAFT', -- DRAFT, SENT, PAID, PARTIAL, OVERDUE, CANCELLED
|
||||
notes TEXT,
|
||||
|
||||
-- EMI tracking fields
|
||||
is_emi BOOLEAN DEFAULT FALSE,
|
||||
emi_amount DECIMAL(15,2),
|
||||
emi_cycle VARCHAR(20), -- MONTHLY, WEEKLY
|
||||
emi_start_date DATE,
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS invoice_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
invoice_id INTEGER REFERENCES invoices(id) ON DELETE CASCADE,
|
||||
product_id INTEGER REFERENCES products(id),
|
||||
description VARCHAR(255),
|
||||
quantity DECIMAL(10,3) NOT NULL,
|
||||
unit_price DECIMAL(15,2) NOT NULL,
|
||||
tax_rate DECIMAL(5,2) DEFAULT 0.0,
|
||||
discount DECIMAL(15,2) DEFAULT 0.0,
|
||||
total DECIMAL(15,2) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS invoice_payments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
invoice_id INTEGER REFERENCES invoices(id) ON DELETE CASCADE,
|
||||
transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE,
|
||||
amount DECIMAL(15,2) NOT NULL,
|
||||
payment_date DATE NOT NULL,
|
||||
emi_installment_number INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user