add product ingredients

This commit is contained in:
Aditya Siregar 2025-09-12 15:37:19 +07:00
parent efe09c21e4
commit 3a04990ec8
35 changed files with 2572 additions and 357 deletions

View File

@ -92,6 +92,8 @@ func (a *App) Initialize(cfg *config.Config) error {
validators.chartOfAccountValidator, validators.chartOfAccountValidator,
services.accountService, services.accountService,
validators.accountValidator, validators.accountValidator,
*services.orderIngredientTransactionService,
validators.orderIngredientTransactionValidator,
) )
return nil return nil
@ -137,95 +139,103 @@ func (a *App) Shutdown() {
} }
type repositories struct { type repositories struct {
userRepo *repository.UserRepositoryImpl userRepo *repository.UserRepositoryImpl
organizationRepo *repository.OrganizationRepositoryImpl organizationRepo *repository.OrganizationRepositoryImpl
outletRepo *repository.OutletRepositoryImpl outletRepo *repository.OutletRepositoryImpl
outletSettingRepo *repository.OutletSettingRepositoryImpl outletSettingRepo *repository.OutletSettingRepositoryImpl
categoryRepo *repository.CategoryRepositoryImpl categoryRepo *repository.CategoryRepositoryImpl
productRepo *repository.ProductRepositoryImpl productRepo *repository.ProductRepositoryImpl
productVariantRepo *repository.ProductVariantRepositoryImpl productVariantRepo *repository.ProductVariantRepositoryImpl
inventoryRepo *repository.InventoryRepositoryImpl inventoryRepo *repository.InventoryRepositoryImpl
inventoryMovementRepo *repository.InventoryMovementRepositoryImpl inventoryMovementRepo *repository.InventoryMovementRepositoryImpl
orderRepo *repository.OrderRepositoryImpl orderRepo *repository.OrderRepositoryImpl
orderItemRepo *repository.OrderItemRepositoryImpl orderItemRepo *repository.OrderItemRepositoryImpl
paymentRepo *repository.PaymentRepositoryImpl paymentRepo *repository.PaymentRepositoryImpl
paymentOrderItemRepo *repository.PaymentOrderItemRepositoryImpl paymentOrderItemRepo *repository.PaymentOrderItemRepositoryImpl
paymentMethodRepo *repository.PaymentMethodRepositoryImpl paymentMethodRepo *repository.PaymentMethodRepositoryImpl
fileRepo *repository.FileRepositoryImpl fileRepo *repository.FileRepositoryImpl
customerRepo *repository.CustomerRepository customerRepo *repository.CustomerRepository
analyticsRepo *repository.AnalyticsRepositoryImpl analyticsRepo *repository.AnalyticsRepositoryImpl
tableRepo *repository.TableRepository tableRepo *repository.TableRepository
unitRepo *repository.UnitRepository unitRepo *repository.UnitRepository
ingredientRepo *repository.IngredientRepository ingredientRepo *repository.IngredientRepository
productRecipeRepo *repository.ProductRecipeRepository productRecipeRepo *repository.ProductRecipeRepository
vendorRepo *repository.VendorRepositoryImpl vendorRepo *repository.VendorRepositoryImpl
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl
chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
accountRepo *repository.AccountRepositoryImpl accountRepo *repository.AccountRepositoryImpl
txManager *repository.TxManager orderIngredientTransactionRepo *repository.OrderIngredientTransactionRepositoryImpl
productIngredientRepo *repository.ProductIngredientRepository
txManager *repository.TxManager
} }
func (a *App) initRepositories() *repositories { func (a *App) initRepositories() *repositories {
return &repositories{ return &repositories{
userRepo: repository.NewUserRepository(a.db), userRepo: repository.NewUserRepository(a.db),
organizationRepo: repository.NewOrganizationRepositoryImpl(a.db), organizationRepo: repository.NewOrganizationRepositoryImpl(a.db),
outletRepo: repository.NewOutletRepositoryImpl(a.db), outletRepo: repository.NewOutletRepositoryImpl(a.db),
outletSettingRepo: repository.NewOutletSettingRepositoryImpl(a.db), outletSettingRepo: repository.NewOutletSettingRepositoryImpl(a.db),
categoryRepo: repository.NewCategoryRepositoryImpl(a.db), categoryRepo: repository.NewCategoryRepositoryImpl(a.db),
productRepo: repository.NewProductRepositoryImpl(a.db), productRepo: repository.NewProductRepositoryImpl(a.db),
productVariantRepo: repository.NewProductVariantRepositoryImpl(a.db), productVariantRepo: repository.NewProductVariantRepositoryImpl(a.db),
inventoryRepo: repository.NewInventoryRepositoryImpl(a.db), inventoryRepo: repository.NewInventoryRepositoryImpl(a.db),
inventoryMovementRepo: repository.NewInventoryMovementRepositoryImpl(a.db), inventoryMovementRepo: repository.NewInventoryMovementRepositoryImpl(a.db),
orderRepo: repository.NewOrderRepositoryImpl(a.db), orderRepo: repository.NewOrderRepositoryImpl(a.db),
orderItemRepo: repository.NewOrderItemRepositoryImpl(a.db), orderItemRepo: repository.NewOrderItemRepositoryImpl(a.db),
paymentRepo: repository.NewPaymentRepositoryImpl(a.db), paymentRepo: repository.NewPaymentRepositoryImpl(a.db),
paymentOrderItemRepo: repository.NewPaymentOrderItemRepositoryImpl(a.db), paymentOrderItemRepo: repository.NewPaymentOrderItemRepositoryImpl(a.db),
paymentMethodRepo: repository.NewPaymentMethodRepositoryImpl(a.db), paymentMethodRepo: repository.NewPaymentMethodRepositoryImpl(a.db),
fileRepo: repository.NewFileRepositoryImpl(a.db), fileRepo: repository.NewFileRepositoryImpl(a.db),
customerRepo: repository.NewCustomerRepository(a.db), customerRepo: repository.NewCustomerRepository(a.db),
analyticsRepo: repository.NewAnalyticsRepositoryImpl(a.db), analyticsRepo: repository.NewAnalyticsRepositoryImpl(a.db),
tableRepo: repository.NewTableRepository(a.db), tableRepo: repository.NewTableRepository(a.db),
unitRepo: repository.NewUnitRepository(a.db), unitRepo: repository.NewUnitRepository(a.db),
ingredientRepo: repository.NewIngredientRepository(a.db), ingredientRepo: repository.NewIngredientRepository(a.db),
productRecipeRepo: repository.NewProductRecipeRepository(a.db), productRecipeRepo: repository.NewProductRecipeRepository(a.db),
vendorRepo: repository.NewVendorRepositoryImpl(a.db), vendorRepo: repository.NewVendorRepositoryImpl(a.db),
purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db), purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db),
unitConverterRepo: repository.NewIngredientUnitConverterRepositoryImpl(a.db).(*repository.IngredientUnitConverterRepositoryImpl), unitConverterRepo: repository.NewIngredientUnitConverterRepositoryImpl(a.db).(*repository.IngredientUnitConverterRepositoryImpl),
chartOfAccountTypeRepo: repository.NewChartOfAccountTypeRepositoryImpl(a.db), chartOfAccountTypeRepo: repository.NewChartOfAccountTypeRepositoryImpl(a.db),
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db), chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
accountRepo: repository.NewAccountRepositoryImpl(a.db), accountRepo: repository.NewAccountRepositoryImpl(a.db),
txManager: repository.NewTxManager(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 { type processors struct {
userProcessor *processor.UserProcessorImpl userProcessor *processor.UserProcessorImpl
organizationProcessor processor.OrganizationProcessor organizationProcessor processor.OrganizationProcessor
outletProcessor processor.OutletProcessor outletProcessor processor.OutletProcessor
outletSettingProcessor *processor.OutletSettingProcessorImpl outletSettingProcessor *processor.OutletSettingProcessorImpl
categoryProcessor processor.CategoryProcessor categoryProcessor processor.CategoryProcessor
productProcessor processor.ProductProcessor productProcessor processor.ProductProcessor
productVariantProcessor processor.ProductVariantProcessor productVariantProcessor processor.ProductVariantProcessor
inventoryProcessor processor.InventoryProcessor inventoryProcessor processor.InventoryProcessor
orderProcessor processor.OrderProcessor orderProcessor processor.OrderProcessor
paymentMethodProcessor processor.PaymentMethodProcessor paymentMethodProcessor processor.PaymentMethodProcessor
fileProcessor processor.FileProcessor fileProcessor processor.FileProcessor
customerProcessor *processor.CustomerProcessor customerProcessor *processor.CustomerProcessor
analyticsProcessor *processor.AnalyticsProcessorImpl analyticsProcessor *processor.AnalyticsProcessorImpl
tableProcessor *processor.TableProcessor tableProcessor *processor.TableProcessor
unitProcessor *processor.UnitProcessorImpl unitProcessor *processor.UnitProcessorImpl
ingredientProcessor *processor.IngredientProcessorImpl ingredientProcessor *processor.IngredientProcessorImpl
productRecipeProcessor *processor.ProductRecipeProcessorImpl productRecipeProcessor *processor.ProductRecipeProcessorImpl
vendorProcessor *processor.VendorProcessorImpl vendorProcessor *processor.VendorProcessorImpl
purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl
unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl
chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
accountProcessor *processor.AccountProcessorImpl accountProcessor *processor.AccountProcessorImpl
fileClient processor.FileClient orderIngredientTransactionProcessor *processor.OrderIngredientTransactionProcessorImpl
inventoryMovementService service.InventoryMovementService fileClient processor.FileClient
inventoryMovementService service.InventoryMovementService
} }
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors { 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) inventoryMovementService := service.NewInventoryMovementService(repos.inventoryMovementRepo, repos.ingredientRepo)
return &processors{ return &processors{
userProcessor: processor.NewUserProcessor(repos.userRepo, repos.organizationRepo, repos.outletRepo), userProcessor: processor.NewUserProcessor(repos.userRepo, repos.organizationRepo, repos.outletRepo),
organizationProcessor: processor.NewOrganizationProcessorImpl(repos.organizationRepo, repos.outletRepo, repos.userRepo), organizationProcessor: processor.NewOrganizationProcessorImpl(repos.organizationRepo, repos.outletRepo, repos.userRepo),
outletProcessor: processor.NewOutletProcessorImpl(repos.outletRepo), outletProcessor: processor.NewOutletProcessorImpl(repos.outletRepo),
outletSettingProcessor: processor.NewOutletSettingProcessorImpl(repos.outletSettingRepo, repos.outletRepo), outletSettingProcessor: processor.NewOutletSettingProcessorImpl(repos.outletSettingRepo, repos.outletRepo),
categoryProcessor: processor.NewCategoryProcessorImpl(repos.categoryRepo), categoryProcessor: processor.NewCategoryProcessorImpl(repos.categoryRepo),
productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo), productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo),
productVariantProcessor: processor.NewProductVariantProcessorImpl(repos.productVariantRepo, repos.productRepo), productVariantProcessor: processor.NewProductVariantProcessorImpl(repos.productVariantRepo, repos.productRepo),
inventoryProcessor: processor.NewInventoryProcessorImpl(repos.inventoryRepo, repos.productRepo, repos.outletRepo, repos.ingredientRepo, repos.inventoryMovementRepo), 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), 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), paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo),
fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient), fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient),
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo), customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo), analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo),
tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo), tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo),
unitProcessor: processor.NewUnitProcessor(repos.unitRepo), unitProcessor: processor.NewUnitProcessor(repos.unitRepo),
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo), ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo),
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo), productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo), vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo), 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), unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo),
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo), chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo), chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo),
accountProcessor: processor.NewAccountProcessorImpl(repos.accountRepo, repos.chartOfAccountRepo), accountProcessor: processor.NewAccountProcessorImpl(repos.accountRepo, repos.chartOfAccountRepo),
fileClient: fileClient, orderIngredientTransactionProcessor: processor.NewOrderIngredientTransactionProcessorImpl(repos.orderIngredientTransactionRepo, repos.productIngredientRepo, repos.ingredientRepo, repos.unitRepo).(*processor.OrderIngredientTransactionProcessorImpl),
inventoryMovementService: inventoryMovementService, fileClient: fileClient,
inventoryMovementService: inventoryMovementService,
} }
} }
type services struct { type services struct {
userService *service.UserServiceImpl userService *service.UserServiceImpl
authService service.AuthService authService service.AuthService
organizationService service.OrganizationService organizationService service.OrganizationService
outletService service.OutletService outletService service.OutletService
outletSettingService service.OutletSettingService outletSettingService service.OutletSettingService
categoryService service.CategoryService categoryService service.CategoryService
productService service.ProductService productService service.ProductService
productVariantService service.ProductVariantService productVariantService service.ProductVariantService
inventoryService service.InventoryService inventoryService service.InventoryService
orderService service.OrderService orderService service.OrderService
paymentMethodService service.PaymentMethodService paymentMethodService service.PaymentMethodService
fileService service.FileService fileService service.FileService
customerService service.CustomerService customerService service.CustomerService
analyticsService *service.AnalyticsServiceImpl analyticsService *service.AnalyticsServiceImpl
reportService service.ReportService reportService service.ReportService
tableService *service.TableServiceImpl tableService *service.TableServiceImpl
unitService *service.UnitServiceImpl unitService *service.UnitServiceImpl
ingredientService *service.IngredientServiceImpl ingredientService *service.IngredientServiceImpl
productRecipeService *service.ProductRecipeServiceImpl productRecipeService *service.ProductRecipeServiceImpl
vendorService *service.VendorServiceImpl vendorService *service.VendorServiceImpl
purchaseOrderService *service.PurchaseOrderServiceImpl purchaseOrderService *service.PurchaseOrderServiceImpl
unitConverterService *service.IngredientUnitConverterServiceImpl unitConverterService *service.IngredientUnitConverterServiceImpl
chartOfAccountTypeService service.ChartOfAccountTypeService chartOfAccountTypeService service.ChartOfAccountTypeService
chartOfAccountService service.ChartOfAccountService chartOfAccountService service.ChartOfAccountService
accountService service.AccountService accountService service.AccountService
orderIngredientTransactionService *service.OrderIngredientTransactionService
} }
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services { 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) productService := service.NewProductService(processors.productProcessor)
productVariantService := service.NewProductVariantService(processors.productVariantProcessor) productVariantService := service.NewProductVariantService(processors.productVariantProcessor)
inventoryService := service.NewInventoryService(processors.inventoryProcessor) 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) paymentMethodService := service.NewPaymentMethodService(processors.paymentMethodProcessor)
fileService := service.NewFileServiceImpl(processors.fileProcessor) fileService := service.NewFileServiceImpl(processors.fileProcessor)
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor) 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) chartOfAccountTypeService := service.NewChartOfAccountTypeService(processors.chartOfAccountTypeProcessor)
chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor) chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor)
accountService := service.NewAccountService(processors.accountProcessor) 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{ return &services{
userService: service.NewUserService(processors.userProcessor), userService: service.NewUserService(processors.userProcessor),
authService: authService, authService: authService,
organizationService: organizationService, organizationService: organizationService,
outletService: outletService, outletService: outletService,
outletSettingService: outletSettingService, outletSettingService: outletSettingService,
categoryService: categoryService, categoryService: categoryService,
productService: productService, productService: productService,
productVariantService: productVariantService, productVariantService: productVariantService,
inventoryService: inventoryService, inventoryService: inventoryService,
orderService: orderService, orderService: orderService,
paymentMethodService: paymentMethodService, paymentMethodService: paymentMethodService,
fileService: fileService, fileService: fileService,
customerService: customerService, customerService: customerService,
analyticsService: analyticsService, analyticsService: analyticsService,
reportService: reportService, reportService: reportService,
tableService: tableService, tableService: tableService,
unitService: unitService, unitService: unitService,
ingredientService: ingredientService, ingredientService: ingredientService,
productRecipeService: productRecipeService, productRecipeService: productRecipeService,
vendorService: vendorService, vendorService: vendorService,
purchaseOrderService: purchaseOrderService, purchaseOrderService: purchaseOrderService,
unitConverterService: unitConverterService, unitConverterService: unitConverterService,
chartOfAccountTypeService: chartOfAccountTypeService, chartOfAccountTypeService: chartOfAccountTypeService,
chartOfAccountService: chartOfAccountService, chartOfAccountService: chartOfAccountService,
accountService: accountService, accountService: accountService,
orderIngredientTransactionService: orderIngredientTransactionService,
} }
} }
@ -357,45 +374,47 @@ func (a *App) initMiddleware(services *services) *middlewares {
} }
type validators struct { type validators struct {
userValidator *validator.UserValidatorImpl userValidator *validator.UserValidatorImpl
organizationValidator validator.OrganizationValidator organizationValidator validator.OrganizationValidator
outletValidator validator.OutletValidator outletValidator validator.OutletValidator
categoryValidator validator.CategoryValidator categoryValidator validator.CategoryValidator
productValidator validator.ProductValidator productValidator validator.ProductValidator
productVariantValidator validator.ProductVariantValidator productVariantValidator validator.ProductVariantValidator
inventoryValidator validator.InventoryValidator inventoryValidator validator.InventoryValidator
orderValidator validator.OrderValidator orderValidator validator.OrderValidator
paymentMethodValidator validator.PaymentMethodValidator paymentMethodValidator validator.PaymentMethodValidator
fileValidator validator.FileValidator fileValidator validator.FileValidator
customerValidator validator.CustomerValidator customerValidator validator.CustomerValidator
tableValidator *validator.TableValidator tableValidator *validator.TableValidator
vendorValidator *validator.VendorValidatorImpl vendorValidator *validator.VendorValidatorImpl
purchaseOrderValidator *validator.PurchaseOrderValidatorImpl purchaseOrderValidator *validator.PurchaseOrderValidatorImpl
unitConverterValidator *validator.IngredientUnitConverterValidatorImpl unitConverterValidator *validator.IngredientUnitConverterValidatorImpl
chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
accountValidator *validator.AccountValidatorImpl accountValidator *validator.AccountValidatorImpl
orderIngredientTransactionValidator *validator.OrderIngredientTransactionValidatorImpl
} }
func (a *App) initValidators() *validators { func (a *App) initValidators() *validators {
return &validators{ return &validators{
userValidator: validator.NewUserValidator(), userValidator: validator.NewUserValidator(),
organizationValidator: validator.NewOrganizationValidator(), organizationValidator: validator.NewOrganizationValidator(),
outletValidator: validator.NewOutletValidator(), outletValidator: validator.NewOutletValidator(),
categoryValidator: validator.NewCategoryValidator(), categoryValidator: validator.NewCategoryValidator(),
productValidator: validator.NewProductValidator(), productValidator: validator.NewProductValidator(),
productVariantValidator: validator.NewProductVariantValidator(), productVariantValidator: validator.NewProductVariantValidator(),
inventoryValidator: validator.NewInventoryValidator(), inventoryValidator: validator.NewInventoryValidator(),
orderValidator: validator.NewOrderValidator(), orderValidator: validator.NewOrderValidator(),
paymentMethodValidator: validator.NewPaymentMethodValidator(), paymentMethodValidator: validator.NewPaymentMethodValidator(),
fileValidator: validator.NewFileValidatorImpl(), fileValidator: validator.NewFileValidatorImpl(),
customerValidator: validator.NewCustomerValidator(), customerValidator: validator.NewCustomerValidator(),
tableValidator: validator.NewTableValidator(), tableValidator: validator.NewTableValidator(),
vendorValidator: validator.NewVendorValidator(), vendorValidator: validator.NewVendorValidator(),
purchaseOrderValidator: validator.NewPurchaseOrderValidator(), purchaseOrderValidator: validator.NewPurchaseOrderValidator(),
unitConverterValidator: validator.NewIngredientUnitConverterValidator().(*validator.IngredientUnitConverterValidatorImpl), unitConverterValidator: validator.NewIngredientUnitConverterValidator().(*validator.IngredientUnitConverterValidatorImpl),
chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl), chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl),
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl), chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
accountValidator: validator.NewAccountValidator().(*validator.AccountValidatorImpl), accountValidator: validator.NewAccountValidator().(*validator.AccountValidatorImpl),
orderIngredientTransactionValidator: validator.NewOrderIngredientTransactionValidator().(*validator.OrderIngredientTransactionValidatorImpl),
} }
} }

