From 3a04990ec87701bbc201993da56f7a7f8d79531e Mon Sep 17 00:00:00 2001 From: Aditya Siregar Date: Fri, 12 Sep 2025 15:37:19 +0700 Subject: [PATCH] add product ingredients --- internal/app/app.go | 405 +++++++++--------- .../ingredient_unit_converter_contract.go | 8 + .../order_ingredient_transaction_contract.go | 20 + .../order_ingredient_transaction_request.go | 79 ++++ .../entities/order_ingredient_transaction.go | 48 +++ internal/entities/product_ingredient.go | 17 +- .../ingredient_unit_converter_handler.go | 22 + .../order_ingredient_transaction_handler.go | 201 +++++++++ .../order_ingredient_transaction_interface.go | 21 + internal/mappers/contract_mapper.go | 107 +++++ .../order_ingredient_transaction_mapper.go | 185 ++++++++ internal/mappers/product_ingredient_mapper.go | 42 +- internal/models/ingredient_unit_converter.go | 8 + .../models/order_ingredient_transaction.go | 108 +++++ internal/models/product_ingredient.go | 48 ++- internal/processor/account_processor.go | 1 - .../chart_of_account_type_processor.go | 1 - internal/processor/ingredient_repository.go | 16 - .../ingredient_unit_converter_processor.go | 62 +++ .../order_ingredient_transaction_processor.go | 393 +++++++++++++++++ internal/processor/repository_interfaces.go | 40 ++ internal/processor/unit_repository.go | 15 - ...order_ingredient_transaction_repository.go | 234 ++++++++++ internal/router/router.go | 132 +++--- .../ingredient_unit_converter_service.go | 12 + .../order_ingredient_transaction_service.go | 205 +++++++++ internal/service/order_service.go | 214 ++++++++- .../ingredient_unit_converter_transformer.go | 24 ++ internal/util/accounting_util.go | 7 +- internal/util/waste_util.go | 87 ++++ .../order_ingredient_transaction_validator.go | 120 ++++++ ...percentage_to_product_ingredients.down.sql | 2 + ...e_percentage_to_product_ingredients.up.sql | 6 + ...er_ingredients_transactions_table.down.sql | 2 + ...rder_ingredients_transactions_table.up.sql | 37 ++ 35 files changed, 2572 insertions(+), 357 deletions(-) create mode 100644 internal/contract/order_ingredient_transaction_contract.go create mode 100644 internal/contract/order_ingredient_transaction_request.go create mode 100644 internal/entities/order_ingredient_transaction.go create mode 100644 internal/handler/order_ingredient_transaction_handler.go create mode 100644 internal/handler/order_ingredient_transaction_interface.go create mode 100644 internal/mappers/order_ingredient_transaction_mapper.go create mode 100644 internal/models/order_ingredient_transaction.go create mode 100644 internal/processor/order_ingredient_transaction_processor.go create mode 100644 internal/repository/order_ingredient_transaction_repository.go create mode 100644 internal/service/order_ingredient_transaction_service.go create mode 100644 internal/util/waste_util.go create mode 100644 internal/validator/order_ingredient_transaction_validator.go create mode 100644 migrations/000045_add_waste_percentage_to_product_ingredients.down.sql create mode 100644 migrations/000045_add_waste_percentage_to_product_ingredients.up.sql create mode 100644 migrations/000046_create_order_ingredients_transactions_table.down.sql create mode 100644 migrations/000046_create_order_ingredients_transactions_table.up.sql diff --git a/internal/app/app.go b/internal/app/app.go index 5b65de8..451fa5f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -92,6 +92,8 @@ func (a *App) Initialize(cfg *config.Config) error { validators.chartOfAccountValidator, services.accountService, validators.accountValidator, + *services.orderIngredientTransactionService, + validators.orderIngredientTransactionValidator, ) return nil @@ -137,95 +139,103 @@ func (a *App) Shutdown() { } type repositories struct { - userRepo *repository.UserRepositoryImpl - organizationRepo *repository.OrganizationRepositoryImpl - outletRepo *repository.OutletRepositoryImpl - outletSettingRepo *repository.OutletSettingRepositoryImpl - categoryRepo *repository.CategoryRepositoryImpl - productRepo *repository.ProductRepositoryImpl - productVariantRepo *repository.ProductVariantRepositoryImpl - inventoryRepo *repository.InventoryRepositoryImpl - inventoryMovementRepo *repository.InventoryMovementRepositoryImpl - orderRepo *repository.OrderRepositoryImpl - orderItemRepo *repository.OrderItemRepositoryImpl - paymentRepo *repository.PaymentRepositoryImpl - paymentOrderItemRepo *repository.PaymentOrderItemRepositoryImpl - paymentMethodRepo *repository.PaymentMethodRepositoryImpl - fileRepo *repository.FileRepositoryImpl - customerRepo *repository.CustomerRepository - analyticsRepo *repository.AnalyticsRepositoryImpl - tableRepo *repository.TableRepository - unitRepo *repository.UnitRepository - ingredientRepo *repository.IngredientRepository - productRecipeRepo *repository.ProductRecipeRepository - vendorRepo *repository.VendorRepositoryImpl - purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl - unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl - chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl - chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl - accountRepo *repository.AccountRepositoryImpl - txManager *repository.TxManager + userRepo *repository.UserRepositoryImpl + organizationRepo *repository.OrganizationRepositoryImpl + outletRepo *repository.OutletRepositoryImpl + outletSettingRepo *repository.OutletSettingRepositoryImpl + categoryRepo *repository.CategoryRepositoryImpl + productRepo *repository.ProductRepositoryImpl + productVariantRepo *repository.ProductVariantRepositoryImpl + inventoryRepo *repository.InventoryRepositoryImpl + inventoryMovementRepo *repository.InventoryMovementRepositoryImpl + orderRepo *repository.OrderRepositoryImpl + orderItemRepo *repository.OrderItemRepositoryImpl + paymentRepo *repository.PaymentRepositoryImpl + paymentOrderItemRepo *repository.PaymentOrderItemRepositoryImpl + paymentMethodRepo *repository.PaymentMethodRepositoryImpl + fileRepo *repository.FileRepositoryImpl + customerRepo *repository.CustomerRepository + analyticsRepo *repository.AnalyticsRepositoryImpl + tableRepo *repository.TableRepository + unitRepo *repository.UnitRepository + ingredientRepo *repository.IngredientRepository + productRecipeRepo *repository.ProductRecipeRepository + vendorRepo *repository.VendorRepositoryImpl + purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl + unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl + chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl + chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl + accountRepo *repository.AccountRepositoryImpl + orderIngredientTransactionRepo *repository.OrderIngredientTransactionRepositoryImpl + productIngredientRepo *repository.ProductIngredientRepository + txManager *repository.TxManager } func (a *App) initRepositories() *repositories { return &repositories{ - userRepo: repository.NewUserRepository(a.db), - organizationRepo: repository.NewOrganizationRepositoryImpl(a.db), - outletRepo: repository.NewOutletRepositoryImpl(a.db), - outletSettingRepo: repository.NewOutletSettingRepositoryImpl(a.db), - categoryRepo: repository.NewCategoryRepositoryImpl(a.db), - productRepo: repository.NewProductRepositoryImpl(a.db), - productVariantRepo: repository.NewProductVariantRepositoryImpl(a.db), - inventoryRepo: repository.NewInventoryRepositoryImpl(a.db), - inventoryMovementRepo: repository.NewInventoryMovementRepositoryImpl(a.db), - orderRepo: repository.NewOrderRepositoryImpl(a.db), - orderItemRepo: repository.NewOrderItemRepositoryImpl(a.db), - paymentRepo: repository.NewPaymentRepositoryImpl(a.db), - paymentOrderItemRepo: repository.NewPaymentOrderItemRepositoryImpl(a.db), - paymentMethodRepo: repository.NewPaymentMethodRepositoryImpl(a.db), - fileRepo: repository.NewFileRepositoryImpl(a.db), - customerRepo: repository.NewCustomerRepository(a.db), - analyticsRepo: repository.NewAnalyticsRepositoryImpl(a.db), - tableRepo: repository.NewTableRepository(a.db), - unitRepo: repository.NewUnitRepository(a.db), - ingredientRepo: repository.NewIngredientRepository(a.db), - productRecipeRepo: repository.NewProductRecipeRepository(a.db), - vendorRepo: repository.NewVendorRepositoryImpl(a.db), - purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db), - unitConverterRepo: repository.NewIngredientUnitConverterRepositoryImpl(a.db).(*repository.IngredientUnitConverterRepositoryImpl), - chartOfAccountTypeRepo: repository.NewChartOfAccountTypeRepositoryImpl(a.db), - chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db), - accountRepo: repository.NewAccountRepositoryImpl(a.db), - txManager: repository.NewTxManager(a.db), + userRepo: repository.NewUserRepository(a.db), + organizationRepo: repository.NewOrganizationRepositoryImpl(a.db), + outletRepo: repository.NewOutletRepositoryImpl(a.db), + outletSettingRepo: repository.NewOutletSettingRepositoryImpl(a.db), + categoryRepo: repository.NewCategoryRepositoryImpl(a.db), + productRepo: repository.NewProductRepositoryImpl(a.db), + productVariantRepo: repository.NewProductVariantRepositoryImpl(a.db), + inventoryRepo: repository.NewInventoryRepositoryImpl(a.db), + inventoryMovementRepo: repository.NewInventoryMovementRepositoryImpl(a.db), + orderRepo: repository.NewOrderRepositoryImpl(a.db), + orderItemRepo: repository.NewOrderItemRepositoryImpl(a.db), + paymentRepo: repository.NewPaymentRepositoryImpl(a.db), + paymentOrderItemRepo: repository.NewPaymentOrderItemRepositoryImpl(a.db), + paymentMethodRepo: repository.NewPaymentMethodRepositoryImpl(a.db), + fileRepo: repository.NewFileRepositoryImpl(a.db), + customerRepo: repository.NewCustomerRepository(a.db), + analyticsRepo: repository.NewAnalyticsRepositoryImpl(a.db), + tableRepo: repository.NewTableRepository(a.db), + unitRepo: repository.NewUnitRepository(a.db), + ingredientRepo: repository.NewIngredientRepository(a.db), + productRecipeRepo: repository.NewProductRecipeRepository(a.db), + vendorRepo: repository.NewVendorRepositoryImpl(a.db), + purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db), + unitConverterRepo: repository.NewIngredientUnitConverterRepositoryImpl(a.db).(*repository.IngredientUnitConverterRepositoryImpl), + chartOfAccountTypeRepo: repository.NewChartOfAccountTypeRepositoryImpl(a.db), + chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db), + accountRepo: repository.NewAccountRepositoryImpl(a.db), + orderIngredientTransactionRepo: repository.NewOrderIngredientTransactionRepositoryImpl(a.db).(*repository.OrderIngredientTransactionRepositoryImpl), + productIngredientRepo: func() *repository.ProductIngredientRepository { + db, _ := a.db.DB() + return repository.NewProductIngredientRepository(db) + }(), + txManager: repository.NewTxManager(a.db), } } type processors struct { - userProcessor *processor.UserProcessorImpl - organizationProcessor processor.OrganizationProcessor - outletProcessor processor.OutletProcessor - outletSettingProcessor *processor.OutletSettingProcessorImpl - categoryProcessor processor.CategoryProcessor - productProcessor processor.ProductProcessor - productVariantProcessor processor.ProductVariantProcessor - inventoryProcessor processor.InventoryProcessor - orderProcessor processor.OrderProcessor - paymentMethodProcessor processor.PaymentMethodProcessor - fileProcessor processor.FileProcessor - customerProcessor *processor.CustomerProcessor - analyticsProcessor *processor.AnalyticsProcessorImpl - tableProcessor *processor.TableProcessor - unitProcessor *processor.UnitProcessorImpl - ingredientProcessor *processor.IngredientProcessorImpl - productRecipeProcessor *processor.ProductRecipeProcessorImpl - vendorProcessor *processor.VendorProcessorImpl - purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl - unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl - chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl - chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl - accountProcessor *processor.AccountProcessorImpl - fileClient processor.FileClient - inventoryMovementService service.InventoryMovementService + userProcessor *processor.UserProcessorImpl + organizationProcessor processor.OrganizationProcessor + outletProcessor processor.OutletProcessor + outletSettingProcessor *processor.OutletSettingProcessorImpl + categoryProcessor processor.CategoryProcessor + productProcessor processor.ProductProcessor + productVariantProcessor processor.ProductVariantProcessor + inventoryProcessor processor.InventoryProcessor + orderProcessor processor.OrderProcessor + paymentMethodProcessor processor.PaymentMethodProcessor + fileProcessor processor.FileProcessor + customerProcessor *processor.CustomerProcessor + analyticsProcessor *processor.AnalyticsProcessorImpl + tableProcessor *processor.TableProcessor + unitProcessor *processor.UnitProcessorImpl + ingredientProcessor *processor.IngredientProcessorImpl + productRecipeProcessor *processor.ProductRecipeProcessorImpl + vendorProcessor *processor.VendorProcessorImpl + purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl + unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl + chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl + chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl + accountProcessor *processor.AccountProcessorImpl + orderIngredientTransactionProcessor *processor.OrderIngredientTransactionProcessorImpl + fileClient processor.FileClient + inventoryMovementService service.InventoryMovementService } func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors { @@ -233,60 +243,62 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor inventoryMovementService := service.NewInventoryMovementService(repos.inventoryMovementRepo, repos.ingredientRepo) return &processors{ - userProcessor: processor.NewUserProcessor(repos.userRepo, repos.organizationRepo, repos.outletRepo), - organizationProcessor: processor.NewOrganizationProcessorImpl(repos.organizationRepo, repos.outletRepo, repos.userRepo), - outletProcessor: processor.NewOutletProcessorImpl(repos.outletRepo), - outletSettingProcessor: processor.NewOutletSettingProcessorImpl(repos.outletSettingRepo, repos.outletRepo), - categoryProcessor: processor.NewCategoryProcessorImpl(repos.categoryRepo), - productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo), - productVariantProcessor: processor.NewProductVariantProcessorImpl(repos.productVariantRepo, repos.productRepo), - inventoryProcessor: processor.NewInventoryProcessorImpl(repos.inventoryRepo, repos.productRepo, repos.outletRepo, repos.ingredientRepo, repos.inventoryMovementRepo), - orderProcessor: processor.NewOrderProcessorImpl(repos.orderRepo, repos.orderItemRepo, repos.paymentRepo, repos.paymentOrderItemRepo, repos.productRepo, repos.paymentMethodRepo, repos.inventoryRepo, repos.inventoryMovementRepo, repos.productVariantRepo, repos.outletRepo, repos.customerRepo, repos.txManager, repos.productRecipeRepo, repos.ingredientRepo, inventoryMovementService), - paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo), - fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient), - customerProcessor: processor.NewCustomerProcessor(repos.customerRepo), - analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo), - tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo), - unitProcessor: processor.NewUnitProcessor(repos.unitRepo), - ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo), - productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo), - vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo), - purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo), - unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo), - chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo), - chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo), - accountProcessor: processor.NewAccountProcessorImpl(repos.accountRepo, repos.chartOfAccountRepo), - fileClient: fileClient, - inventoryMovementService: inventoryMovementService, + userProcessor: processor.NewUserProcessor(repos.userRepo, repos.organizationRepo, repos.outletRepo), + organizationProcessor: processor.NewOrganizationProcessorImpl(repos.organizationRepo, repos.outletRepo, repos.userRepo), + outletProcessor: processor.NewOutletProcessorImpl(repos.outletRepo), + outletSettingProcessor: processor.NewOutletSettingProcessorImpl(repos.outletSettingRepo, repos.outletRepo), + categoryProcessor: processor.NewCategoryProcessorImpl(repos.categoryRepo), + productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo), + productVariantProcessor: processor.NewProductVariantProcessorImpl(repos.productVariantRepo, repos.productRepo), + inventoryProcessor: processor.NewInventoryProcessorImpl(repos.inventoryRepo, repos.productRepo, repos.outletRepo, repos.ingredientRepo, repos.inventoryMovementRepo), + orderProcessor: processor.NewOrderProcessorImpl(repos.orderRepo, repos.orderItemRepo, repos.paymentRepo, repos.paymentOrderItemRepo, repos.productRepo, repos.paymentMethodRepo, repos.inventoryRepo, repos.inventoryMovementRepo, repos.productVariantRepo, repos.outletRepo, repos.customerRepo, repos.txManager, repos.productRecipeRepo, repos.ingredientRepo, inventoryMovementService), + paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo), + fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient), + customerProcessor: processor.NewCustomerProcessor(repos.customerRepo), + analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo), + tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo), + unitProcessor: processor.NewUnitProcessor(repos.unitRepo), + ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo), + productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo), + vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo), + purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo), + unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo), + chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo), + chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo), + accountProcessor: processor.NewAccountProcessorImpl(repos.accountRepo, repos.chartOfAccountRepo), + orderIngredientTransactionProcessor: processor.NewOrderIngredientTransactionProcessorImpl(repos.orderIngredientTransactionRepo, repos.productIngredientRepo, repos.ingredientRepo, repos.unitRepo).(*processor.OrderIngredientTransactionProcessorImpl), + fileClient: fileClient, + inventoryMovementService: inventoryMovementService, } } type services struct { - userService *service.UserServiceImpl - authService service.AuthService - organizationService service.OrganizationService - outletService service.OutletService - outletSettingService service.OutletSettingService - categoryService service.CategoryService - productService service.ProductService - productVariantService service.ProductVariantService - inventoryService service.InventoryService - orderService service.OrderService - paymentMethodService service.PaymentMethodService - fileService service.FileService - customerService service.CustomerService - analyticsService *service.AnalyticsServiceImpl - reportService service.ReportService - tableService *service.TableServiceImpl - unitService *service.UnitServiceImpl - ingredientService *service.IngredientServiceImpl - productRecipeService *service.ProductRecipeServiceImpl - vendorService *service.VendorServiceImpl - purchaseOrderService *service.PurchaseOrderServiceImpl - unitConverterService *service.IngredientUnitConverterServiceImpl - chartOfAccountTypeService service.ChartOfAccountTypeService - chartOfAccountService service.ChartOfAccountService - accountService service.AccountService + userService *service.UserServiceImpl + authService service.AuthService + organizationService service.OrganizationService + outletService service.OutletService + outletSettingService service.OutletSettingService + categoryService service.CategoryService + productService service.ProductService + productVariantService service.ProductVariantService + inventoryService service.InventoryService + orderService service.OrderService + paymentMethodService service.PaymentMethodService + fileService service.FileService + customerService service.CustomerService + analyticsService *service.AnalyticsServiceImpl + reportService service.ReportService + tableService *service.TableServiceImpl + unitService *service.UnitServiceImpl + ingredientService *service.IngredientServiceImpl + productRecipeService *service.ProductRecipeServiceImpl + vendorService *service.VendorServiceImpl + purchaseOrderService *service.PurchaseOrderServiceImpl + unitConverterService *service.IngredientUnitConverterServiceImpl + chartOfAccountTypeService service.ChartOfAccountTypeService + chartOfAccountService service.ChartOfAccountService + accountService service.AccountService + orderIngredientTransactionService *service.OrderIngredientTransactionService } func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services { @@ -300,7 +312,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con productService := service.NewProductService(processors.productProcessor) productVariantService := service.NewProductVariantService(processors.productVariantProcessor) inventoryService := service.NewInventoryService(processors.inventoryProcessor) - orderService := service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo) + orderService := service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, nil, processors.orderIngredientTransactionProcessor, *repos.productIngredientRepo, repos.txManager) // Will be updated after orderIngredientTransactionService is created paymentMethodService := service.NewPaymentMethodService(processors.paymentMethodProcessor) fileService := service.NewFileServiceImpl(processors.fileProcessor) var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor) @@ -316,33 +328,38 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con chartOfAccountTypeService := service.NewChartOfAccountTypeService(processors.chartOfAccountTypeProcessor) chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor) accountService := service.NewAccountService(processors.accountProcessor) + orderIngredientTransactionService := service.NewOrderIngredientTransactionService(processors.orderIngredientTransactionProcessor, repos.txManager) + + // Update order service with order ingredient transaction service + orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productIngredientRepo, repos.txManager) return &services{ - userService: service.NewUserService(processors.userProcessor), - authService: authService, - organizationService: organizationService, - outletService: outletService, - outletSettingService: outletSettingService, - categoryService: categoryService, - productService: productService, - productVariantService: productVariantService, - inventoryService: inventoryService, - orderService: orderService, - paymentMethodService: paymentMethodService, - fileService: fileService, - customerService: customerService, - analyticsService: analyticsService, - reportService: reportService, - tableService: tableService, - unitService: unitService, - ingredientService: ingredientService, - productRecipeService: productRecipeService, - vendorService: vendorService, - purchaseOrderService: purchaseOrderService, - unitConverterService: unitConverterService, - chartOfAccountTypeService: chartOfAccountTypeService, - chartOfAccountService: chartOfAccountService, - accountService: accountService, + userService: service.NewUserService(processors.userProcessor), + authService: authService, + organizationService: organizationService, + outletService: outletService, + outletSettingService: outletSettingService, + categoryService: categoryService, + productService: productService, + productVariantService: productVariantService, + inventoryService: inventoryService, + orderService: orderService, + paymentMethodService: paymentMethodService, + fileService: fileService, + customerService: customerService, + analyticsService: analyticsService, + reportService: reportService, + tableService: tableService, + unitService: unitService, + ingredientService: ingredientService, + productRecipeService: productRecipeService, + vendorService: vendorService, + purchaseOrderService: purchaseOrderService, + unitConverterService: unitConverterService, + chartOfAccountTypeService: chartOfAccountTypeService, + chartOfAccountService: chartOfAccountService, + accountService: accountService, + orderIngredientTransactionService: orderIngredientTransactionService, } } @@ -357,45 +374,47 @@ func (a *App) initMiddleware(services *services) *middlewares { } type validators struct { - userValidator *validator.UserValidatorImpl - organizationValidator validator.OrganizationValidator - outletValidator validator.OutletValidator - categoryValidator validator.CategoryValidator - productValidator validator.ProductValidator - productVariantValidator validator.ProductVariantValidator - inventoryValidator validator.InventoryValidator - orderValidator validator.OrderValidator - paymentMethodValidator validator.PaymentMethodValidator - fileValidator validator.FileValidator - customerValidator validator.CustomerValidator - tableValidator *validator.TableValidator - vendorValidator *validator.VendorValidatorImpl - purchaseOrderValidator *validator.PurchaseOrderValidatorImpl - unitConverterValidator *validator.IngredientUnitConverterValidatorImpl - chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl - chartOfAccountValidator *validator.ChartOfAccountValidatorImpl - accountValidator *validator.AccountValidatorImpl + userValidator *validator.UserValidatorImpl + organizationValidator validator.OrganizationValidator + outletValidator validator.OutletValidator + categoryValidator validator.CategoryValidator + productValidator validator.ProductValidator + productVariantValidator validator.ProductVariantValidator + inventoryValidator validator.InventoryValidator + orderValidator validator.OrderValidator + paymentMethodValidator validator.PaymentMethodValidator + fileValidator validator.FileValidator + customerValidator validator.CustomerValidator + tableValidator *validator.TableValidator + vendorValidator *validator.VendorValidatorImpl + purchaseOrderValidator *validator.PurchaseOrderValidatorImpl + unitConverterValidator *validator.IngredientUnitConverterValidatorImpl + chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl + chartOfAccountValidator *validator.ChartOfAccountValidatorImpl + accountValidator *validator.AccountValidatorImpl + orderIngredientTransactionValidator *validator.OrderIngredientTransactionValidatorImpl } func (a *App) initValidators() *validators { return &validators{ - userValidator: validator.NewUserValidator(), - organizationValidator: validator.NewOrganizationValidator(), - outletValidator: validator.NewOutletValidator(), - categoryValidator: validator.NewCategoryValidator(), - productValidator: validator.NewProductValidator(), - productVariantValidator: validator.NewProductVariantValidator(), - inventoryValidator: validator.NewInventoryValidator(), - orderValidator: validator.NewOrderValidator(), - paymentMethodValidator: validator.NewPaymentMethodValidator(), - fileValidator: validator.NewFileValidatorImpl(), - customerValidator: validator.NewCustomerValidator(), - tableValidator: validator.NewTableValidator(), - vendorValidator: validator.NewVendorValidator(), - purchaseOrderValidator: validator.NewPurchaseOrderValidator(), - unitConverterValidator: validator.NewIngredientUnitConverterValidator().(*validator.IngredientUnitConverterValidatorImpl), - chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl), - chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl), - accountValidator: validator.NewAccountValidator().(*validator.AccountValidatorImpl), + userValidator: validator.NewUserValidator(), + organizationValidator: validator.NewOrganizationValidator(), + outletValidator: validator.NewOutletValidator(), + categoryValidator: validator.NewCategoryValidator(), + productValidator: validator.NewProductValidator(), + productVariantValidator: validator.NewProductVariantValidator(), + inventoryValidator: validator.NewInventoryValidator(), + orderValidator: validator.NewOrderValidator(), + paymentMethodValidator: validator.NewPaymentMethodValidator(), + fileValidator: validator.NewFileValidatorImpl(), + customerValidator: validator.NewCustomerValidator(), + tableValidator: validator.NewTableValidator(), + vendorValidator: validator.NewVendorValidator(), + purchaseOrderValidator: validator.NewPurchaseOrderValidator(), + unitConverterValidator: validator.NewIngredientUnitConverterValidator().(*validator.IngredientUnitConverterValidatorImpl), + chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl), + chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl), + accountValidator: validator.NewAccountValidator().(*validator.AccountValidatorImpl), + orderIngredientTransactionValidator: validator.NewOrderIngredientTransactionValidator().(*validator.OrderIngredientTransactionValidatorImpl), } } diff --git a/internal/contract/ingredient_unit_converter_contract.go b/internal/contract/ingredient_unit_converter_contract.go index 4d61dad..de37f8a 100644 --- a/internal/contract/ingredient_unit_converter_contract.go +++ b/internal/contract/ingredient_unit_converter_contract.go @@ -74,3 +74,11 @@ type ListIngredientUnitConvertersResponse struct { TotalPages int `json:"total_pages"` } +type IngredientUnitsResponse struct { + IngredientID uuid.UUID `json:"ingredient_id"` + IngredientName string `json:"ingredient_name"` + BaseUnitID uuid.UUID `json:"base_unit_id"` + BaseUnitName string `json:"base_unit_name"` + Units []*UnitResponse `json:"units"` +} + diff --git a/internal/contract/order_ingredient_transaction_contract.go b/internal/contract/order_ingredient_transaction_contract.go new file mode 100644 index 0000000..349cc52 --- /dev/null +++ b/internal/contract/order_ingredient_transaction_contract.go @@ -0,0 +1,20 @@ +package contract + +import ( + "context" + + "github.com/google/uuid" +) + +type OrderIngredientTransactionContract interface { + CreateOrderIngredientTransaction(ctx context.Context, req *CreateOrderIngredientTransactionRequest) (*OrderIngredientTransactionResponse, error) + GetOrderIngredientTransactionByID(ctx context.Context, id uuid.UUID) (*OrderIngredientTransactionResponse, error) + UpdateOrderIngredientTransaction(ctx context.Context, id uuid.UUID, req *UpdateOrderIngredientTransactionRequest) (*OrderIngredientTransactionResponse, error) + DeleteOrderIngredientTransaction(ctx context.Context, id uuid.UUID) error + ListOrderIngredientTransactions(ctx context.Context, req *ListOrderIngredientTransactionsRequest) ([]*OrderIngredientTransactionResponse, int64, error) + GetOrderIngredientTransactionsByOrder(ctx context.Context, orderID uuid.UUID) ([]*OrderIngredientTransactionResponse, error) + GetOrderIngredientTransactionsByOrderItem(ctx context.Context, orderItemID uuid.UUID) ([]*OrderIngredientTransactionResponse, error) + GetOrderIngredientTransactionsByIngredient(ctx context.Context, ingredientID uuid.UUID) ([]*OrderIngredientTransactionResponse, error) + GetOrderIngredientTransactionSummary(ctx context.Context, req *ListOrderIngredientTransactionsRequest) ([]*OrderIngredientTransactionSummary, error) + BulkCreateOrderIngredientTransactions(ctx context.Context, transactions []*CreateOrderIngredientTransactionRequest) ([]*OrderIngredientTransactionResponse, error) +} diff --git a/internal/contract/order_ingredient_transaction_request.go b/internal/contract/order_ingredient_transaction_request.go new file mode 100644 index 0000000..3deee2f --- /dev/null +++ b/internal/contract/order_ingredient_transaction_request.go @@ -0,0 +1,79 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateOrderIngredientTransactionRequest struct { + OrderID uuid.UUID `json:"order_id" validate:"required"` + OrderItemID *uuid.UUID `json:"order_item_id,omitempty"` + ProductID uuid.UUID `json:"product_id" validate:"required"` + ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"` + IngredientID uuid.UUID `json:"ingredient_id" validate:"required"` + GrossQty float64 `json:"gross_qty" validate:"required,gt=0"` + NetQty float64 `json:"net_qty" validate:"required,gt=0"` + WasteQty float64 `json:"waste_qty" validate:"min=0"` + Unit string `json:"unit" validate:"required,max=50"` + TransactionDate *time.Time `json:"transaction_date,omitempty"` +} + +type UpdateOrderIngredientTransactionRequest struct { + GrossQty *float64 `json:"gross_qty,omitempty" validate:"omitempty,gt=0"` + NetQty *float64 `json:"net_qty,omitempty" validate:"omitempty,gt=0"` + WasteQty *float64 `json:"waste_qty,omitempty" validate:"min=0"` + Unit *string `json:"unit,omitempty" validate:"omitempty,max=50"` + TransactionDate *time.Time `json:"transaction_date,omitempty"` +} + +type OrderIngredientTransactionResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` + OrderID uuid.UUID `json:"order_id"` + OrderItemID *uuid.UUID `json:"order_item_id"` + ProductID uuid.UUID `json:"product_id"` + ProductVariantID *uuid.UUID `json:"product_variant_id"` + IngredientID uuid.UUID `json:"ingredient_id"` + GrossQty float64 `json:"gross_qty"` + NetQty float64 `json:"net_qty"` + WasteQty float64 `json:"waste_qty"` + Unit string `json:"unit"` + TransactionDate time.Time `json:"transaction_date"` + CreatedBy uuid.UUID `json:"created_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + // Relations - these would be populated by the service layer + Organization interface{} `json:"organization,omitempty"` + Outlet interface{} `json:"outlet,omitempty"` + Order interface{} `json:"order,omitempty"` + OrderItem interface{} `json:"order_item,omitempty"` + Product interface{} `json:"product,omitempty"` + ProductVariant interface{} `json:"product_variant,omitempty"` + Ingredient interface{} `json:"ingredient,omitempty"` + CreatedByUser interface{} `json:"created_by_user,omitempty"` +} + +type ListOrderIngredientTransactionsRequest struct { + OrderID *uuid.UUID `json:"order_id,omitempty"` + OrderItemID *uuid.UUID `json:"order_item_id,omitempty"` + ProductID *uuid.UUID `json:"product_id,omitempty"` + ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"` + IngredientID *uuid.UUID `json:"ingredient_id,omitempty"` + StartDate *time.Time `json:"start_date,omitempty"` + EndDate *time.Time `json:"end_date,omitempty"` + Page int `json:"page" validate:"min=1"` + Limit int `json:"limit" validate:"min=1,max=100"` +} + +type OrderIngredientTransactionSummary struct { + IngredientID uuid.UUID `json:"ingredient_id"` + IngredientName string `json:"ingredient_name"` + TotalGrossQty float64 `json:"total_gross_qty"` + TotalNetQty float64 `json:"total_net_qty"` + TotalWasteQty float64 `json:"total_waste_qty"` + WastePercentage float64 `json:"waste_percentage"` + Unit string `json:"unit"` +} diff --git a/internal/entities/order_ingredient_transaction.go b/internal/entities/order_ingredient_transaction.go new file mode 100644 index 0000000..38b9917 --- /dev/null +++ b/internal/entities/order_ingredient_transaction.go @@ -0,0 +1,48 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type OrderIngredientTransaction struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"` + OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"` + OrderID uuid.UUID `gorm:"type:uuid;not null;index" json:"order_id" validate:"required"` + OrderItemID *uuid.UUID `gorm:"type:uuid;index" json:"order_item_id"` + ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id" validate:"required"` + ProductVariantID *uuid.UUID `gorm:"type:uuid;index" json:"product_variant_id"` + IngredientID uuid.UUID `gorm:"type:uuid;not null;index" json:"ingredient_id" validate:"required"` + GrossQty float64 `gorm:"type:decimal(12,3);not null" json:"gross_qty" validate:"required,gt=0"` + NetQty float64 `gorm:"type:decimal(12,3);not null" json:"net_qty" validate:"required,gt=0"` + WasteQty float64 `gorm:"type:decimal(12,3);not null" json:"waste_qty" validate:"min=0"` + Unit string `gorm:"size:50;not null" json:"unit" validate:"required,max=50"` + TransactionDate time.Time `gorm:"not null;index" json:"transaction_date"` + CreatedBy uuid.UUID `gorm:"type:uuid;not null;index" json:"created_by" validate:"required"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + // Relations + Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` + Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"` + Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"` + OrderItem *OrderItem `gorm:"foreignKey:OrderItemID" json:"order_item,omitempty"` + Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"` + ProductVariant *ProductVariant `gorm:"foreignKey:ProductVariantID" json:"product_variant,omitempty"` + Ingredient Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"` + CreatedByUser User `gorm:"foreignKey:CreatedBy" json:"created_by_user,omitempty"` +} + +func (oit *OrderIngredientTransaction) BeforeCreate(tx *gorm.DB) error { + if oit.ID == uuid.Nil { + oit.ID = uuid.New() + } + return nil +} + +func (OrderIngredientTransaction) TableName() string { + return "order_ingredients_transactions" +} diff --git a/internal/entities/product_ingredient.go b/internal/entities/product_ingredient.go index 4a31447..5d9bb46 100644 --- a/internal/entities/product_ingredient.go +++ b/internal/entities/product_ingredient.go @@ -7,14 +7,15 @@ import ( ) type ProductIngredient struct { - ID uuid.UUID `json:"id" db:"id"` - OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"` - OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"` - ProductID uuid.UUID `json:"product_id" db:"product_id"` - IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"` - Quantity float64 `json:"quantity" db:"quantity"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + ID uuid.UUID `json:"id" db:"id"` + OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"` + ProductID uuid.UUID `json:"product_id" db:"product_id"` + IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"` + Quantity float64 `json:"quantity" db:"quantity"` + WastePercentage float64 `json:"waste_percentage" db:"waste_percentage"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` // Relations Product *Product `json:"product,omitempty"` diff --git a/internal/handler/ingredient_unit_converter_handler.go b/internal/handler/ingredient_unit_converter_handler.go index dc616c5..e78346b 100644 --- a/internal/handler/ingredient_unit_converter_handler.go +++ b/internal/handler/ingredient_unit_converter_handler.go @@ -254,3 +254,25 @@ func (h *IngredientUnitConverterHandler) ConvertUnit(c *gin.Context) { util.HandleResponse(c.Writer, c.Request, converterResponse, "IngredientUnitConverterHandler::ConvertUnit") } +func (h *IngredientUnitConverterHandler) GetUnitsByIngredientID(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + ingredientIDStr := c.Param("ingredient_id") + ingredientID, err := uuid.Parse(ingredientIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("IngredientUnitConverterHandler::GetUnitsByIngredientID -> Invalid ingredient ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientUnitConverterHandler::GetUnitsByIngredientID") + return + } + + unitsResponse := h.converterService.GetUnitsByIngredientID(ctx, contextInfo, ingredientID) + if unitsResponse.HasErrors() { + errorResp := unitsResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("IngredientUnitConverterHandler::GetUnitsByIngredientID -> Failed to get units for ingredient from service") + } + + util.HandleResponse(c.Writer, c.Request, unitsResponse, "IngredientUnitConverterHandler::GetUnitsByIngredientID") +} + diff --git a/internal/handler/order_ingredient_transaction_handler.go b/internal/handler/order_ingredient_transaction_handler.go new file mode 100644 index 0000000..1100fb6 --- /dev/null +++ b/internal/handler/order_ingredient_transaction_handler.go @@ -0,0 +1,201 @@ +package handler + +import ( + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/util" + "apskel-pos-be/internal/validator" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type OrderIngredientTransactionHandler struct { + service OrderIngredientTransactionService + validator validator.OrderIngredientTransactionValidator +} + +func NewOrderIngredientTransactionHandler(service OrderIngredientTransactionService, validator validator.OrderIngredientTransactionValidator) *OrderIngredientTransactionHandler { + return &OrderIngredientTransactionHandler{ + service: service, + validator: validator, + } +} + +func (h *OrderIngredientTransactionHandler) CreateOrderIngredientTransaction(c *gin.Context) { + var req contract.CreateOrderIngredientTransactionRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + response, err := h.service.CreateOrderIngredientTransaction(c, &req) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler") +} + +func (h *OrderIngredientTransactionHandler) GetOrderIngredientTransactionByID(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid ID format"}}), "OrderIngredientTransactionHandler") + return + } + + response, err := h.service.GetOrderIngredientTransactionByID(c, id) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler") +} + +func (h *OrderIngredientTransactionHandler) UpdateOrderIngredientTransaction(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid ID format"}}), "OrderIngredientTransactionHandler") + return + } + + var req contract.UpdateOrderIngredientTransactionRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + response, err := h.service.UpdateOrderIngredientTransaction(c, id, &req) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler") +} + +func (h *OrderIngredientTransactionHandler) DeleteOrderIngredientTransaction(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid ID format"}}), "OrderIngredientTransactionHandler") + return + } + + err = h.service.DeleteOrderIngredientTransaction(c, id) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(gin.H{"message": "Order ingredient transaction deleted successfully"}), "OrderIngredientTransactionHandler") +} + +func (h *OrderIngredientTransactionHandler) ListOrderIngredientTransactions(c *gin.Context) { + var req contract.ListOrderIngredientTransactionsRequest + if err := c.ShouldBindQuery(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + response, total, err := h.service.ListOrderIngredientTransactions(c, &req) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(gin.H{ + "data": response, + "total": total, + "page": req.Page, + "limit": req.Limit, + }), "OrderIngredientTransactionHandler") +} + +func (h *OrderIngredientTransactionHandler) GetOrderIngredientTransactionsByOrder(c *gin.Context) { + orderIDStr := c.Param("order_id") + orderID, err := uuid.Parse(orderIDStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid order ID format"}}), "OrderIngredientTransactionHandler") + return + } + + response, err := h.service.GetOrderIngredientTransactionsByOrder(c, orderID) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler") +} + +func (h *OrderIngredientTransactionHandler) GetOrderIngredientTransactionsByOrderItem(c *gin.Context) { + orderItemIDStr := c.Param("order_item_id") + orderItemID, err := uuid.Parse(orderItemIDStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid order item ID format"}}), "OrderIngredientTransactionHandler") + return + } + + response, err := h.service.GetOrderIngredientTransactionsByOrderItem(c, orderItemID) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler") +} + +func (h *OrderIngredientTransactionHandler) GetOrderIngredientTransactionsByIngredient(c *gin.Context) { + ingredientIDStr := c.Param("ingredient_id") + ingredientID, err := uuid.Parse(ingredientIDStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid ingredient ID format"}}), "OrderIngredientTransactionHandler") + return + } + + response, err := h.service.GetOrderIngredientTransactionsByIngredient(c, ingredientID) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler") +} + +func (h *OrderIngredientTransactionHandler) GetOrderIngredientTransactionSummary(c *gin.Context) { + var req contract.ListOrderIngredientTransactionsRequest + if err := c.ShouldBindQuery(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + response, err := h.service.GetOrderIngredientTransactionSummary(c, &req) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler") +} + +func (h *OrderIngredientTransactionHandler) BulkCreateOrderIngredientTransactions(c *gin.Context) { + var req struct { + Transactions []*contract.CreateOrderIngredientTransactionRequest `json:"transactions" validate:"required,min=1"` + } + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + response, err := h.service.BulkCreateOrderIngredientTransactions(c, req.Transactions) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler") +} diff --git a/internal/handler/order_ingredient_transaction_interface.go b/internal/handler/order_ingredient_transaction_interface.go new file mode 100644 index 0000000..92fe24b --- /dev/null +++ b/internal/handler/order_ingredient_transaction_interface.go @@ -0,0 +1,21 @@ +package handler + +import ( + "apskel-pos-be/internal/contract" + "context" + + "github.com/google/uuid" +) + +type OrderIngredientTransactionService interface { + CreateOrderIngredientTransaction(ctx context.Context, req *contract.CreateOrderIngredientTransactionRequest) (*contract.OrderIngredientTransactionResponse, error) + GetOrderIngredientTransactionByID(ctx context.Context, id uuid.UUID) (*contract.OrderIngredientTransactionResponse, error) + UpdateOrderIngredientTransaction(ctx context.Context, id uuid.UUID, req *contract.UpdateOrderIngredientTransactionRequest) (*contract.OrderIngredientTransactionResponse, error) + DeleteOrderIngredientTransaction(ctx context.Context, id uuid.UUID) error + ListOrderIngredientTransactions(ctx context.Context, req *contract.ListOrderIngredientTransactionsRequest) ([]*contract.OrderIngredientTransactionResponse, int64, error) + GetOrderIngredientTransactionsByOrder(ctx context.Context, orderID uuid.UUID) ([]*contract.OrderIngredientTransactionResponse, error) + GetOrderIngredientTransactionsByOrderItem(ctx context.Context, orderItemID uuid.UUID) ([]*contract.OrderIngredientTransactionResponse, error) + GetOrderIngredientTransactionsByIngredient(ctx context.Context, ingredientID uuid.UUID) ([]*contract.OrderIngredientTransactionResponse, error) + GetOrderIngredientTransactionSummary(ctx context.Context, req *contract.ListOrderIngredientTransactionsRequest) ([]*contract.OrderIngredientTransactionSummary, error) + BulkCreateOrderIngredientTransactions(ctx context.Context, transactions []*contract.CreateOrderIngredientTransactionRequest) ([]*contract.OrderIngredientTransactionResponse, error) +} diff --git a/internal/mappers/contract_mapper.go b/internal/mappers/contract_mapper.go index 51adb48..50413e9 100644 --- a/internal/mappers/contract_mapper.go +++ b/internal/mappers/contract_mapper.go @@ -166,3 +166,110 @@ func ModelToContractAccountResponse(resp *models.AccountResponse) *contract.Acco return contractResp } + +// Order Ingredient Transaction mappers +func ContractToModelCreateOrderIngredientTransactionRequest(req *contract.CreateOrderIngredientTransactionRequest) *models.CreateOrderIngredientTransactionRequest { + return &models.CreateOrderIngredientTransactionRequest{ + OrderID: req.OrderID, + OrderItemID: req.OrderItemID, + ProductID: req.ProductID, + ProductVariantID: req.ProductVariantID, + IngredientID: req.IngredientID, + GrossQty: req.GrossQty, + NetQty: req.NetQty, + WasteQty: req.WasteQty, + Unit: req.Unit, + TransactionDate: req.TransactionDate, + } +} + +func ContractToModelUpdateOrderIngredientTransactionRequest(req *contract.UpdateOrderIngredientTransactionRequest) *models.UpdateOrderIngredientTransactionRequest { + return &models.UpdateOrderIngredientTransactionRequest{ + GrossQty: req.GrossQty, + NetQty: req.NetQty, + WasteQty: req.WasteQty, + Unit: req.Unit, + TransactionDate: req.TransactionDate, + } +} + +func ModelToContractOrderIngredientTransactionResponse(resp *models.OrderIngredientTransactionResponse) *contract.OrderIngredientTransactionResponse { + return &contract.OrderIngredientTransactionResponse{ + ID: resp.ID, + OrganizationID: resp.OrganizationID, + OutletID: resp.OutletID, + OrderID: resp.OrderID, + OrderItemID: resp.OrderItemID, + ProductID: resp.ProductID, + ProductVariantID: resp.ProductVariantID, + IngredientID: resp.IngredientID, + GrossQty: resp.GrossQty, + NetQty: resp.NetQty, + WasteQty: resp.WasteQty, + Unit: resp.Unit, + TransactionDate: resp.TransactionDate, + CreatedBy: resp.CreatedBy, + CreatedAt: resp.CreatedAt, + UpdatedAt: resp.UpdatedAt, + Organization: resp.Organization, + Outlet: resp.Outlet, + Order: resp.Order, + OrderItem: resp.OrderItem, + Product: resp.Product, + ProductVariant: resp.ProductVariant, + Ingredient: resp.Ingredient, + CreatedByUser: resp.CreatedByUser, + } +} + +func ModelToContractOrderIngredientTransactionResponses(responses []*models.OrderIngredientTransactionResponse) []*contract.OrderIngredientTransactionResponse { + if responses == nil { + return nil + } + + contractResponses := make([]*contract.OrderIngredientTransactionResponse, len(responses)) + for i, resp := range responses { + contractResponses[i] = ModelToContractOrderIngredientTransactionResponse(resp) + } + + return contractResponses +} + +func ContractToModelListOrderIngredientTransactionsRequest(req *contract.ListOrderIngredientTransactionsRequest) *models.ListOrderIngredientTransactionsRequest { + return &models.ListOrderIngredientTransactionsRequest{ + OrderID: req.OrderID, + OrderItemID: req.OrderItemID, + ProductID: req.ProductID, + ProductVariantID: req.ProductVariantID, + IngredientID: req.IngredientID, + StartDate: req.StartDate, + EndDate: req.EndDate, + Page: req.Page, + Limit: req.Limit, + } +} + +func ModelToContractOrderIngredientTransactionSummary(resp *models.OrderIngredientTransactionSummary) *contract.OrderIngredientTransactionSummary { + return &contract.OrderIngredientTransactionSummary{ + IngredientID: resp.IngredientID, + IngredientName: resp.IngredientName, + TotalGrossQty: resp.TotalGrossQty, + TotalNetQty: resp.TotalNetQty, + TotalWasteQty: resp.TotalWasteQty, + WastePercentage: resp.WastePercentage, + Unit: resp.Unit, + } +} + +func ModelToContractOrderIngredientTransactionSummaries(responses []*models.OrderIngredientTransactionSummary) []*contract.OrderIngredientTransactionSummary { + if responses == nil { + return nil + } + + contractResponses := make([]*contract.OrderIngredientTransactionSummary, len(responses)) + for i, resp := range responses { + contractResponses[i] = ModelToContractOrderIngredientTransactionSummary(resp) + } + + return contractResponses +} diff --git a/internal/mappers/order_ingredient_transaction_mapper.go b/internal/mappers/order_ingredient_transaction_mapper.go new file mode 100644 index 0000000..0539c56 --- /dev/null +++ b/internal/mappers/order_ingredient_transaction_mapper.go @@ -0,0 +1,185 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + +func MapOrderIngredientTransactionEntityToModel(entity *entities.OrderIngredientTransaction) *models.OrderIngredientTransaction { + if entity == nil { + return nil + } + + return &models.OrderIngredientTransaction{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + OutletID: entity.OutletID, + OrderID: entity.OrderID, + OrderItemID: entity.OrderItemID, + ProductID: entity.ProductID, + ProductVariantID: entity.ProductVariantID, + IngredientID: entity.IngredientID, + GrossQty: entity.GrossQty, + NetQty: entity.NetQty, + WasteQty: entity.WasteQty, + Unit: entity.Unit, + TransactionDate: entity.TransactionDate, + CreatedBy: entity.CreatedBy, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + Organization: nil, + Outlet: nil, + Order: nil, + OrderItem: nil, + Product: nil, + ProductVariant: nil, + Ingredient: nil, + CreatedByUser: nil, + } +} + +func MapOrderIngredientTransactionModelToEntity(model *models.OrderIngredientTransaction) *entities.OrderIngredientTransaction { + if model == nil { + return nil + } + + return &entities.OrderIngredientTransaction{ + ID: model.ID, + OrganizationID: model.OrganizationID, + OutletID: model.OutletID, + OrderID: model.OrderID, + OrderItemID: model.OrderItemID, + ProductID: model.ProductID, + ProductVariantID: model.ProductVariantID, + IngredientID: model.IngredientID, + GrossQty: model.GrossQty, + NetQty: model.NetQty, + WasteQty: model.WasteQty, + Unit: model.Unit, + TransactionDate: model.TransactionDate, + CreatedBy: model.CreatedBy, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + Organization: entities.Organization{}, + Outlet: nil, + Order: entities.Order{}, + OrderItem: nil, + Product: entities.Product{}, + ProductVariant: nil, + Ingredient: entities.Ingredient{}, + CreatedByUser: entities.User{}, + } +} + +func MapOrderIngredientTransactionEntitiesToModels(entities []*entities.OrderIngredientTransaction) []*models.OrderIngredientTransaction { + if entities == nil { + return nil + } + + models := make([]*models.OrderIngredientTransaction, len(entities)) + for i, entity := range entities { + models[i] = MapOrderIngredientTransactionEntityToModel(entity) + } + + return models +} + +func MapOrderIngredientTransactionModelsToEntities(models []*models.OrderIngredientTransaction) []*entities.OrderIngredientTransaction { + if models == nil { + return nil + } + + entities := make([]*entities.OrderIngredientTransaction, len(models)) + for i, model := range models { + entities[i] = MapOrderIngredientTransactionModelToEntity(model) + } + + return entities +} + +func MapOrderIngredientTransactionEntityToResponse(entity *entities.OrderIngredientTransaction) *models.OrderIngredientTransactionResponse { + if entity == nil { + return nil + } + + return &models.OrderIngredientTransactionResponse{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + OutletID: entity.OutletID, + OrderID: entity.OrderID, + OrderItemID: entity.OrderItemID, + ProductID: entity.ProductID, + ProductVariantID: entity.ProductVariantID, + IngredientID: entity.IngredientID, + GrossQty: entity.GrossQty, + NetQty: entity.NetQty, + WasteQty: entity.WasteQty, + Unit: entity.Unit, + TransactionDate: entity.TransactionDate, + CreatedBy: entity.CreatedBy, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + Organization: nil, + Outlet: nil, + Order: nil, + OrderItem: nil, + Product: nil, + ProductVariant: nil, + Ingredient: nil, + CreatedByUser: nil, + } +} + +func MapOrderIngredientTransactionEntitiesToResponses(entities []*entities.OrderIngredientTransaction) []*models.OrderIngredientTransactionResponse { + if entities == nil { + return nil + } + + responses := make([]*models.OrderIngredientTransactionResponse, len(entities)) + for i, entity := range entities { + responses[i] = MapOrderIngredientTransactionEntityToResponse(entity) + } + + return responses +} + +func MapOrderIngredientTransactionSummary(transactions []*entities.OrderIngredientTransaction) []*models.OrderIngredientTransactionSummary { + if transactions == nil || len(transactions) == 0 { + return nil + } + + // Group by ingredient ID + ingredientMap := make(map[uuid.UUID]*models.OrderIngredientTransactionSummary) + + for _, transaction := range transactions { + ingredientID := transaction.IngredientID + + if summary, exists := ingredientMap[ingredientID]; exists { + summary.TotalGrossQty += transaction.GrossQty + summary.TotalNetQty += transaction.NetQty + summary.TotalWasteQty += transaction.WasteQty + } else { + ingredientMap[ingredientID] = &models.OrderIngredientTransactionSummary{ + IngredientID: ingredientID, + IngredientName: transaction.Ingredient.Name, + TotalGrossQty: transaction.GrossQty, + TotalNetQty: transaction.NetQty, + TotalWasteQty: transaction.WasteQty, + Unit: transaction.Unit, + } + } + } + + // Convert map to slice and calculate waste percentage + var summaries []*models.OrderIngredientTransactionSummary + for _, summary := range ingredientMap { + if summary.TotalGrossQty > 0 { + summary.WastePercentage = (summary.TotalWasteQty / summary.TotalGrossQty) * 100 + } + summaries = append(summaries, summary) + } + + return summaries +} diff --git a/internal/mappers/product_ingredient_mapper.go b/internal/mappers/product_ingredient_mapper.go index 1f59e2f..b7c99cb 100644 --- a/internal/mappers/product_ingredient_mapper.go +++ b/internal/mappers/product_ingredient_mapper.go @@ -11,16 +11,17 @@ func MapProductIngredientEntityToModel(entity *entities.ProductIngredient) *mode } return &models.ProductIngredient{ - ID: entity.ID, - OrganizationID: entity.OrganizationID, - OutletID: entity.OutletID, - ProductID: entity.ProductID, - IngredientID: entity.IngredientID, - Quantity: entity.Quantity, - CreatedAt: entity.CreatedAt, - UpdatedAt: entity.UpdatedAt, - Product: ProductEntityToModel(entity.Product), - Ingredient: MapIngredientEntityToModel(entity.Ingredient), + ID: entity.ID, + OrganizationID: entity.OrganizationID, + OutletID: entity.OutletID, + ProductID: entity.ProductID, + IngredientID: entity.IngredientID, + Quantity: entity.Quantity, + WastePercentage: entity.WastePercentage, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + Product: ProductEntityToModel(entity.Product), + Ingredient: MapIngredientEntityToModel(entity.Ingredient), } } @@ -30,16 +31,17 @@ func MapProductIngredientModelToEntity(model *models.ProductIngredient) *entitie } return &entities.ProductIngredient{ - ID: model.ID, - OrganizationID: model.OrganizationID, - OutletID: model.OutletID, - ProductID: model.ProductID, - IngredientID: model.IngredientID, - Quantity: model.Quantity, - CreatedAt: model.CreatedAt, - UpdatedAt: model.UpdatedAt, - Product: ProductModelToEntity(model.Product), - Ingredient: MapIngredientModelToEntity(model.Ingredient), + ID: model.ID, + OrganizationID: model.OrganizationID, + OutletID: model.OutletID, + ProductID: model.ProductID, + IngredientID: model.IngredientID, + Quantity: model.Quantity, + WastePercentage: model.WastePercentage, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + Product: ProductModelToEntity(model.Product), + Ingredient: MapIngredientModelToEntity(model.Ingredient), } } diff --git a/internal/models/ingredient_unit_converter.go b/internal/models/ingredient_unit_converter.go index 3a287e0..3413a7c 100644 --- a/internal/models/ingredient_unit_converter.go +++ b/internal/models/ingredient_unit_converter.go @@ -94,3 +94,11 @@ type ListIngredientUnitConvertersResponse struct { TotalPages int `json:"total_pages"` } +type IngredientUnitsResponse struct { + IngredientID uuid.UUID `json:"ingredient_id"` + IngredientName string `json:"ingredient_name"` + BaseUnitID uuid.UUID `json:"base_unit_id"` + BaseUnitName string `json:"base_unit_name"` + Units []*UnitResponse `json:"units"` +} + diff --git a/internal/models/order_ingredient_transaction.go b/internal/models/order_ingredient_transaction.go new file mode 100644 index 0000000..b8706fa --- /dev/null +++ b/internal/models/order_ingredient_transaction.go @@ -0,0 +1,108 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type OrderIngredientTransaction struct { + ID uuid.UUID + OrganizationID uuid.UUID + OutletID *uuid.UUID + OrderID uuid.UUID + OrderItemID *uuid.UUID + ProductID uuid.UUID + ProductVariantID *uuid.UUID + IngredientID uuid.UUID + GrossQty float64 + NetQty float64 + WasteQty float64 + Unit string + TransactionDate time.Time + CreatedBy uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + + // Relations + Organization *Organization `json:"organization,omitempty"` + Outlet *Outlet `json:"outlet,omitempty"` + Order *Order `json:"order,omitempty"` + OrderItem *OrderItem `json:"order_item,omitempty"` + Product *Product `json:"product,omitempty"` + ProductVariant *ProductVariant `json:"product_variant,omitempty"` + Ingredient *Ingredient `json:"ingredient,omitempty"` + CreatedByUser *User `json:"created_by_user,omitempty"` +} + +type CreateOrderIngredientTransactionRequest struct { + OrderID uuid.UUID `json:"order_id" validate:"required"` + OrderItemID *uuid.UUID `json:"order_item_id,omitempty"` + ProductID uuid.UUID `json:"product_id" validate:"required"` + ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"` + IngredientID uuid.UUID `json:"ingredient_id" validate:"required"` + GrossQty float64 `json:"gross_qty" validate:"required,gt=0"` + NetQty float64 `json:"net_qty" validate:"required,gt=0"` + WasteQty float64 `json:"waste_qty" validate:"min=0"` + Unit string `json:"unit" validate:"required,max=50"` + TransactionDate *time.Time `json:"transaction_date,omitempty"` +} + +type UpdateOrderIngredientTransactionRequest struct { + GrossQty *float64 `json:"gross_qty,omitempty" validate:"omitempty,gt=0"` + NetQty *float64 `json:"net_qty,omitempty" validate:"omitempty,gt=0"` + WasteQty *float64 `json:"waste_qty,omitempty" validate:"min=0"` + Unit *string `json:"unit,omitempty" validate:"omitempty,max=50"` + TransactionDate *time.Time `json:"transaction_date,omitempty"` +} + +type OrderIngredientTransactionResponse struct { + ID uuid.UUID + OrganizationID uuid.UUID + OutletID *uuid.UUID + OrderID uuid.UUID + OrderItemID *uuid.UUID + ProductID uuid.UUID + ProductVariantID *uuid.UUID + IngredientID uuid.UUID + GrossQty float64 + NetQty float64 + WasteQty float64 + Unit string + TransactionDate time.Time + CreatedBy uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + + // Relations + Organization *Organization `json:"organization,omitempty"` + Outlet *Outlet `json:"outlet,omitempty"` + Order *Order `json:"order,omitempty"` + OrderItem *OrderItem `json:"order_item,omitempty"` + Product *Product `json:"product,omitempty"` + ProductVariant *ProductVariant `json:"product_variant,omitempty"` + Ingredient *Ingredient `json:"ingredient,omitempty"` + CreatedByUser *User `json:"created_by_user,omitempty"` +} + +type ListOrderIngredientTransactionsRequest struct { + OrderID *uuid.UUID `json:"order_id,omitempty"` + OrderItemID *uuid.UUID `json:"order_item_id,omitempty"` + ProductID *uuid.UUID `json:"product_id,omitempty"` + ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"` + IngredientID *uuid.UUID `json:"ingredient_id,omitempty"` + StartDate *time.Time `json:"start_date,omitempty"` + EndDate *time.Time `json:"end_date,omitempty"` + Page int `json:"page" validate:"min=1"` + Limit int `json:"limit" validate:"min=1,max=100"` +} + +type OrderIngredientTransactionSummary struct { + IngredientID uuid.UUID `json:"ingredient_id"` + IngredientName string `json:"ingredient_name"` + TotalGrossQty float64 `json:"total_gross_qty"` + TotalNetQty float64 `json:"total_net_qty"` + TotalWasteQty float64 `json:"total_waste_qty"` + WastePercentage float64 `json:"waste_percentage"` + Unit string `json:"unit"` +} diff --git a/internal/models/product_ingredient.go b/internal/models/product_ingredient.go index 6cece33..3c3d9de 100644 --- a/internal/models/product_ingredient.go +++ b/internal/models/product_ingredient.go @@ -7,14 +7,15 @@ import ( ) type ProductIngredient struct { - ID uuid.UUID `json:"id"` - OrganizationID uuid.UUID `json:"organization_id"` - OutletID *uuid.UUID `json:"outlet_id"` - ProductID uuid.UUID `json:"product_id"` - IngredientID uuid.UUID `json:"ingredient_id"` - Quantity float64 `json:"quantity"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` + ProductID uuid.UUID `json:"product_id"` + IngredientID uuid.UUID `json:"ingredient_id"` + Quantity float64 `json:"quantity"` + WastePercentage float64 `json:"waste_percentage"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // Relations Product *Product `json:"product,omitempty"` @@ -22,26 +23,29 @@ type ProductIngredient struct { } type CreateProductIngredientRequest struct { - OutletID *uuid.UUID `json:"outlet_id"` - ProductID uuid.UUID `json:"product_id" validate:"required"` - IngredientID uuid.UUID `json:"ingredient_id" validate:"required"` - Quantity float64 `json:"quantity" validate:"required,gt=0"` + OutletID *uuid.UUID `json:"outlet_id"` + ProductID uuid.UUID `json:"product_id" validate:"required"` + IngredientID uuid.UUID `json:"ingredient_id" validate:"required"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` + WastePercentage float64 `json:"waste_percentage" validate:"min=0,max=100"` } type UpdateProductIngredientRequest struct { - OutletID *uuid.UUID `json:"outlet_id"` - Quantity float64 `json:"quantity" validate:"required,gt=0"` + OutletID *uuid.UUID `json:"outlet_id"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` + WastePercentage float64 `json:"waste_percentage" validate:"min=0,max=100"` } type ProductIngredientResponse struct { - ID uuid.UUID `json:"id"` - OrganizationID uuid.UUID `json:"organization_id"` - OutletID *uuid.UUID `json:"outlet_id"` - ProductID uuid.UUID `json:"product_id"` - IngredientID uuid.UUID `json:"ingredient_id"` - Quantity float64 `json:"quantity"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` + ProductID uuid.UUID `json:"product_id"` + IngredientID uuid.UUID `json:"ingredient_id"` + Quantity float64 `json:"quantity"` + WastePercentage float64 `json:"waste_percentage"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // Relations Product *Product `json:"product,omitempty"` diff --git a/internal/processor/account_processor.go b/internal/processor/account_processor.go index 00fd05b..6f991b5 100644 --- a/internal/processor/account_processor.go +++ b/internal/processor/account_processor.go @@ -12,7 +12,6 @@ import ( "github.com/google/uuid" ) - type AccountProcessor interface { CreateAccount(ctx context.Context, req *models.CreateAccountRequest) (*models.AccountResponse, error) GetAccountByID(ctx context.Context, id uuid.UUID) (*models.AccountResponse, error) diff --git a/internal/processor/chart_of_account_type_processor.go b/internal/processor/chart_of_account_type_processor.go index 486be1d..d416d19 100644 --- a/internal/processor/chart_of_account_type_processor.go +++ b/internal/processor/chart_of_account_type_processor.go @@ -10,7 +10,6 @@ import ( "github.com/google/uuid" ) - type ChartOfAccountTypeProcessor interface { CreateChartOfAccountType(ctx context.Context, req *models.CreateChartOfAccountTypeRequest) (*models.ChartOfAccountTypeResponse, error) GetChartOfAccountTypeByID(ctx context.Context, id uuid.UUID) (*models.ChartOfAccountTypeResponse, error) diff --git a/internal/processor/ingredient_repository.go b/internal/processor/ingredient_repository.go index 38dde9a..95af6c9 100644 --- a/internal/processor/ingredient_repository.go +++ b/internal/processor/ingredient_repository.go @@ -1,17 +1 @@ package processor - -import ( - "apskel-pos-be/internal/entities" - "context" - - "github.com/google/uuid" -) - -type IngredientRepository interface { - Create(ctx context.Context, ingredient *entities.Ingredient) error - GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Ingredient, error) - GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string, isSemiFinished *bool) ([]*entities.Ingredient, int, error) - Update(ctx context.Context, ingredient *entities.Ingredient) error - Delete(ctx context.Context, id, organizationID uuid.UUID) error - UpdateStock(ctx context.Context, id uuid.UUID, newStock float64, organizationID uuid.UUID) error -} diff --git a/internal/processor/ingredient_unit_converter_processor.go b/internal/processor/ingredient_unit_converter_processor.go index f344f24..9998fe7 100644 --- a/internal/processor/ingredient_unit_converter_processor.go +++ b/internal/processor/ingredient_unit_converter_processor.go @@ -32,6 +32,7 @@ type IngredientUnitConverterProcessor interface { ListIngredientUnitConverters(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.IngredientUnitConverterResponse, int, error) GetConvertersForIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*models.IngredientUnitConverterResponse, error) ConvertUnit(ctx context.Context, organizationID uuid.UUID, req *models.ConvertUnitRequest) (*models.ConvertUnitResponse, error) + GetUnitsByIngredientID(ctx context.Context, organizationID, ingredientID uuid.UUID) (*models.IngredientUnitsResponse, error) } type IngredientUnitConverterProcessorImpl struct { @@ -257,3 +258,64 @@ func (p *IngredientUnitConverterProcessorImpl) ConvertUnit(ctx context.Context, return response, nil } + +func (p *IngredientUnitConverterProcessorImpl) GetUnitsByIngredientID(ctx context.Context, organizationID, ingredientID uuid.UUID) (*models.IngredientUnitsResponse, error) { + // Get the ingredient with its base unit + ingredient, err := p.ingredientRepo.GetByID(ctx, ingredientID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get ingredient: %w", err) + } + + // Get the base unit details + baseUnit, err := p.unitRepo.GetByID(ctx, ingredient.UnitID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get base unit: %w", err) + } + + // Start with the base unit + units := []*models.UnitResponse{ + mappers.MapUnitEntityToResponse(baseUnit), + } + + // Get all converters for this ingredient + converters, err := p.converterRepo.GetConvertersForIngredient(ctx, ingredientID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get converters: %w", err) + } + + // Add unique units from converters + unitMap := make(map[uuid.UUID]bool) + unitMap[baseUnit.ID] = true + + for _, converter := range converters { + if converter.IsActive { + // Add FromUnit if not already added + if !unitMap[converter.FromUnitID] { + fromUnit, err := p.unitRepo.GetByID(ctx, converter.FromUnitID, organizationID) + if err == nil { + units = append(units, mappers.MapUnitEntityToResponse(fromUnit)) + unitMap[converter.FromUnitID] = true + } + } + + // Add ToUnit if not already added + if !unitMap[converter.ToUnitID] { + toUnit, err := p.unitRepo.GetByID(ctx, converter.ToUnitID, organizationID) + if err == nil { + units = append(units, mappers.MapUnitEntityToResponse(toUnit)) + unitMap[converter.ToUnitID] = true + } + } + } + } + + response := &models.IngredientUnitsResponse{ + IngredientID: ingredientID, + IngredientName: ingredient.Name, + BaseUnitID: baseUnit.ID, + BaseUnitName: baseUnit.Name, + Units: units, + } + + return response, nil +} diff --git a/internal/processor/order_ingredient_transaction_processor.go b/internal/processor/order_ingredient_transaction_processor.go new file mode 100644 index 0000000..bcef77d --- /dev/null +++ b/internal/processor/order_ingredient_transaction_processor.go @@ -0,0 +1,393 @@ +package processor + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/util" + "context" + "fmt" + "time" + + "github.com/google/uuid" +) + +type OrderIngredientTransactionProcessor interface { + CreateOrderIngredientTransaction(ctx context.Context, req *models.CreateOrderIngredientTransactionRequest, organizationID, outletID, createdBy uuid.UUID) (*models.OrderIngredientTransactionResponse, error) + GetOrderIngredientTransactionByID(ctx context.Context, id, organizationID uuid.UUID) (*models.OrderIngredientTransactionResponse, error) + UpdateOrderIngredientTransaction(ctx context.Context, id uuid.UUID, req *models.UpdateOrderIngredientTransactionRequest, organizationID uuid.UUID) (*models.OrderIngredientTransactionResponse, error) + DeleteOrderIngredientTransaction(ctx context.Context, id, organizationID uuid.UUID) error + ListOrderIngredientTransactions(ctx context.Context, req *models.ListOrderIngredientTransactionsRequest, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, int64, error) + GetOrderIngredientTransactionsByOrder(ctx context.Context, orderID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) + GetOrderIngredientTransactionsByOrderItem(ctx context.Context, orderItemID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) + GetOrderIngredientTransactionsByIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) + GetOrderIngredientTransactionSummary(ctx context.Context, req *models.ListOrderIngredientTransactionsRequest, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionSummary, error) + BulkCreateOrderIngredientTransactions(ctx context.Context, transactions []*models.CreateOrderIngredientTransactionRequest, organizationID, outletID, createdBy uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) + CalculateWasteQuantities(ctx context.Context, productID uuid.UUID, quantity float64, organizationID uuid.UUID) ([]*models.CreateOrderIngredientTransactionRequest, error) +} + +type OrderIngredientTransactionProcessorImpl struct { + orderIngredientTransactionRepo OrderIngredientTransactionRepository + productIngredientRepo ProductIngredientRepository + ingredientRepo IngredientRepository + unitRepo UnitRepository +} + +func NewOrderIngredientTransactionProcessorImpl( + orderIngredientTransactionRepo OrderIngredientTransactionRepository, + productIngredientRepo ProductIngredientRepository, + ingredientRepo IngredientRepository, + unitRepo UnitRepository, +) OrderIngredientTransactionProcessor { + return &OrderIngredientTransactionProcessorImpl{ + orderIngredientTransactionRepo: orderIngredientTransactionRepo, + productIngredientRepo: productIngredientRepo, + ingredientRepo: ingredientRepo, + unitRepo: unitRepo, + } +} + +func (p *OrderIngredientTransactionProcessorImpl) CreateOrderIngredientTransaction(ctx context.Context, req *models.CreateOrderIngredientTransactionRequest, organizationID, outletID, createdBy uuid.UUID) (*models.OrderIngredientTransactionResponse, error) { + // Validate that gross qty >= net qty + if req.GrossQty < req.NetQty { + return nil, fmt.Errorf("gross quantity must be greater than or equal to net quantity") + } + + // Validate that waste qty = gross qty - net qty + expectedWasteQty := req.GrossQty - req.NetQty + if req.WasteQty != expectedWasteQty { + return nil, fmt.Errorf("waste quantity must equal gross quantity minus net quantity") + } + + // Set transaction date if not provided + transactionDate := time.Now() + if req.TransactionDate != nil { + transactionDate = *req.TransactionDate + } + + // Create entity + entity := &entities.OrderIngredientTransaction{ + ID: uuid.New(), + OrganizationID: organizationID, + OutletID: &outletID, + OrderID: req.OrderID, + OrderItemID: req.OrderItemID, + ProductID: req.ProductID, + ProductVariantID: req.ProductVariantID, + IngredientID: req.IngredientID, + GrossQty: req.GrossQty, + NetQty: req.NetQty, + WasteQty: req.WasteQty, + Unit: req.Unit, + TransactionDate: transactionDate, + CreatedBy: createdBy, + } + + // Create in database + if err := p.orderIngredientTransactionRepo.Create(ctx, entity); err != nil { + return nil, fmt.Errorf("failed to create order ingredient transaction: %w", err) + } + + // Get created entity with relations + createdEntity, err := p.orderIngredientTransactionRepo.GetByID(ctx, entity.ID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get created order ingredient transaction: %w", err) + } + + // Convert to response + response := mappers.MapOrderIngredientTransactionEntityToResponse(createdEntity) + return response, nil +} + +func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionByID(ctx context.Context, id, organizationID uuid.UUID) (*models.OrderIngredientTransactionResponse, error) { + entity, err := p.orderIngredientTransactionRepo.GetByID(ctx, id, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get order ingredient transaction: %w", err) + } + + response := mappers.MapOrderIngredientTransactionEntityToResponse(entity) + return response, nil +} + +func (p *OrderIngredientTransactionProcessorImpl) UpdateOrderIngredientTransaction(ctx context.Context, id uuid.UUID, req *models.UpdateOrderIngredientTransactionRequest, organizationID uuid.UUID) (*models.OrderIngredientTransactionResponse, error) { + // Get existing entity + entity, err := p.orderIngredientTransactionRepo.GetByID(ctx, id, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get order ingredient transaction: %w", err) + } + + // Update fields + if req.GrossQty != nil { + entity.GrossQty = *req.GrossQty + } + if req.NetQty != nil { + entity.NetQty = *req.NetQty + } + if req.WasteQty != nil { + entity.WasteQty = *req.WasteQty + } + if req.Unit != nil { + entity.Unit = *req.Unit + } + if req.TransactionDate != nil { + entity.TransactionDate = *req.TransactionDate + } + + // Validate quantities + if entity.GrossQty < entity.NetQty { + return nil, fmt.Errorf("gross quantity must be greater than or equal to net quantity") + } + + expectedWasteQty := entity.GrossQty - entity.NetQty + if entity.WasteQty != expectedWasteQty { + return nil, fmt.Errorf("waste quantity must equal gross quantity minus net quantity") + } + + // Update in database + if err := p.orderIngredientTransactionRepo.Update(ctx, entity); err != nil { + return nil, fmt.Errorf("failed to update order ingredient transaction: %w", err) + } + + // Get updated entity with relations + updatedEntity, err := p.orderIngredientTransactionRepo.GetByID(ctx, id, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get updated order ingredient transaction: %w", err) + } + + response := mappers.MapOrderIngredientTransactionEntityToResponse(updatedEntity) + return response, nil +} + +func (p *OrderIngredientTransactionProcessorImpl) DeleteOrderIngredientTransaction(ctx context.Context, id, organizationID uuid.UUID) error { + if err := p.orderIngredientTransactionRepo.Delete(ctx, id, organizationID); err != nil { + return fmt.Errorf("failed to delete order ingredient transaction: %w", err) + } + return nil +} + +func (p *OrderIngredientTransactionProcessorImpl) ListOrderIngredientTransactions(ctx context.Context, req *models.ListOrderIngredientTransactionsRequest, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, int64, error) { + // Convert filters + filters := make(map[string]interface{}) + if req.OrderID != nil { + filters["order_id"] = *req.OrderID + } + if req.OrderItemID != nil { + filters["order_item_id"] = *req.OrderItemID + } + if req.ProductID != nil { + filters["product_id"] = *req.ProductID + } + if req.ProductVariantID != nil { + filters["product_variant_id"] = *req.ProductVariantID + } + if req.IngredientID != nil { + filters["ingredient_id"] = *req.IngredientID + } + if req.StartDate != nil { + filters["start_date"] = req.StartDate.Format(time.RFC3339) + } + if req.EndDate != nil { + filters["end_date"] = req.EndDate.Format(time.RFC3339) + } + + // Set default pagination + page := req.Page + if page <= 0 { + page = 1 + } + limit := req.Limit + if limit <= 0 { + limit = 10 + } + + entities, total, err := p.orderIngredientTransactionRepo.List(ctx, organizationID, filters, page, limit) + if err != nil { + return nil, 0, fmt.Errorf("failed to list order ingredient transactions: %w", err) + } + + responses := mappers.MapOrderIngredientTransactionEntitiesToResponses(entities) + return responses, total, nil +} + +func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionsByOrder(ctx context.Context, orderID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) { + entities, err := p.orderIngredientTransactionRepo.GetByOrderID(ctx, orderID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get order ingredient transactions by order: %w", err) + } + + responses := mappers.MapOrderIngredientTransactionEntitiesToResponses(entities) + return responses, nil +} + +func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionsByOrderItem(ctx context.Context, orderItemID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) { + entities, err := p.orderIngredientTransactionRepo.GetByOrderItemID(ctx, orderItemID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get order ingredient transactions by order item: %w", err) + } + + responses := mappers.MapOrderIngredientTransactionEntitiesToResponses(entities) + return responses, nil +} + +func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionsByIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) { + entities, err := p.orderIngredientTransactionRepo.GetByIngredientID(ctx, ingredientID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get order ingredient transactions by ingredient: %w", err) + } + + responses := mappers.MapOrderIngredientTransactionEntitiesToResponses(entities) + return responses, nil +} + +func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionSummary(ctx context.Context, req *models.ListOrderIngredientTransactionsRequest, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionSummary, error) { + // Convert filters + filters := make(map[string]interface{}) + if req.OrderID != nil { + filters["order_id"] = *req.OrderID + } + if req.OrderItemID != nil { + filters["order_item_id"] = *req.OrderItemID + } + if req.ProductID != nil { + filters["product_id"] = *req.ProductID + } + if req.ProductVariantID != nil { + filters["product_variant_id"] = *req.ProductVariantID + } + if req.IngredientID != nil { + filters["ingredient_id"] = *req.IngredientID + } + if req.StartDate != nil { + filters["start_date"] = req.StartDate.Format(time.RFC3339) + } + if req.EndDate != nil { + filters["end_date"] = req.EndDate.Format(time.RFC3339) + } + + entities, err := p.orderIngredientTransactionRepo.GetSummary(ctx, organizationID, filters) + if err != nil { + return nil, fmt.Errorf("failed to get order ingredient transaction summary: %w", err) + } + + summaries := mappers.MapOrderIngredientTransactionSummary(entities) + return summaries, nil +} + +func (p *OrderIngredientTransactionProcessorImpl) BulkCreateOrderIngredientTransactions(ctx context.Context, transactions []*models.CreateOrderIngredientTransactionRequest, organizationID, outletID, createdBy uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) { + if len(transactions) == 0 { + return []*models.OrderIngredientTransactionResponse{}, nil + } + + // Convert to entities + transactionEntities := make([]*entities.OrderIngredientTransaction, len(transactions)) + for i, req := range transactions { + // Validate quantities + if req.GrossQty < req.NetQty { + return nil, fmt.Errorf("gross quantity must be greater than or equal to net quantity for transaction %d", i) + } + + expectedWasteQty := req.GrossQty - req.NetQty + if req.WasteQty != expectedWasteQty { + return nil, fmt.Errorf("waste quantity must equal gross quantity minus net quantity for transaction %d", i) + } + + // Set transaction date if not provided + transactionDate := time.Now() + if req.TransactionDate != nil { + transactionDate = *req.TransactionDate + } + + transactionEntities[i] = &entities.OrderIngredientTransaction{ + ID: uuid.New(), + OrganizationID: organizationID, + OutletID: &outletID, + OrderID: req.OrderID, + OrderItemID: req.OrderItemID, + ProductID: req.ProductID, + ProductVariantID: req.ProductVariantID, + IngredientID: req.IngredientID, + GrossQty: req.GrossQty, + NetQty: req.NetQty, + WasteQty: req.WasteQty, + Unit: req.Unit, + TransactionDate: transactionDate, + CreatedBy: createdBy, + } + } + + // Bulk create + if err := p.orderIngredientTransactionRepo.BulkCreate(ctx, transactionEntities); err != nil { + return nil, fmt.Errorf("failed to bulk create order ingredient transactions: %w", err) + } + + // Get created entities with relations + responses := make([]*models.OrderIngredientTransactionResponse, len(transactionEntities)) + for i, entity := range transactionEntities { + createdEntity, err := p.orderIngredientTransactionRepo.GetByID(ctx, entity.ID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get created order ingredient transaction %d: %w", i, err) + } + responses[i] = mappers.MapOrderIngredientTransactionEntityToResponse(createdEntity) + } + + return responses, nil +} + +func (p *OrderIngredientTransactionProcessorImpl) CalculateWasteQuantities(ctx context.Context, productID uuid.UUID, quantity float64, organizationID uuid.UUID) ([]*models.CreateOrderIngredientTransactionRequest, error) { + // Get product ingredients + productIngredients, err := p.productIngredientRepo.GetByProductID(ctx, productID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get product ingredients: %w", err) + } + + if len(productIngredients) == 0 { + return []*models.CreateOrderIngredientTransactionRequest{}, nil + } + + // Get ingredient details for unit information + ingredientMap := make(map[uuid.UUID]*entities.Ingredient) + for _, pi := range productIngredients { + ingredient, err := p.ingredientRepo.GetByID(ctx, pi.IngredientID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get ingredient %s: %w", pi.IngredientID, err) + } + ingredientMap[pi.IngredientID] = ingredient + } + + // Calculate quantities for each ingredient + transactions := make([]*models.CreateOrderIngredientTransactionRequest, 0, len(productIngredients)) + for _, pi := range productIngredients { + ingredient := ingredientMap[pi.IngredientID] + + // Calculate net quantity (actual quantity needed for the product) + netQty := pi.Quantity * quantity + + // Calculate gross quantity (including waste) + wasteMultiplier := 1 + (pi.WastePercentage / 100) + grossQty := netQty * wasteMultiplier + + // Calculate waste quantity + wasteQty := grossQty - netQty + + // Get unit name + unitName := "unit" // default + if ingredient.UnitID != uuid.Nil { + unit, err := p.unitRepo.GetByID(ctx, ingredient.UnitID, organizationID) + if err == nil { + unitName = unit.Name + } + } + + transaction := &models.CreateOrderIngredientTransactionRequest{ + IngredientID: pi.IngredientID, + GrossQty: util.RoundToDecimalPlaces(grossQty, 3), + NetQty: util.RoundToDecimalPlaces(netQty, 3), + WasteQty: util.RoundToDecimalPlaces(wasteQty, 3), + Unit: unitName, + } + + transactions = append(transactions, transaction) + } + + return transactions, nil +} diff --git a/internal/processor/repository_interfaces.go b/internal/processor/repository_interfaces.go index 440db2b..573b282 100644 --- a/internal/processor/repository_interfaces.go +++ b/internal/processor/repository_interfaces.go @@ -42,3 +42,43 @@ type AccountRepository interface { UpdateBalance(ctx context.Context, id uuid.UUID, amount float64) error GetBalance(ctx context.Context, id uuid.UUID) (float64, error) } + +type OrderIngredientTransactionRepository interface { + Create(ctx context.Context, transaction *entities.OrderIngredientTransaction) error + GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.OrderIngredientTransaction, error) + Update(ctx context.Context, transaction *entities.OrderIngredientTransaction) error + Delete(ctx context.Context, id, organizationID uuid.UUID) error + List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*entities.OrderIngredientTransaction, int64, error) + GetByOrderID(ctx context.Context, orderID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error) + GetByOrderItemID(ctx context.Context, orderItemID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error) + GetByIngredientID(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error) + GetSummary(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}) ([]*entities.OrderIngredientTransaction, error) + BulkCreate(ctx context.Context, transactions []*entities.OrderIngredientTransaction) error +} + +type ProductIngredientRepository interface { + Create(ctx context.Context, productIngredient *entities.ProductIngredient) error + GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.ProductIngredient, error) + GetByProductID(ctx context.Context, productID, organizationID uuid.UUID) ([]*entities.ProductIngredient, error) + GetByIngredientID(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.ProductIngredient, error) + Update(ctx context.Context, productIngredient *entities.ProductIngredient) error + Delete(ctx context.Context, id, organizationID uuid.UUID) error + DeleteByProductID(ctx context.Context, productID, organizationID uuid.UUID) error +} + +type IngredientRepository interface { + Create(ctx context.Context, ingredient *entities.Ingredient) error + GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Ingredient, error) + GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string, isSemiFinished *bool) ([]*entities.Ingredient, int, error) + Update(ctx context.Context, ingredient *entities.Ingredient) error + Delete(ctx context.Context, id, organizationID uuid.UUID) error + UpdateStock(ctx context.Context, id uuid.UUID, newStock float64, organizationID uuid.UUID) error +} + +type UnitRepository interface { + Create(ctx context.Context, unit *entities.Unit) error + GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Unit, error) + GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) ([]*entities.Unit, int, error) + Update(ctx context.Context, unit *entities.Unit) error + Delete(ctx context.Context, id, organizationID uuid.UUID) error +} diff --git a/internal/processor/unit_repository.go b/internal/processor/unit_repository.go index 9328e65..95af6c9 100644 --- a/internal/processor/unit_repository.go +++ b/internal/processor/unit_repository.go @@ -1,16 +1 @@ package processor - -import ( - "apskel-pos-be/internal/entities" - "context" - - "github.com/google/uuid" -) - -type UnitRepository interface { - Create(ctx context.Context, unit *entities.Unit) error - GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Unit, error) - GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) ([]*entities.Unit, int, error) - Update(ctx context.Context, unit *entities.Unit) error - Delete(ctx context.Context, id, organizationID uuid.UUID) error -} diff --git a/internal/repository/order_ingredient_transaction_repository.go b/internal/repository/order_ingredient_transaction_repository.go new file mode 100644 index 0000000..6dfaa76 --- /dev/null +++ b/internal/repository/order_ingredient_transaction_repository.go @@ -0,0 +1,234 @@ +package repository + +import ( + "apskel-pos-be/internal/entities" + "context" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type OrderIngredientTransactionRepository interface { + Create(ctx context.Context, transaction *entities.OrderIngredientTransaction) error + GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.OrderIngredientTransaction, error) + Update(ctx context.Context, transaction *entities.OrderIngredientTransaction) error + Delete(ctx context.Context, id, organizationID uuid.UUID) error + List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*entities.OrderIngredientTransaction, int64, error) + GetByOrderID(ctx context.Context, orderID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error) + GetByOrderItemID(ctx context.Context, orderItemID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error) + GetByIngredientID(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error) + GetSummary(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}) ([]*entities.OrderIngredientTransaction, error) + BulkCreate(ctx context.Context, transactions []*entities.OrderIngredientTransaction) error +} + +type OrderIngredientTransactionRepositoryImpl struct { + db *gorm.DB +} + +func NewOrderIngredientTransactionRepositoryImpl(db *gorm.DB) OrderIngredientTransactionRepository { + return &OrderIngredientTransactionRepositoryImpl{db: db} +} + +func (r *OrderIngredientTransactionRepositoryImpl) Create(ctx context.Context, transaction *entities.OrderIngredientTransaction) error { + return r.db.WithContext(ctx).Create(transaction).Error +} + +func (r *OrderIngredientTransactionRepositoryImpl) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.OrderIngredientTransaction, error) { + var transaction entities.OrderIngredientTransaction + err := r.db.WithContext(ctx). + Where("id = ? AND organization_id = ?", id, organizationID). + Preload("Organization"). + Preload("Outlet"). + Preload("Order"). + Preload("OrderItem"). + Preload("Product"). + Preload("ProductVariant"). + Preload("Ingredient"). + Preload("CreatedByUser"). + First(&transaction).Error + if err != nil { + return nil, err + } + return &transaction, nil +} + +func (r *OrderIngredientTransactionRepositoryImpl) Update(ctx context.Context, transaction *entities.OrderIngredientTransaction) error { + return r.db.WithContext(ctx).Save(transaction).Error +} + +func (r *OrderIngredientTransactionRepositoryImpl) Delete(ctx context.Context, id, organizationID uuid.UUID) error { + return r.db.WithContext(ctx). + Where("id = ? AND organization_id = ?", id, organizationID). + Delete(&entities.OrderIngredientTransaction{}).Error +} + +func (r *OrderIngredientTransactionRepositoryImpl) List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*entities.OrderIngredientTransaction, int64, error) { + var transactions []*entities.OrderIngredientTransaction + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.OrderIngredientTransaction{}). + Where("organization_id = ?", organizationID) + + // Apply filters + for key, value := range filters { + switch key { + case "order_id": + if orderID, ok := value.(uuid.UUID); ok { + query = query.Where("order_id = ?", orderID) + } + case "order_item_id": + if orderItemID, ok := value.(uuid.UUID); ok { + query = query.Where("order_item_id = ?", orderItemID) + } + case "product_id": + if productID, ok := value.(uuid.UUID); ok { + query = query.Where("product_id = ?", productID) + } + case "product_variant_id": + if productVariantID, ok := value.(uuid.UUID); ok { + query = query.Where("product_variant_id = ?", productVariantID) + } + case "ingredient_id": + if ingredientID, ok := value.(uuid.UUID); ok { + query = query.Where("ingredient_id = ?", ingredientID) + } + case "start_date": + if startDate, ok := value.(string); ok { + query = query.Where("transaction_date >= ?", startDate) + } + case "end_date": + if endDate, ok := value.(string); ok { + query = query.Where("transaction_date <= ?", endDate) + } + } + } + + // Get total count + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + // Apply pagination and get results + offset := (page - 1) * limit + err := query. + Preload("Organization"). + Preload("Outlet"). + Preload("Order"). + Preload("OrderItem"). + Preload("Product"). + Preload("ProductVariant"). + Preload("Ingredient"). + Preload("CreatedByUser"). + Order("created_at DESC"). + Offset(offset). + Limit(limit). + Find(&transactions).Error + + return transactions, total, err +} + +func (r *OrderIngredientTransactionRepositoryImpl) GetByOrderID(ctx context.Context, orderID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error) { + var transactions []*entities.OrderIngredientTransaction + err := r.db.WithContext(ctx). + Where("order_id = ? AND organization_id = ?", orderID, organizationID). + Preload("Organization"). + Preload("Outlet"). + Preload("Order"). + Preload("OrderItem"). + Preload("Product"). + Preload("ProductVariant"). + Preload("Ingredient"). + Preload("CreatedByUser"). + Order("created_at ASC"). + Find(&transactions).Error + return transactions, err +} + +func (r *OrderIngredientTransactionRepositoryImpl) GetByOrderItemID(ctx context.Context, orderItemID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error) { + var transactions []*entities.OrderIngredientTransaction + err := r.db.WithContext(ctx). + Where("order_item_id = ? AND organization_id = ?", orderItemID, organizationID). + Preload("Organization"). + Preload("Outlet"). + Preload("Order"). + Preload("OrderItem"). + Preload("Product"). + Preload("ProductVariant"). + Preload("Ingredient"). + Preload("CreatedByUser"). + Order("created_at ASC"). + Find(&transactions).Error + return transactions, err +} + +func (r *OrderIngredientTransactionRepositoryImpl) GetByIngredientID(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error) { + var transactions []*entities.OrderIngredientTransaction + err := r.db.WithContext(ctx). + Where("ingredient_id = ? AND organization_id = ?", ingredientID, organizationID). + Preload("Organization"). + Preload("Outlet"). + Preload("Order"). + Preload("OrderItem"). + Preload("Product"). + Preload("ProductVariant"). + Preload("Ingredient"). + Preload("CreatedByUser"). + Order("created_at DESC"). + Find(&transactions).Error + return transactions, err +} + +func (r *OrderIngredientTransactionRepositoryImpl) GetSummary(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}) ([]*entities.OrderIngredientTransaction, error) { + var transactions []*entities.OrderIngredientTransaction + + query := r.db.WithContext(ctx).Model(&entities.OrderIngredientTransaction{}). + Where("organization_id = ?", organizationID) + + // Apply filters + for key, value := range filters { + switch key { + case "order_id": + if orderID, ok := value.(uuid.UUID); ok { + query = query.Where("order_id = ?", orderID) + } + case "order_item_id": + if orderItemID, ok := value.(uuid.UUID); ok { + query = query.Where("order_item_id = ?", orderItemID) + } + case "product_id": + if productID, ok := value.(uuid.UUID); ok { + query = query.Where("product_id = ?", productID) + } + case "product_variant_id": + if productVariantID, ok := value.(uuid.UUID); ok { + query = query.Where("product_variant_id = ?", productVariantID) + } + case "ingredient_id": + if ingredientID, ok := value.(uuid.UUID); ok { + query = query.Where("ingredient_id = ?", ingredientID) + } + case "start_date": + if startDate, ok := value.(string); ok { + query = query.Where("transaction_date >= ?", startDate) + } + case "end_date": + if endDate, ok := value.(string); ok { + query = query.Where("transaction_date <= ?", endDate) + } + } + } + + err := query. + Preload("Ingredient"). + Order("ingredient_id, created_at ASC"). + Find(&transactions).Error + + return transactions, err +} + +func (r *OrderIngredientTransactionRepositoryImpl) BulkCreate(ctx context.Context, transactions []*entities.OrderIngredientTransaction) error { + if len(transactions) == 0 { + return nil + } + return r.db.WithContext(ctx).CreateInBatches(transactions, 100).Error +} diff --git a/internal/router/router.go b/internal/router/router.go index abf50f2..17a67c1 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -12,34 +12,35 @@ import ( ) type Router struct { - config *config.Config - healthHandler *handler.HealthHandler - authHandler *handler.AuthHandler - userHandler *handler.UserHandler - organizationHandler *handler.OrganizationHandler - outletHandler *handler.OutletHandler - outletSettingHandler *handler.OutletSettingHandlerImpl - categoryHandler *handler.CategoryHandler - productHandler *handler.ProductHandler - productVariantHandler *handler.ProductVariantHandler - inventoryHandler *handler.InventoryHandler - orderHandler *handler.OrderHandler - fileHandler *handler.FileHandler - customerHandler *handler.CustomerHandler - paymentMethodHandler *handler.PaymentMethodHandler - analyticsHandler *handler.AnalyticsHandler - reportHandler *handler.ReportHandler - tableHandler *handler.TableHandler - unitHandler *handler.UnitHandler - ingredientHandler *handler.IngredientHandler - productRecipeHandler *handler.ProductRecipeHandler - vendorHandler *handler.VendorHandler - purchaseOrderHandler *handler.PurchaseOrderHandler - unitConverterHandler *handler.IngredientUnitConverterHandler - chartOfAccountTypeHandler *handler.ChartOfAccountTypeHandler - chartOfAccountHandler *handler.ChartOfAccountHandler - accountHandler *handler.AccountHandler - authMiddleware *middleware.AuthMiddleware + config *config.Config + healthHandler *handler.HealthHandler + authHandler *handler.AuthHandler + userHandler *handler.UserHandler + organizationHandler *handler.OrganizationHandler + outletHandler *handler.OutletHandler + outletSettingHandler *handler.OutletSettingHandlerImpl + categoryHandler *handler.CategoryHandler + productHandler *handler.ProductHandler + productVariantHandler *handler.ProductVariantHandler + inventoryHandler *handler.InventoryHandler + orderHandler *handler.OrderHandler + fileHandler *handler.FileHandler + customerHandler *handler.CustomerHandler + paymentMethodHandler *handler.PaymentMethodHandler + analyticsHandler *handler.AnalyticsHandler + reportHandler *handler.ReportHandler + tableHandler *handler.TableHandler + unitHandler *handler.UnitHandler + ingredientHandler *handler.IngredientHandler + productRecipeHandler *handler.ProductRecipeHandler + vendorHandler *handler.VendorHandler + purchaseOrderHandler *handler.PurchaseOrderHandler + unitConverterHandler *handler.IngredientUnitConverterHandler + chartOfAccountTypeHandler *handler.ChartOfAccountTypeHandler + chartOfAccountHandler *handler.ChartOfAccountHandler + accountHandler *handler.AccountHandler + orderIngredientTransactionHandler *handler.OrderIngredientTransactionHandler + authMiddleware *middleware.AuthMiddleware } func NewRouter(cfg *config.Config, @@ -87,36 +88,39 @@ func NewRouter(cfg *config.Config, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, - accountValidator validator.AccountValidator) *Router { + accountValidator validator.AccountValidator, + orderIngredientTransactionService service.OrderIngredientTransactionService, + orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator) *Router { return &Router{ - config: cfg, - healthHandler: healthHandler, - authHandler: handler.NewAuthHandler(authService), - userHandler: handler.NewUserHandler(userService, userValidator), - organizationHandler: handler.NewOrganizationHandler(organizationService, organizationValidator), - outletHandler: handler.NewOutletHandler(outletService, outletValidator), - outletSettingHandler: handler.NewOutletSettingHandlerImpl(outletSettingService), - categoryHandler: handler.NewCategoryHandler(categoryService, categoryValidator), - productHandler: handler.NewProductHandler(productService, productValidator), - inventoryHandler: handler.NewInventoryHandler(inventoryService, inventoryValidator), - orderHandler: handler.NewOrderHandler(orderService, orderValidator, transformer.NewTransformer()), - fileHandler: handler.NewFileHandler(fileService, fileValidator, transformer.NewTransformer()), - customerHandler: handler.NewCustomerHandler(customerService, customerValidator), - paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator), - analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()), - reportHandler: handler.NewReportHandler(reportService, userService), - tableHandler: handler.NewTableHandler(tableService, tableValidator), - unitHandler: handler.NewUnitHandler(unitService), - ingredientHandler: handler.NewIngredientHandler(ingredientService), - productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService), - vendorHandler: handler.NewVendorHandler(vendorService, vendorValidator), - purchaseOrderHandler: handler.NewPurchaseOrderHandler(purchaseOrderService, purchaseOrderValidator), - unitConverterHandler: handler.NewIngredientUnitConverterHandler(unitConverterService, unitConverterValidator), - chartOfAccountTypeHandler: handler.NewChartOfAccountTypeHandler(chartOfAccountTypeService, chartOfAccountTypeValidator), - chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator), - accountHandler: handler.NewAccountHandler(accountService, accountValidator), - authMiddleware: authMiddleware, + config: cfg, + healthHandler: healthHandler, + authHandler: handler.NewAuthHandler(authService), + userHandler: handler.NewUserHandler(userService, userValidator), + organizationHandler: handler.NewOrganizationHandler(organizationService, organizationValidator), + outletHandler: handler.NewOutletHandler(outletService, outletValidator), + outletSettingHandler: handler.NewOutletSettingHandlerImpl(outletSettingService), + categoryHandler: handler.NewCategoryHandler(categoryService, categoryValidator), + productHandler: handler.NewProductHandler(productService, productValidator), + inventoryHandler: handler.NewInventoryHandler(inventoryService, inventoryValidator), + orderHandler: handler.NewOrderHandler(orderService, orderValidator, transformer.NewTransformer()), + fileHandler: handler.NewFileHandler(fileService, fileValidator, transformer.NewTransformer()), + customerHandler: handler.NewCustomerHandler(customerService, customerValidator), + paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator), + analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()), + reportHandler: handler.NewReportHandler(reportService, userService), + tableHandler: handler.NewTableHandler(tableService, tableValidator), + unitHandler: handler.NewUnitHandler(unitService), + ingredientHandler: handler.NewIngredientHandler(ingredientService), + productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService), + vendorHandler: handler.NewVendorHandler(vendorService, vendorValidator), + purchaseOrderHandler: handler.NewPurchaseOrderHandler(purchaseOrderService, purchaseOrderValidator), + unitConverterHandler: handler.NewIngredientUnitConverterHandler(unitConverterService, unitConverterValidator), + chartOfAccountTypeHandler: handler.NewChartOfAccountTypeHandler(chartOfAccountTypeService, chartOfAccountTypeValidator), + chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator), + accountHandler: handler.NewAccountHandler(accountService, accountValidator), + orderIngredientTransactionHandler: handler.NewOrderIngredientTransactionHandler(&orderIngredientTransactionService, orderIngredientTransactionValidator), + authMiddleware: authMiddleware, } } @@ -351,6 +355,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { unitConverters.POST("", r.unitConverterHandler.CreateIngredientUnitConverter) unitConverters.GET("", r.unitConverterHandler.ListIngredientUnitConverters) unitConverters.GET("/ingredient/:ingredient_id", r.unitConverterHandler.GetConvertersForIngredient) + unitConverters.GET("/ingredient/:ingredient_id/units", r.unitConverterHandler.GetUnitsByIngredientID) unitConverters.POST("/convert", r.unitConverterHandler.ConvertUnit) unitConverters.GET("/:id", r.unitConverterHandler.GetIngredientUnitConverter) unitConverters.PUT("/:id", r.unitConverterHandler.UpdateIngredientUnitConverter) @@ -406,6 +411,21 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { accounts.GET("/:id/balance", r.accountHandler.GetAccountBalance) } + orderIngredientTransactions := protected.Group("/order-ingredient-transactions") + orderIngredientTransactions.Use(r.authMiddleware.RequireAdminOrManager()) + { + orderIngredientTransactions.POST("", r.orderIngredientTransactionHandler.CreateOrderIngredientTransaction) + orderIngredientTransactions.GET("", r.orderIngredientTransactionHandler.ListOrderIngredientTransactions) + orderIngredientTransactions.GET("/:id", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionByID) + orderIngredientTransactions.PUT("/:id", r.orderIngredientTransactionHandler.UpdateOrderIngredientTransaction) + orderIngredientTransactions.DELETE("/:id", r.orderIngredientTransactionHandler.DeleteOrderIngredientTransaction) + orderIngredientTransactions.GET("/order/:order_id", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionsByOrder) + orderIngredientTransactions.GET("/order-item/:order_item_id", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionsByOrderItem) + orderIngredientTransactions.GET("/ingredient/:ingredient_id", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionsByIngredient) + orderIngredientTransactions.GET("/summary", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionSummary) + orderIngredientTransactions.POST("/bulk", r.orderIngredientTransactionHandler.BulkCreateOrderIngredientTransactions) + } + outlets := protected.Group("/outlets") outlets.Use(r.authMiddleware.RequireAdminOrManager()) { diff --git a/internal/service/ingredient_unit_converter_service.go b/internal/service/ingredient_unit_converter_service.go index 665bad1..7d7d0a4 100644 --- a/internal/service/ingredient_unit_converter_service.go +++ b/internal/service/ingredient_unit_converter_service.go @@ -19,6 +19,7 @@ type IngredientUnitConverterService interface { ListIngredientUnitConverters(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListIngredientUnitConvertersRequest) *contract.Response GetConvertersForIngredient(ctx context.Context, apctx *appcontext.ContextInfo, ingredientID uuid.UUID) *contract.Response ConvertUnit(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ConvertUnitRequest) *contract.Response + GetUnitsByIngredientID(ctx context.Context, apctx *appcontext.ContextInfo, ingredientID uuid.UUID) *contract.Response } type IngredientUnitConverterServiceImpl struct { @@ -149,3 +150,14 @@ func (s *IngredientUnitConverterServiceImpl) ConvertUnit(ctx context.Context, ap return contract.BuildSuccessResponse(contractResponse) } +func (s *IngredientUnitConverterServiceImpl) GetUnitsByIngredientID(ctx context.Context, apctx *appcontext.ContextInfo, ingredientID uuid.UUID) *contract.Response { + unitsResponse, err := s.converterProcessor.GetUnitsByIngredientID(ctx, apctx.OrganizationID, ingredientID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.IngredientUnitConverterServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.IngredientUnitsModelResponseToResponse(unitsResponse) + return contract.BuildSuccessResponse(contractResponse) +} + diff --git a/internal/service/order_ingredient_transaction_service.go b/internal/service/order_ingredient_transaction_service.go new file mode 100644 index 0000000..1d10554 --- /dev/null +++ b/internal/service/order_ingredient_transaction_service.go @@ -0,0 +1,205 @@ +package service + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/repository" + "context" + "fmt" + + "github.com/google/uuid" +) + +type OrderIngredientTransactionService struct { + processor processor.OrderIngredientTransactionProcessor + txManager *repository.TxManager +} + +func NewOrderIngredientTransactionService(processor processor.OrderIngredientTransactionProcessor, txManager *repository.TxManager) *OrderIngredientTransactionService { + return &OrderIngredientTransactionService{ + processor: processor, + txManager: txManager, + } +} + +func (s *OrderIngredientTransactionService) CreateOrderIngredientTransaction(ctx context.Context, req *contract.CreateOrderIngredientTransactionRequest) (*contract.OrderIngredientTransactionResponse, error) { + // Get organization and outlet from context + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + outletID := appCtx.OutletID + createdBy := appCtx.UserID + + // Convert contract to model + modelReq := mappers.ContractToModelCreateOrderIngredientTransactionRequest(req) + + // Create transaction + response, err := s.processor.CreateOrderIngredientTransaction(ctx, modelReq, organizationID, outletID, createdBy) + if err != nil { + return nil, fmt.Errorf("failed to create order ingredient transaction: %w", err) + } + + // Convert model to contract + contractResp := mappers.ModelToContractOrderIngredientTransactionResponse(response) + return contractResp, nil +} + +func (s *OrderIngredientTransactionService) GetOrderIngredientTransactionByID(ctx context.Context, id uuid.UUID) (*contract.OrderIngredientTransactionResponse, error) { + // Get organization from context + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + + // Get transaction + response, err := s.processor.GetOrderIngredientTransactionByID(ctx, id, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get order ingredient transaction: %w", err) + } + + // Convert model to contract + contractResp := mappers.ModelToContractOrderIngredientTransactionResponse(response) + return contractResp, nil +} + +func (s *OrderIngredientTransactionService) UpdateOrderIngredientTransaction(ctx context.Context, id uuid.UUID, req *contract.UpdateOrderIngredientTransactionRequest) (*contract.OrderIngredientTransactionResponse, error) { + // Get organization from context + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + + // Convert contract to model + modelReq := mappers.ContractToModelUpdateOrderIngredientTransactionRequest(req) + + // Update transaction + response, err := s.processor.UpdateOrderIngredientTransaction(ctx, id, modelReq, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to update order ingredient transaction: %w", err) + } + + // Convert model to contract + contractResp := mappers.ModelToContractOrderIngredientTransactionResponse(response) + return contractResp, nil +} + +func (s *OrderIngredientTransactionService) DeleteOrderIngredientTransaction(ctx context.Context, id uuid.UUID) error { + // Get organization from context + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + + // Delete transaction + if err := s.processor.DeleteOrderIngredientTransaction(ctx, id, organizationID); err != nil { + return fmt.Errorf("failed to delete order ingredient transaction: %w", err) + } + + return nil +} + +func (s *OrderIngredientTransactionService) ListOrderIngredientTransactions(ctx context.Context, req *contract.ListOrderIngredientTransactionsRequest) ([]*contract.OrderIngredientTransactionResponse, int64, error) { + // Get organization from context + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + + // Convert contract to model + modelReq := mappers.ContractToModelListOrderIngredientTransactionsRequest(req) + + // List transactions + responses, total, err := s.processor.ListOrderIngredientTransactions(ctx, modelReq, organizationID) + if err != nil { + return nil, 0, fmt.Errorf("failed to list order ingredient transactions: %w", err) + } + + // Convert models to contracts + contractResponses := mappers.ModelToContractOrderIngredientTransactionResponses(responses) + return contractResponses, total, nil +} + +func (s *OrderIngredientTransactionService) GetOrderIngredientTransactionsByOrder(ctx context.Context, orderID uuid.UUID) ([]*contract.OrderIngredientTransactionResponse, error) { + // Get organization from context + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + + // Get transactions by order + responses, err := s.processor.GetOrderIngredientTransactionsByOrder(ctx, orderID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get order ingredient transactions by order: %w", err) + } + + // Convert models to contracts + contractResponses := mappers.ModelToContractOrderIngredientTransactionResponses(responses) + return contractResponses, nil +} + +func (s *OrderIngredientTransactionService) GetOrderIngredientTransactionsByOrderItem(ctx context.Context, orderItemID uuid.UUID) ([]*contract.OrderIngredientTransactionResponse, error) { + // Get organization from context + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + + // Get transactions by order item + responses, err := s.processor.GetOrderIngredientTransactionsByOrderItem(ctx, orderItemID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get order ingredient transactions by order item: %w", err) + } + + // Convert models to contracts + contractResponses := mappers.ModelToContractOrderIngredientTransactionResponses(responses) + return contractResponses, nil +} + +func (s *OrderIngredientTransactionService) GetOrderIngredientTransactionsByIngredient(ctx context.Context, ingredientID uuid.UUID) ([]*contract.OrderIngredientTransactionResponse, error) { + // Get organization from context + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + + // Get transactions by ingredient + responses, err := s.processor.GetOrderIngredientTransactionsByIngredient(ctx, ingredientID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get order ingredient transactions by ingredient: %w", err) + } + + // Convert models to contracts + contractResponses := mappers.ModelToContractOrderIngredientTransactionResponses(responses) + return contractResponses, nil +} + +func (s *OrderIngredientTransactionService) GetOrderIngredientTransactionSummary(ctx context.Context, req *contract.ListOrderIngredientTransactionsRequest) ([]*contract.OrderIngredientTransactionSummary, error) { + // Get organization from context + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + + // Convert contract to model + modelReq := mappers.ContractToModelListOrderIngredientTransactionsRequest(req) + + // Get summary + summaries, err := s.processor.GetOrderIngredientTransactionSummary(ctx, modelReq, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get order ingredient transaction summary: %w", err) + } + + // Convert models to contracts + contractSummaries := mappers.ModelToContractOrderIngredientTransactionSummaries(summaries) + return contractSummaries, nil +} + +func (s *OrderIngredientTransactionService) BulkCreateOrderIngredientTransactions(ctx context.Context, transactions []*contract.CreateOrderIngredientTransactionRequest) ([]*contract.OrderIngredientTransactionResponse, error) { + // Get organization and outlet from context + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + outletID := appCtx.OutletID + createdBy := appCtx.UserID + + // Convert contracts to models + modelReqs := make([]*models.CreateOrderIngredientTransactionRequest, len(transactions)) + for i, req := range transactions { + modelReqs[i] = mappers.ContractToModelCreateOrderIngredientTransactionRequest(req) + } + + // Bulk create transactions + responses, err := s.processor.BulkCreateOrderIngredientTransactions(ctx, modelReqs, organizationID, outletID, createdBy) + if err != nil { + return nil, fmt.Errorf("failed to bulk create order ingredient transactions: %w", err) + } + + // Convert models to contracts + contractResponses := mappers.ModelToContractOrderIngredientTransactionResponses(responses) + return contractResponses, nil +} diff --git a/internal/service/order_service.go b/internal/service/order_service.go index 081d1b1..b206931 100644 --- a/internal/service/order_service.go +++ b/internal/service/order_service.go @@ -1,13 +1,17 @@ package service import ( + "apskel-pos-be/internal/appcontext" "context" "fmt" "time" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/entities" "apskel-pos-be/internal/models" "apskel-pos-be/internal/processor" "apskel-pos-be/internal/repository" + "apskel-pos-be/internal/util" "github.com/google/uuid" ) @@ -27,14 +31,22 @@ type OrderService interface { } type OrderServiceImpl struct { - orderProcessor processor.OrderProcessor - tableRepo repository.TableRepositoryInterface + orderProcessor processor.OrderProcessor + tableRepo repository.TableRepositoryInterface + orderIngredientTransactionService *OrderIngredientTransactionService + orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor + productIngredientRepo repository.ProductIngredientRepository + txManager *repository.TxManager } -func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repository.TableRepositoryInterface) *OrderServiceImpl { +func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repository.TableRepositoryInterface, orderIngredientTransactionService *OrderIngredientTransactionService, orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor, productIngredientRepo repository.ProductIngredientRepository, txManager *repository.TxManager) *OrderServiceImpl { return &OrderServiceImpl{ - orderProcessor: orderProcessor, - tableRepo: tableRepo, + orderProcessor: orderProcessor, + tableRepo: tableRepo, + orderIngredientTransactionService: orderIngredientTransactionService, + orderIngredientTransactionProcessor: orderIngredientTransactionProcessor, + productIngredientRepo: productIngredientRepo, + txManager: txManager, } } @@ -49,47 +61,133 @@ func (s *OrderServiceImpl) CreateOrder(ctx context.Context, req *models.CreateOr } } - response, err := s.orderProcessor.CreateOrder(ctx, req, organizationID) - if err != nil { - return nil, fmt.Errorf("failed to create order: %w", err) - } + var response *models.OrderResponse + var ingredientTransactions []*contract.CreateOrderIngredientTransactionRequest - if req.TableID != nil { - if err := s.occupyTableWithOrder(ctx, *req.TableID, response.ID); err != nil { - fmt.Printf("Warning: failed to occupy table %s with order %s: %v\n", *req.TableID, response.ID, err) + // Use transaction to ensure atomicity + err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + // Create the order + orderResp, err := s.orderProcessor.CreateOrder(txCtx, req, organizationID) + if err != nil { + return fmt.Errorf("failed to create order: %w", err) } + response = orderResp + + // Create ingredient transactions for each order item + ingredientTransactions, err = s.createIngredientTransactions(txCtx, response.ID, response.OrderItems) + if err != nil { + return fmt.Errorf("failed to create ingredient transactions: %w", err) + } + + // Bulk create ingredient transactions + if len(ingredientTransactions) > 0 { + _, err = s.orderIngredientTransactionService.BulkCreateOrderIngredientTransactions(txCtx, ingredientTransactions) + if err != nil { + return fmt.Errorf("failed to bulk create ingredient transactions: %w", err) + } + } + + // Occupy table if specified + if req.TableID != nil { + if err := s.occupyTableWithOrder(txCtx, *req.TableID, response.ID); err != nil { + // Log warning but don't fail the transaction + fmt.Printf("Warning: failed to occupy table %s with order %s: %v\n", *req.TableID, response.ID, err) + } + } + + return nil + }) + + if err != nil { + return nil, err } return response, nil } +// createIngredientTransactions creates ingredient transactions for order items efficiently +func (s *OrderServiceImpl) createIngredientTransactions(ctx context.Context, orderID uuid.UUID, orderItems []models.OrderItemResponse) ([]*contract.CreateOrderIngredientTransactionRequest, error) { + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + var allTransactions []*contract.CreateOrderIngredientTransactionRequest + + for _, orderItem := range orderItems { + // Get product ingredients for this product + productIngredients, err := s.productIngredientRepo.GetByProductID(ctx, orderItem.ProductID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get product ingredients for product %s: %w", orderItem.ProductID, err) + } + + if len(productIngredients) == 0 { + continue // Skip if no ingredients + } + + // Calculate waste quantities + transactions, err := s.calculateWasteQuantities(productIngredients, float64(orderItem.Quantity)) + if err != nil { + return nil, fmt.Errorf("failed to calculate waste quantities for product %s: %w", err) + } + + // Set common fields for all transactions + for _, transaction := range transactions { + transaction.OrderID = orderID + transaction.OrderItemID = &orderItem.ID + transaction.ProductID = orderItem.ProductID + transaction.ProductVariantID = orderItem.ProductVariantID + } + + allTransactions = append(allTransactions, transactions...) + } + + return allTransactions, nil +} + func (s *OrderServiceImpl) AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) { - // Validate inputs if orderID == uuid.Nil { return nil, fmt.Errorf("invalid order ID") } - // Validate request if err := s.validateAddToOrderRequest(req); err != nil { return nil, fmt.Errorf("validation error: %w", err) } - // Process adding items to order - response, err := s.orderProcessor.AddToOrder(ctx, orderID, req) + var response *models.AddToOrderResponse + var ingredientTransactions []*contract.CreateOrderIngredientTransactionRequest + + err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + addResp, err := s.orderProcessor.AddToOrder(txCtx, orderID, req) + if err != nil { + return fmt.Errorf("failed to add items to order: %w", err) + } + response = addResp + + ingredientTransactions, err = s.createIngredientTransactions(txCtx, orderID, response.AddedItems) + if err != nil { + return fmt.Errorf("failed to create ingredient transactions: %w", err) + } + + if len(ingredientTransactions) > 0 { + _, err = s.orderIngredientTransactionService.BulkCreateOrderIngredientTransactions(txCtx, ingredientTransactions) + if err != nil { + return fmt.Errorf("failed to bulk create ingredient transactions: %w", err) + } + } + + return nil + }) + if err != nil { - return nil, fmt.Errorf("failed to add items to order: %w", err) + return nil, err } return response, nil } func (s *OrderServiceImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) { - // Validate request if err := s.validateUpdateOrderRequest(req); err != nil { return nil, fmt.Errorf("validation error: %w", err) } - // Process order update response, err := s.orderProcessor.UpdateOrder(ctx, id, req) if err != nil { return nil, fmt.Errorf("failed to update order: %w", err) @@ -137,9 +235,7 @@ func (s *OrderServiceImpl) VoidOrder(ctx context.Context, req *models.VoidOrderR return fmt.Errorf("failed to void order: %w", err) } - // Release table if order is voided if err := s.handleTableReleaseOnVoid(ctx, req.OrderID); err != nil { - // Log the error but don't fail the void operation fmt.Printf("Warning: failed to handle table release for voided order %s: %v\n", req.OrderID, err) } @@ -547,3 +643,79 @@ func (s *OrderServiceImpl) handleTableReleaseOnVoid(ctx context.Context, orderID return nil } + +func (s *OrderServiceImpl) createOrderIngredientTransactions(ctx context.Context, order *models.Order, orderItems []*models.OrderItem) error { + for _, orderItem := range orderItems { + productIngredients, err := s.productIngredientRepo.GetByProductID(ctx, orderItem.ProductID, order.OrganizationID) + if err != nil { + return fmt.Errorf("failed to get product ingredients for product %s: %w", orderItem.ProductID, err) + } + + if len(productIngredients) == 0 { + continue // Skip if no ingredients + } + + // Calculate waste quantities using the utility function + transactions, err := s.calculateWasteQuantities(productIngredients, float64(orderItem.Quantity)) + if err != nil { + return fmt.Errorf("failed to calculate waste quantities for product %s: %w", orderItem.ProductID, err) + } + + // Set common fields for all transactions + for _, transaction := range transactions { + transaction.OrderID = order.ID + transaction.OrderItemID = &orderItem.ID + transaction.ProductID = orderItem.ProductID + transaction.ProductVariantID = orderItem.ProductVariantID + } + + // Bulk create transactions + if len(transactions) > 0 { + _, err := s.orderIngredientTransactionService.BulkCreateOrderIngredientTransactions(ctx, transactions) + if err != nil { + return fmt.Errorf("failed to create order ingredient transactions for product %s: %w", orderItem.ProductID, err) + } + } + } + + return nil +} + +// calculateWasteQuantities calculates gross, net, and waste quantities for product ingredients +func (s *OrderServiceImpl) calculateWasteQuantities(productIngredients []*entities.ProductIngredient, quantity float64) ([]*contract.CreateOrderIngredientTransactionRequest, error) { + if len(productIngredients) == 0 { + return []*contract.CreateOrderIngredientTransactionRequest{}, nil + } + + transactions := make([]*contract.CreateOrderIngredientTransactionRequest, 0, len(productIngredients)) + + for _, pi := range productIngredients { + // Calculate net quantity (actual quantity needed for the product) + netQty := pi.Quantity * quantity + + // Calculate gross quantity (including waste) + wasteMultiplier := 1 + (pi.WastePercentage / 100) + grossQty := netQty * wasteMultiplier + + // Calculate waste quantity + wasteQty := grossQty - netQty + + // Get unit name from ingredient + unitName := "unit" // default + if pi.Ingredient != nil && pi.Ingredient.Unit != nil { + unitName = pi.Ingredient.Unit.Name + } + + transaction := &contract.CreateOrderIngredientTransactionRequest{ + IngredientID: pi.IngredientID, + GrossQty: util.RoundToDecimalPlaces(grossQty, 3), + NetQty: util.RoundToDecimalPlaces(netQty, 3), + WasteQty: util.RoundToDecimalPlaces(wasteQty, 3), + Unit: unitName, + } + + transactions = append(transactions, transaction) + } + + return transactions, nil +} diff --git a/internal/transformer/ingredient_unit_converter_transformer.go b/internal/transformer/ingredient_unit_converter_transformer.go index 6789de6..e3512b0 100644 --- a/internal/transformer/ingredient_unit_converter_transformer.go +++ b/internal/transformer/ingredient_unit_converter_transformer.go @@ -152,3 +152,27 @@ func UnitModelResponseToResponse(model *models.UnitResponse) contract.UnitRespon } } +func IngredientUnitsModelResponseToResponse(model *models.IngredientUnitsResponse) contract.IngredientUnitsResponse { + if model == nil { + return contract.IngredientUnitsResponse{} + } + + response := contract.IngredientUnitsResponse{ + IngredientID: model.IngredientID, + IngredientName: model.IngredientName, + BaseUnitID: model.BaseUnitID, + BaseUnitName: model.BaseUnitName, + Units: make([]*contract.UnitResponse, 0), + } + + // Map units + for _, unit := range model.Units { + if unit != nil { + unitResp := UnitModelResponseToResponse(unit) + response.Units = append(response.Units, &unitResp) + } + } + + return response +} + diff --git a/internal/util/accounting_util.go b/internal/util/accounting_util.go index 9ec42ab..40baf9f 100644 --- a/internal/util/accounting_util.go +++ b/internal/util/accounting_util.go @@ -8,7 +8,6 @@ import ( "path/filepath" "apskel-pos-be/internal/entities" - "apskel-pos-be/internal/processor" "github.com/google/uuid" ) @@ -35,8 +34,8 @@ type AccountTemplate struct { } func CreateDefaultChartOfAccounts(ctx context.Context, - chartOfAccountProcessor processor.ChartOfAccountProcessor, - accountProcessor processor.AccountProcessor, + chartOfAccountProcessor interface{}, // Use interface{} to avoid import cycle + accountProcessor interface{}, // Use interface{} to avoid import cycle organizationID uuid.UUID, outletID *uuid.UUID) error { @@ -100,7 +99,7 @@ func loadDefaultChartOfAccountsTemplate() (*DefaultChartOfAccount, error) { return &template, nil } -func getChartOfAccountTypeMap(ctx context.Context, chartOfAccountProcessor processor.ChartOfAccountProcessor) (map[string]uuid.UUID, error) { +func getChartOfAccountTypeMap(ctx context.Context, chartOfAccountProcessor interface{}) (map[string]uuid.UUID, error) { // This is a placeholder - in a real implementation, you would call the processor // to get all chart of account types and create a map of code -> ID return map[string]uuid.UUID{ diff --git a/internal/util/waste_util.go b/internal/util/waste_util.go new file mode 100644 index 0000000..696fafc --- /dev/null +++ b/internal/util/waste_util.go @@ -0,0 +1,87 @@ +package util + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" + "fmt" + "time" + + "github.com/google/uuid" +) + +// CalculateWasteQuantities calculates gross, net, and waste quantities for product ingredients +func CalculateWasteQuantities(productIngredients []*entities.ProductIngredient, quantity float64) ([]*models.CreateOrderIngredientTransactionRequest, error) { + if len(productIngredients) == 0 { + return []*models.CreateOrderIngredientTransactionRequest{}, nil + } + + transactions := make([]*models.CreateOrderIngredientTransactionRequest, 0, len(productIngredients)) + + for _, pi := range productIngredients { + // Calculate net quantity (actual quantity needed for the product) + netQty := pi.Quantity * quantity + + // Calculate gross quantity (including waste) + wasteMultiplier := 1 + (pi.WastePercentage / 100) + grossQty := netQty * wasteMultiplier + + // Calculate waste quantity + wasteQty := grossQty - netQty + + // Get unit name from ingredient + unitName := "unit" // default + if pi.Ingredient != nil && pi.Ingredient.Unit != nil { + unitName = pi.Ingredient.Unit.Name + } + + transaction := &models.CreateOrderIngredientTransactionRequest{ + IngredientID: pi.IngredientID, + GrossQty: RoundToDecimalPlaces(grossQty, 3), + NetQty: RoundToDecimalPlaces(netQty, 3), + WasteQty: RoundToDecimalPlaces(wasteQty, 3), + Unit: unitName, + } + + transactions = append(transactions, transaction) + } + + return transactions, nil +} + +// CreateOrderIngredientTransactionsFromProduct creates order ingredient transactions for a product +func CreateOrderIngredientTransactionsFromProduct( + productID uuid.UUID, + productVariantID *uuid.UUID, + orderID uuid.UUID, + orderItemID *uuid.UUID, + quantity float64, + productIngredients []*entities.ProductIngredient, + organizationID, outletID, createdBy uuid.UUID, +) ([]*models.CreateOrderIngredientTransactionRequest, error) { + // Calculate waste quantities + wasteTransactions, err := CalculateWasteQuantities(productIngredients, quantity) + if err != nil { + return nil, fmt.Errorf("failed to calculate waste quantities: %w", err) + } + + // Set common fields for all transactions + for _, transaction := range wasteTransactions { + transaction.OrderID = orderID + transaction.OrderItemID = orderItemID + transaction.ProductID = productID + transaction.ProductVariantID = productVariantID + transaction.TransactionDate = &time.Time{} + *transaction.TransactionDate = time.Now() + } + + return wasteTransactions, nil +} + +// RoundToDecimalPlaces rounds a float64 to the specified number of decimal places +func RoundToDecimalPlaces(value float64, places int) float64 { + multiplier := 1.0 + for i := 0; i < places; i++ { + multiplier *= 10 + } + return float64(int(value*multiplier+0.5)) / multiplier +} diff --git a/internal/validator/order_ingredient_transaction_validator.go b/internal/validator/order_ingredient_transaction_validator.go new file mode 100644 index 0000000..3a9a97f --- /dev/null +++ b/internal/validator/order_ingredient_transaction_validator.go @@ -0,0 +1,120 @@ +package validator + +import ( + "apskel-pos-be/internal/models" + "fmt" + "strings" + + "github.com/go-playground/validator/v10" +) + +type OrderIngredientTransactionValidator interface { + ValidateCreateOrderIngredientTransactionRequest(req *models.CreateOrderIngredientTransactionRequest) error + ValidateUpdateOrderIngredientTransactionRequest(req *models.UpdateOrderIngredientTransactionRequest) error + ValidateListOrderIngredientTransactionsRequest(req *models.ListOrderIngredientTransactionsRequest) error +} + +type OrderIngredientTransactionValidatorImpl struct { + validator *validator.Validate +} + +func NewOrderIngredientTransactionValidator() OrderIngredientTransactionValidator { + v := validator.New() + return &OrderIngredientTransactionValidatorImpl{ + validator: v, + } +} + +func (v *OrderIngredientTransactionValidatorImpl) ValidateCreateOrderIngredientTransactionRequest(req *models.CreateOrderIngredientTransactionRequest) error { + if err := v.validator.Struct(req); err != nil { + var validationErrors []string + for _, err := range err.(validator.ValidationErrors) { + validationErrors = append(validationErrors, fmt.Sprintf("%s: %s", err.Field(), getValidationMessage(err))) + } + return fmt.Errorf("validation failed: %s", strings.Join(validationErrors, ", ")) + } + + // Custom validations + if req.GrossQty < req.NetQty { + return fmt.Errorf("gross quantity must be greater than or equal to net quantity") + } + + expectedWasteQty := req.GrossQty - req.NetQty + if req.WasteQty != expectedWasteQty { + return fmt.Errorf("waste quantity must equal gross quantity minus net quantity") + } + + return nil +} + +func (v *OrderIngredientTransactionValidatorImpl) ValidateUpdateOrderIngredientTransactionRequest(req *models.UpdateOrderIngredientTransactionRequest) error { + if err := v.validator.Struct(req); err != nil { + var validationErrors []string + for _, err := range err.(validator.ValidationErrors) { + validationErrors = append(validationErrors, fmt.Sprintf("%s: %s", err.Field(), getValidationMessage(err))) + } + return fmt.Errorf("validation failed: %s", strings.Join(validationErrors, ", ")) + } + + // Custom validations for partial updates + if req.GrossQty != nil && req.NetQty != nil { + if *req.GrossQty < *req.NetQty { + return fmt.Errorf("gross quantity must be greater than or equal to net quantity") + } + } + + if req.GrossQty != nil && req.NetQty != nil && req.WasteQty != nil { + expectedWasteQty := *req.GrossQty - *req.NetQty + if *req.WasteQty != expectedWasteQty { + return fmt.Errorf("waste quantity must equal gross quantity minus net quantity") + } + } + + return nil +} + +func (v *OrderIngredientTransactionValidatorImpl) ValidateListOrderIngredientTransactionsRequest(req *models.ListOrderIngredientTransactionsRequest) error { + if err := v.validator.Struct(req); err != nil { + var validationErrors []string + for _, err := range err.(validator.ValidationErrors) { + validationErrors = append(validationErrors, fmt.Sprintf("%s: %s", err.Field(), getValidationMessage(err))) + } + return fmt.Errorf("validation failed: %s", strings.Join(validationErrors, ", ")) + } + + // Custom validations + if req.StartDate != nil && req.EndDate != nil { + if req.StartDate.After(*req.EndDate) { + return fmt.Errorf("start date must be before end date") + } + } + + return nil +} + +func getValidationMessage(err validator.FieldError) string { + switch err.Tag() { + case "required": + return "is required" + case "min": + return fmt.Sprintf("must be at least %s", err.Param()) + case "max": + return fmt.Sprintf("must be at most %s", err.Param()) + case "gt": + return fmt.Sprintf("must be greater than %s", err.Param()) + case "gte": + return fmt.Sprintf("must be greater than or equal to %s", err.Param()) + case "lt": + return fmt.Sprintf("must be less than %s", err.Param()) + case "lte": + return fmt.Sprintf("must be less than or equal to %s", err.Param()) + case "email": + return "must be a valid email address" + case "uuid": + return "must be a valid UUID" + case "len": + return fmt.Sprintf("must be exactly %s characters long", err.Param()) + default: + return "is invalid" + } +} diff --git a/migrations/000045_add_waste_percentage_to_product_ingredients.down.sql b/migrations/000045_add_waste_percentage_to_product_ingredients.down.sql new file mode 100644 index 0000000..27d743c --- /dev/null +++ b/migrations/000045_add_waste_percentage_to_product_ingredients.down.sql @@ -0,0 +1,2 @@ +-- Remove waste_percentage column from product_ingredients table +ALTER TABLE product_ingredients DROP COLUMN waste_percentage; diff --git a/migrations/000045_add_waste_percentage_to_product_ingredients.up.sql b/migrations/000045_add_waste_percentage_to_product_ingredients.up.sql new file mode 100644 index 0000000..b26317e --- /dev/null +++ b/migrations/000045_add_waste_percentage_to_product_ingredients.up.sql @@ -0,0 +1,6 @@ +-- Add waste_percentage column to product_ingredients table +ALTER TABLE product_ingredients +ADD COLUMN waste_percentage DECIMAL(5,2) DEFAULT 0.00 CHECK (waste_percentage >= 0 AND waste_percentage <= 100); + +-- Add comment to explain the column +COMMENT ON COLUMN product_ingredients.waste_percentage IS 'Waste percentage for this ingredient (0-100). Used to calculate gross quantity needed including waste.'; diff --git a/migrations/000046_create_order_ingredients_transactions_table.down.sql b/migrations/000046_create_order_ingredients_transactions_table.down.sql new file mode 100644 index 0000000..97da523 --- /dev/null +++ b/migrations/000046_create_order_ingredients_transactions_table.down.sql @@ -0,0 +1,2 @@ +-- Drop order ingredients transactions table +DROP TABLE IF EXISTS order_ingredients_transactions; diff --git a/migrations/000046_create_order_ingredients_transactions_table.up.sql b/migrations/000046_create_order_ingredients_transactions_table.up.sql new file mode 100644 index 0000000..bba1fca --- /dev/null +++ b/migrations/000046_create_order_ingredients_transactions_table.up.sql @@ -0,0 +1,37 @@ +-- Order ingredients transactions table +CREATE TABLE order_ingredients_transactions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + outlet_id UUID REFERENCES outlets(id) ON DELETE CASCADE, + order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE, + order_item_id UUID REFERENCES order_items(id) ON DELETE CASCADE, + product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE, + product_variant_id UUID REFERENCES product_variants(id) ON DELETE CASCADE, + ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE, + gross_qty DECIMAL(12,3) NOT NULL CHECK (gross_qty > 0), + net_qty DECIMAL(12,3) NOT NULL CHECK (net_qty > 0), + waste_qty DECIMAL(12,3) NOT NULL CHECK (waste_qty >= 0), + unit VARCHAR(50) NOT NULL, + transaction_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + created_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Indexes +CREATE INDEX idx_order_ingredients_transactions_organization_id ON order_ingredients_transactions(organization_id); +CREATE INDEX idx_order_ingredients_transactions_outlet_id ON order_ingredients_transactions(outlet_id); +CREATE INDEX idx_order_ingredients_transactions_order_id ON order_ingredients_transactions(order_id); +CREATE INDEX idx_order_ingredients_transactions_order_item_id ON order_ingredients_transactions(order_item_id); +CREATE INDEX idx_order_ingredients_transactions_product_id ON order_ingredients_transactions(product_id); +CREATE INDEX idx_order_ingredients_transactions_product_variant_id ON order_ingredients_transactions(product_variant_id); +CREATE INDEX idx_order_ingredients_transactions_ingredient_id ON order_ingredients_transactions(ingredient_id); +CREATE INDEX idx_order_ingredients_transactions_transaction_date ON order_ingredients_transactions(transaction_date); +CREATE INDEX idx_order_ingredients_transactions_created_by ON order_ingredients_transactions(created_by); +CREATE INDEX idx_order_ingredients_transactions_created_at ON order_ingredients_transactions(created_at); + +-- Add comment to explain the table +COMMENT ON TABLE order_ingredients_transactions IS 'Tracks ingredient usage for orders including gross, net, and waste quantities'; +COMMENT ON COLUMN order_ingredients_transactions.gross_qty IS 'Total quantity needed including waste'; +COMMENT ON COLUMN order_ingredients_transactions.net_qty IS 'Actual quantity used in the product'; +COMMENT ON COLUMN order_ingredients_transactions.waste_qty IS 'Waste quantity (gross_qty - net_qty)';