View File

@ -74,3 +74,11 @@ type ListIngredientUnitConvertersResponse struct {
TotalPages int `json:"total_pages"` 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"`
}

View File

@ -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)
}

View File

@ -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"`
}

View File

@ -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"
}

View File

@ -7,14 +7,15 @@ import (
) )
type ProductIngredient struct { type ProductIngredient struct {
ID uuid.UUID `json:"id" db:"id"` ID uuid.UUID `json:"id" db:"id"`
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"` OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"` OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
ProductID uuid.UUID `json:"product_id" db:"product_id"` ProductID uuid.UUID `json:"product_id" db:"product_id"`
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"` IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
Quantity float64 `json:"quantity" db:"quantity"` Quantity float64 `json:"quantity" db:"quantity"`
CreatedAt time.Time `json:"created_at" db:"created_at"` WastePercentage float64 `json:"waste_percentage" db:"waste_percentage"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"` CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
// Relations // Relations
Product *Product `json:"product,omitempty"` Product *Product `json:"product,omitempty"`

View File

@ -254,3 +254,25 @@ func (h *IngredientUnitConverterHandler) ConvertUnit(c *gin.Context) {
util.HandleResponse(c.Writer, c.Request, converterResponse, "IngredientUnitConverterHandler::ConvertUnit") 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")
}

View File

@ -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")
}

View File

@ -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)
}

View File

@ -166,3 +166,110 @@ func ModelToContractAccountResponse(resp *models.AccountResponse) *contract.Acco
return contractResp 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
}

View File

@ -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
}

View File

@ -11,16 +11,17 @@ func MapProductIngredientEntityToModel(entity *entities.ProductIngredient) *mode
} }
return &models.ProductIngredient{ return &models.ProductIngredient{
ID: entity.ID, ID: entity.ID,
OrganizationID: entity.OrganizationID, OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID, OutletID: entity.OutletID,
ProductID: entity.ProductID, ProductID: entity.ProductID,
IngredientID: entity.IngredientID, IngredientID: entity.IngredientID,
Quantity: entity.Quantity, Quantity: entity.Quantity,
CreatedAt: entity.CreatedAt, WastePercentage: entity.WastePercentage,
UpdatedAt: entity.UpdatedAt, CreatedAt: entity.CreatedAt,
Product: ProductEntityToModel(entity.Product), UpdatedAt: entity.UpdatedAt,
Ingredient: MapIngredientEntityToModel(entity.Ingredient), Product: ProductEntityToModel(entity.Product),
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
} }
} }
@ -30,16 +31,17 @@ func MapProductIngredientModelToEntity(model *models.ProductIngredient) *entitie
} }
return &entities.ProductIngredient{ return &entities.ProductIngredient{
ID: model.ID, ID: model.ID,
OrganizationID: model.OrganizationID, OrganizationID: model.OrganizationID,
OutletID: model.OutletID, OutletID: model.OutletID,
ProductID: model.ProductID, ProductID: model.ProductID,
IngredientID: model.IngredientID, IngredientID: model.IngredientID,
Quantity: model.Quantity, Quantity: model.Quantity,
CreatedAt: model.CreatedAt, WastePercentage: model.WastePercentage,
UpdatedAt: model.UpdatedAt, CreatedAt: model.CreatedAt,
Product: ProductModelToEntity(model.Product), UpdatedAt: model.UpdatedAt,
Ingredient: MapIngredientModelToEntity(model.Ingredient), Product: ProductModelToEntity(model.Product),
Ingredient: MapIngredientModelToEntity(model.Ingredient),
} }
} }

View File

@ -94,3 +94,11 @@ type ListIngredientUnitConvertersResponse struct {
TotalPages int `json:"total_pages"` 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"`
}

View File

@ -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"`
}

View File

@ -7,14 +7,15 @@ import (
) )
type ProductIngredient struct { type ProductIngredient struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"` OutletID *uuid.UUID `json:"outlet_id"`
ProductID uuid.UUID `json:"product_id"` ProductID uuid.UUID `json:"product_id"`
IngredientID uuid.UUID `json:"ingredient_id"` IngredientID uuid.UUID `json:"ingredient_id"`
Quantity float64 `json:"quantity"` Quantity float64 `json:"quantity"`
CreatedAt time.Time `json:"created_at"` WastePercentage float64 `json:"waste_percentage"`
UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Relations // Relations
Product *Product `json:"product,omitempty"` Product *Product `json:"product,omitempty"`
@ -22,26 +23,29 @@ type ProductIngredient struct {
} }
type CreateProductIngredientRequest struct { type CreateProductIngredientRequest struct {
OutletID *uuid.UUID `json:"outlet_id"` OutletID *uuid.UUID `json:"outlet_id"`
ProductID uuid.UUID `json:"product_id" validate:"required"` ProductID uuid.UUID `json:"product_id" validate:"required"`
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"` IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
Quantity float64 `json:"quantity" validate:"required,gt=0"` Quantity float64 `json:"quantity" validate:"required,gt=0"`
WastePercentage float64 `json:"waste_percentage" validate:"min=0,max=100"`
} }
type UpdateProductIngredientRequest struct { type UpdateProductIngredientRequest struct {
OutletID *uuid.UUID `json:"outlet_id"` OutletID *uuid.UUID `json:"outlet_id"`
Quantity float64 `json:"quantity" validate:"required,gt=0"` Quantity float64 `json:"quantity" validate:"required,gt=0"`
WastePercentage float64 `json:"waste_percentage" validate:"min=0,max=100"`
} }
type ProductIngredientResponse struct { type ProductIngredientResponse struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"` OutletID *uuid.UUID `json:"outlet_id"`
ProductID uuid.UUID `json:"product_id"` ProductID uuid.UUID `json:"product_id"`
IngredientID uuid.UUID `json:"ingredient_id"` IngredientID uuid.UUID `json:"ingredient_id"`
Quantity float64 `json:"quantity"` Quantity float64 `json:"quantity"`
CreatedAt time.Time `json:"created_at"` WastePercentage float64 `json:"waste_percentage"`
UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Relations // Relations
Product *Product `json:"product,omitempty"` Product *Product `json:"product,omitempty"`

View File

@ -12,7 +12,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
type AccountProcessor interface { type AccountProcessor interface {
CreateAccount(ctx context.Context, req *models.CreateAccountRequest) (*models.AccountResponse, error) CreateAccount(ctx context.Context, req *models.CreateAccountRequest) (*models.AccountResponse, error)
GetAccountByID(ctx context.Context, id uuid.UUID) (*models.AccountResponse, error) GetAccountByID(ctx context.Context, id uuid.UUID) (*models.AccountResponse, error)

View File

@ -10,7 +10,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
type ChartOfAccountTypeProcessor interface { type ChartOfAccountTypeProcessor interface {
CreateChartOfAccountType(ctx context.Context, req *models.CreateChartOfAccountTypeRequest) (*models.ChartOfAccountTypeResponse, error) CreateChartOfAccountType(ctx context.Context, req *models.CreateChartOfAccountTypeRequest) (*models.ChartOfAccountTypeResponse, error)
GetChartOfAccountTypeByID(ctx context.Context, id uuid.UUID) (*models.ChartOfAccountTypeResponse, error) GetChartOfAccountTypeByID(ctx context.Context, id uuid.UUID) (*models.ChartOfAccountTypeResponse, error)

View File

@ -1,17 +1 @@
package processor 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
}

View File

@ -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) 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) GetConvertersForIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*models.IngredientUnitConverterResponse, error)
ConvertUnit(ctx context.Context, organizationID uuid.UUID, req *models.ConvertUnitRequest) (*models.ConvertUnitResponse, 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 { type IngredientUnitConverterProcessorImpl struct {
@ -257,3 +258,64 @@ func (p *IngredientUnitConverterProcessorImpl) ConvertUnit(ctx context.Context,
return response, nil 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
}

View File

@ -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
}

View File

@ -42,3 +42,43 @@ type AccountRepository interface {
UpdateBalance(ctx context.Context, id uuid.UUID, amount float64) error UpdateBalance(ctx context.Context, id uuid.UUID, amount float64) error
GetBalance(ctx context.Context, id uuid.UUID) (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
}

View File

@ -1,16 +1 @@
package processor 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
}

View File

@ -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
}

View File

@ -12,34 +12,35 @@ import (
) )
type Router struct { type Router struct {
config *config.Config config *config.Config
healthHandler *handler.HealthHandler healthHandler *handler.HealthHandler
authHandler *handler.AuthHandler authHandler *handler.AuthHandler
userHandler *handler.UserHandler userHandler *handler.UserHandler
organizationHandler *handler.OrganizationHandler organizationHandler *handler.OrganizationHandler
outletHandler *handler.OutletHandler outletHandler *handler.OutletHandler
outletSettingHandler *handler.OutletSettingHandlerImpl outletSettingHandler *handler.OutletSettingHandlerImpl
categoryHandler *handler.CategoryHandler categoryHandler *handler.CategoryHandler
productHandler *handler.ProductHandler productHandler *handler.ProductHandler
productVariantHandler *handler.ProductVariantHandler productVariantHandler *handler.ProductVariantHandler
inventoryHandler *handler.InventoryHandler inventoryHandler *handler.InventoryHandler
orderHandler *handler.OrderHandler orderHandler *handler.OrderHandler
fileHandler *handler.FileHandler fileHandler *handler.FileHandler
customerHandler *handler.CustomerHandler customerHandler *handler.CustomerHandler
paymentMethodHandler *handler.PaymentMethodHandler paymentMethodHandler *handler.PaymentMethodHandler
analyticsHandler *handler.AnalyticsHandler analyticsHandler *handler.AnalyticsHandler
reportHandler *handler.ReportHandler reportHandler *handler.ReportHandler
tableHandler *handler.TableHandler tableHandler *handler.TableHandler
unitHandler *handler.UnitHandler unitHandler *handler.UnitHandler
ingredientHandler *handler.IngredientHandler ingredientHandler *handler.IngredientHandler
productRecipeHandler *handler.ProductRecipeHandler productRecipeHandler *handler.ProductRecipeHandler
vendorHandler *handler.VendorHandler vendorHandler *handler.VendorHandler
purchaseOrderHandler *handler.PurchaseOrderHandler purchaseOrderHandler *handler.PurchaseOrderHandler
unitConverterHandler *handler.IngredientUnitConverterHandler unitConverterHandler *handler.IngredientUnitConverterHandler
chartOfAccountTypeHandler *handler.ChartOfAccountTypeHandler chartOfAccountTypeHandler *handler.ChartOfAccountTypeHandler
chartOfAccountHandler *handler.ChartOfAccountHandler chartOfAccountHandler *handler.ChartOfAccountHandler
accountHandler *handler.AccountHandler accountHandler *handler.AccountHandler
authMiddleware *middleware.AuthMiddleware orderIngredientTransactionHandler *handler.OrderIngredientTransactionHandler
authMiddleware *middleware.AuthMiddleware
} }
func NewRouter(cfg *config.Config, func NewRouter(cfg *config.Config,
@ -87,36 +88,39 @@ func NewRouter(cfg *config.Config,
chartOfAccountService service.ChartOfAccountService, chartOfAccountService service.ChartOfAccountService,
chartOfAccountValidator validator.ChartOfAccountValidator, chartOfAccountValidator validator.ChartOfAccountValidator,
accountService service.AccountService, accountService service.AccountService,
accountValidator validator.AccountValidator) *Router { accountValidator validator.AccountValidator,
orderIngredientTransactionService service.OrderIngredientTransactionService,
orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator) *Router {
return &Router{ return &Router{
config: cfg, config: cfg,
healthHandler: healthHandler, healthHandler: healthHandler,
authHandler: handler.NewAuthHandler(authService), authHandler: handler.NewAuthHandler(authService),
userHandler: handler.NewUserHandler(userService, userValidator), userHandler: handler.NewUserHandler(userService, userValidator),
organizationHandler: handler.NewOrganizationHandler(organizationService, organizationValidator), organizationHandler: handler.NewOrganizationHandler(organizationService, organizationValidator),
outletHandler: handler.NewOutletHandler(outletService, outletValidator), outletHandler: handler.NewOutletHandler(outletService, outletValidator),
outletSettingHandler: handler.NewOutletSettingHandlerImpl(outletSettingService), outletSettingHandler: handler.NewOutletSettingHandlerImpl(outletSettingService),
categoryHandler: handler.NewCategoryHandler(categoryService, categoryValidator), categoryHandler: handler.NewCategoryHandler(categoryService, categoryValidator),
productHandler: handler.NewProductHandler(productService, productValidator), productHandler: handler.NewProductHandler(productService, productValidator),
inventoryHandler: handler.NewInventoryHandler(inventoryService, inventoryValidator), inventoryHandler: handler.NewInventoryHandler(inventoryService, inventoryValidator),
orderHandler: handler.NewOrderHandler(orderService, orderValidator, transformer.NewTransformer()), orderHandler: handler.NewOrderHandler(orderService, orderValidator, transformer.NewTransformer()),
fileHandler: handler.NewFileHandler(fileService, fileValidator, transformer.NewTransformer()), fileHandler: handler.NewFileHandler(fileService, fileValidator, transformer.NewTransformer()),
customerHandler: handler.NewCustomerHandler(customerService, customerValidator), customerHandler: handler.NewCustomerHandler(customerService, customerValidator),
paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator), paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator),
analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()), analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()),
reportHandler: handler.NewReportHandler(reportService, userService), reportHandler: handler.NewReportHandler(reportService, userService),
tableHandler: handler.NewTableHandler(tableService, tableValidator), tableHandler: handler.NewTableHandler(tableService, tableValidator),
unitHandler: handler.NewUnitHandler(unitService), unitHandler: handler.NewUnitHandler(unitService),
ingredientHandler: handler.NewIngredientHandler(ingredientService), ingredientHandler: handler.NewIngredientHandler(ingredientService),
productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService), productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService),
vendorHandler: handler.NewVendorHandler(vendorService, vendorValidator), vendorHandler: handler.NewVendorHandler(vendorService, vendorValidator),
purchaseOrderHandler: handler.NewPurchaseOrderHandler(purchaseOrderService, purchaseOrderValidator), purchaseOrderHandler: handler.NewPurchaseOrderHandler(purchaseOrderService, purchaseOrderValidator),
unitConverterHandler: handler.NewIngredientUnitConverterHandler(unitConverterService, unitConverterValidator), unitConverterHandler: handler.NewIngredientUnitConverterHandler(unitConverterService, unitConverterValidator),
chartOfAccountTypeHandler: handler.NewChartOfAccountTypeHandler(chartOfAccountTypeService, chartOfAccountTypeValidator), chartOfAccountTypeHandler: handler.NewChartOfAccountTypeHandler(chartOfAccountTypeService, chartOfAccountTypeValidator),
chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator), chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator),
accountHandler: handler.NewAccountHandler(accountService, accountValidator), accountHandler: handler.NewAccountHandler(accountService, accountValidator),
authMiddleware: authMiddleware, orderIngredientTransactionHandler: handler.NewOrderIngredientTransactionHandler(&orderIngredientTransactionService, orderIngredientTransactionValidator),
authMiddleware: authMiddleware,
} }
} }
@ -351,6 +355,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
unitConverters.POST("", r.unitConverterHandler.CreateIngredientUnitConverter) unitConverters.POST("", r.unitConverterHandler.CreateIngredientUnitConverter)
unitConverters.GET("", r.unitConverterHandler.ListIngredientUnitConverters) unitConverters.GET("", r.unitConverterHandler.ListIngredientUnitConverters)
unitConverters.GET("/ingredient/:ingredient_id", r.unitConverterHandler.GetConvertersForIngredient) unitConverters.GET("/ingredient/:ingredient_id", r.unitConverterHandler.GetConvertersForIngredient)
unitConverters.GET("/ingredient/:ingredient_id/units", r.unitConverterHandler.GetUnitsByIngredientID)
unitConverters.POST("/convert", r.unitConverterHandler.ConvertUnit) unitConverters.POST("/convert", r.unitConverterHandler.ConvertUnit)
unitConverters.GET("/:id", r.unitConverterHandler.GetIngredientUnitConverter) unitConverters.GET("/:id", r.unitConverterHandler.GetIngredientUnitConverter)
unitConverters.PUT("/:id", r.unitConverterHandler.UpdateIngredientUnitConverter) unitConverters.PUT("/:id", r.unitConverterHandler.UpdateIngredientUnitConverter)
@ -406,6 +411,21 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
accounts.GET("/:id/balance", r.accountHandler.GetAccountBalance) 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 := protected.Group("/outlets")
outlets.Use(r.authMiddleware.RequireAdminOrManager()) outlets.Use(r.authMiddleware.RequireAdminOrManager())
{ {

View File

@ -19,6 +19,7 @@ type IngredientUnitConverterService interface {
ListIngredientUnitConverters(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListIngredientUnitConvertersRequest) *contract.Response ListIngredientUnitConverters(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListIngredientUnitConvertersRequest) *contract.Response
GetConvertersForIngredient(ctx context.Context, apctx *appcontext.ContextInfo, ingredientID uuid.UUID) *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 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 { type IngredientUnitConverterServiceImpl struct {
@ -149,3 +150,14 @@ func (s *IngredientUnitConverterServiceImpl) ConvertUnit(ctx context.Context, ap
return contract.BuildSuccessResponse(contractResponse) 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)
}

View File

@ -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
}

View File

@ -1,13 +1,17 @@
package service package service
import ( import (
"apskel-pos-be/internal/appcontext"
"context" "context"
"fmt" "fmt"
"time" "time"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models" "apskel-pos-be/internal/models"
"apskel-pos-be/internal/processor" "apskel-pos-be/internal/processor"
"apskel-pos-be/internal/repository" "apskel-pos-be/internal/repository"
"apskel-pos-be/internal/util"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -27,14 +31,22 @@ type OrderService interface {
} }
type OrderServiceImpl struct { type OrderServiceImpl struct {
orderProcessor processor.OrderProcessor orderProcessor processor.OrderProcessor
tableRepo repository.TableRepositoryInterface 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{ return &OrderServiceImpl{
orderProcessor: orderProcessor, orderProcessor: orderProcessor,
tableRepo: tableRepo, 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) var response *models.OrderResponse
if err != nil { var ingredientTransactions []*contract.CreateOrderIngredientTransactionRequest
return nil, fmt.Errorf("failed to create order: %w", err)
}
if req.TableID != nil { // Use transaction to ensure atomicity
if err := s.occupyTableWithOrder(ctx, *req.TableID, response.ID); err != nil { err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
fmt.Printf("Warning: failed to occupy table %s with order %s: %v\n", *req.TableID, response.ID, err) // 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 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) { func (s *OrderServiceImpl) AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) {
// Validate inputs
if orderID == uuid.Nil { if orderID == uuid.Nil {
return nil, fmt.Errorf("invalid order ID") return nil, fmt.Errorf("invalid order ID")
} }
// Validate request
if err := s.validateAddToOrderRequest(req); err != nil { if err := s.validateAddToOrderRequest(req); err != nil {
return nil, fmt.Errorf("validation error: %w", err) return nil, fmt.Errorf("validation error: %w", err)
} }
// Process adding items to order var response *models.AddToOrderResponse
response, err := s.orderProcessor.AddToOrder(ctx, orderID, req) 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 { if err != nil {
return nil, fmt.Errorf("failed to add items to order: %w", err) return nil, err
} }
return response, nil return response, nil
} }
func (s *OrderServiceImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) { 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 { if err := s.validateUpdateOrderRequest(req); err != nil {
return nil, fmt.Errorf("validation error: %w", err) return nil, fmt.Errorf("validation error: %w", err)
} }
// Process order update
response, err := s.orderProcessor.UpdateOrder(ctx, id, req) response, err := s.orderProcessor.UpdateOrder(ctx, id, req)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to update order: %w", err) 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) return fmt.Errorf("failed to void order: %w", err)
} }
// Release table if order is voided
if err := s.handleTableReleaseOnVoid(ctx, req.OrderID); err != nil { 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) 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 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
}

View File

@ -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
}

View File

@ -8,7 +8,6 @@ import (
"path/filepath" "path/filepath"
"apskel-pos-be/internal/entities" "apskel-pos-be/internal/entities"
"apskel-pos-be/internal/processor"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -35,8 +34,8 @@ type AccountTemplate struct {
} }
func CreateDefaultChartOfAccounts(ctx context.Context, func CreateDefaultChartOfAccounts(ctx context.Context,
chartOfAccountProcessor processor.ChartOfAccountProcessor, chartOfAccountProcessor interface{}, // Use interface{} to avoid import cycle
accountProcessor processor.AccountProcessor, accountProcessor interface{}, // Use interface{} to avoid import cycle
organizationID uuid.UUID, organizationID uuid.UUID,
outletID *uuid.UUID) error { outletID *uuid.UUID) error {
@ -100,7 +99,7 @@ func loadDefaultChartOfAccountsTemplate() (*DefaultChartOfAccount, error) {
return &template, nil 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 // 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 // to get all chart of account types and create a map of code -> ID
return map[string]uuid.UUID{ return map[string]uuid.UUID{

View File

@ -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
}

View File

@ -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"
}
}

View File

@ -0,0 +1,2 @@
-- Remove waste_percentage column from product_ingredients table
ALTER TABLE product_ingredients DROP COLUMN waste_percentage;

View File

@ -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.';

View File

@ -0,0 +1,2 @@
-- Drop order ingredients transactions table
DROP TABLE IF EXISTS order_ingredients_transactions;

View File

@ -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)';