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

View File

@ -74,3 +74,11 @@ type ListIngredientUnitConvertersResponse struct {
TotalPages int `json:"total_pages"`
}
type IngredientUnitsResponse struct {
IngredientID uuid.UUID `json:"ingredient_id"`
IngredientName string `json:"ingredient_name"`
BaseUnitID uuid.UUID `json:"base_unit_id"`
BaseUnitName string `json:"base_unit_name"`
Units []*UnitResponse `json:"units"`
}

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 {
ID uuid.UUID `json:"id" db:"id"`
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
ProductID uuid.UUID `json:"product_id" db:"product_id"`
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
Quantity float64 `json:"quantity" db:"quantity"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
ID uuid.UUID `json:"id" db:"id"`
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
ProductID uuid.UUID `json:"product_id" db:"product_id"`
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
Quantity float64 `json:"quantity" db:"quantity"`
WastePercentage float64 `json:"waste_percentage" db:"waste_percentage"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
// Relations
Product *Product `json:"product,omitempty"`

View File

@ -254,3 +254,25 @@ func (h *IngredientUnitConverterHandler) ConvertUnit(c *gin.Context) {
util.HandleResponse(c.Writer, c.Request, converterResponse, "IngredientUnitConverterHandler::ConvertUnit")
}
func (h *IngredientUnitConverterHandler) GetUnitsByIngredientID(c *gin.Context) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
ingredientIDStr := c.Param("ingredient_id")
ingredientID, err := uuid.Parse(ingredientIDStr)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("IngredientUnitConverterHandler::GetUnitsByIngredientID -> Invalid ingredient ID")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientUnitConverterHandler::GetUnitsByIngredientID")
return
}
unitsResponse := h.converterService.GetUnitsByIngredientID(ctx, contextInfo, ingredientID)
if unitsResponse.HasErrors() {
errorResp := unitsResponse.GetErrors()[0]
logger.FromContext(ctx).WithError(errorResp).Error("IngredientUnitConverterHandler::GetUnitsByIngredientID -> Failed to get units for ingredient from service")
}
util.HandleResponse(c.Writer, c.Request, unitsResponse, "IngredientUnitConverterHandler::GetUnitsByIngredientID")
}

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
}
// 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{
ID: entity.ID,
OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
ProductID: entity.ProductID,
IngredientID: entity.IngredientID,
Quantity: entity.Quantity,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
Product: ProductEntityToModel(entity.Product),
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
ID: entity.ID,
OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
ProductID: entity.ProductID,
IngredientID: entity.IngredientID,
Quantity: entity.Quantity,
WastePercentage: entity.WastePercentage,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
Product: ProductEntityToModel(entity.Product),
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
}
}
@ -30,16 +31,17 @@ func MapProductIngredientModelToEntity(model *models.ProductIngredient) *entitie
}
return &entities.ProductIngredient{
ID: model.ID,
OrganizationID: model.OrganizationID,
OutletID: model.OutletID,
ProductID: model.ProductID,
IngredientID: model.IngredientID,
Quantity: model.Quantity,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
Product: ProductModelToEntity(model.Product),
Ingredient: MapIngredientModelToEntity(model.Ingredient),
ID: model.ID,
OrganizationID: model.OrganizationID,
OutletID: model.OutletID,
ProductID: model.ProductID,
IngredientID: model.IngredientID,
Quantity: model.Quantity,
WastePercentage: model.WastePercentage,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
Product: ProductModelToEntity(model.Product),
Ingredient: MapIngredientModelToEntity(model.Ingredient),
}
}

View File

@ -94,3 +94,11 @@ type ListIngredientUnitConvertersResponse struct {
TotalPages int `json:"total_pages"`
}
type IngredientUnitsResponse struct {
IngredientID uuid.UUID `json:"ingredient_id"`
IngredientName string `json:"ingredient_name"`
BaseUnitID uuid.UUID `json:"base_unit_id"`
BaseUnitName string `json:"base_unit_name"`
Units []*UnitResponse `json:"units"`
}

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

View File

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

View File

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

View File

@ -1,17 +1 @@
package processor
import (
"apskel-pos-be/internal/entities"
"context"
"github.com/google/uuid"
)
type IngredientRepository interface {
Create(ctx context.Context, ingredient *entities.Ingredient) error
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Ingredient, error)
GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string, isSemiFinished *bool) ([]*entities.Ingredient, int, error)
Update(ctx context.Context, ingredient *entities.Ingredient) error
Delete(ctx context.Context, id, organizationID uuid.UUID) error
UpdateStock(ctx context.Context, id uuid.UUID, newStock float64, organizationID uuid.UUID) error
}

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)
GetConvertersForIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*models.IngredientUnitConverterResponse, error)
ConvertUnit(ctx context.Context, organizationID uuid.UUID, req *models.ConvertUnitRequest) (*models.ConvertUnitResponse, error)
GetUnitsByIngredientID(ctx context.Context, organizationID, ingredientID uuid.UUID) (*models.IngredientUnitsResponse, error)
}
type IngredientUnitConverterProcessorImpl struct {
@ -257,3 +258,64 @@ func (p *IngredientUnitConverterProcessorImpl) ConvertUnit(ctx context.Context,
return response, nil
}
func (p *IngredientUnitConverterProcessorImpl) GetUnitsByIngredientID(ctx context.Context, organizationID, ingredientID uuid.UUID) (*models.IngredientUnitsResponse, error) {
// Get the ingredient with its base unit
ingredient, err := p.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get ingredient: %w", err)
}
// Get the base unit details
baseUnit, err := p.unitRepo.GetByID(ctx, ingredient.UnitID, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get base unit: %w", err)
}
// Start with the base unit
units := []*models.UnitResponse{
mappers.MapUnitEntityToResponse(baseUnit),
}
// Get all converters for this ingredient
converters, err := p.converterRepo.GetConvertersForIngredient(ctx, ingredientID, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get converters: %w", err)
}
// Add unique units from converters
unitMap := make(map[uuid.UUID]bool)
unitMap[baseUnit.ID] = true
for _, converter := range converters {
if converter.IsActive {
// Add FromUnit if not already added
if !unitMap[converter.FromUnitID] {
fromUnit, err := p.unitRepo.GetByID(ctx, converter.FromUnitID, organizationID)
if err == nil {
units = append(units, mappers.MapUnitEntityToResponse(fromUnit))
unitMap[converter.FromUnitID] = true
}
}
// Add ToUnit if not already added
if !unitMap[converter.ToUnitID] {
toUnit, err := p.unitRepo.GetByID(ctx, converter.ToUnitID, organizationID)
if err == nil {
units = append(units, mappers.MapUnitEntityToResponse(toUnit))
unitMap[converter.ToUnitID] = true
}
}
}
}
response := &models.IngredientUnitsResponse{
IngredientID: ingredientID,
IngredientName: ingredient.Name,
BaseUnitID: baseUnit.ID,
BaseUnitName: baseUnit.Name,
Units: units,
}
return response, nil
}

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

View File

@ -19,6 +19,7 @@ type IngredientUnitConverterService interface {
ListIngredientUnitConverters(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListIngredientUnitConvertersRequest) *contract.Response
GetConvertersForIngredient(ctx context.Context, apctx *appcontext.ContextInfo, ingredientID uuid.UUID) *contract.Response
ConvertUnit(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ConvertUnitRequest) *contract.Response
GetUnitsByIngredientID(ctx context.Context, apctx *appcontext.ContextInfo, ingredientID uuid.UUID) *contract.Response
}
type IngredientUnitConverterServiceImpl struct {
@ -149,3 +150,14 @@ func (s *IngredientUnitConverterServiceImpl) ConvertUnit(ctx context.Context, ap
return contract.BuildSuccessResponse(contractResponse)
}
func (s *IngredientUnitConverterServiceImpl) GetUnitsByIngredientID(ctx context.Context, apctx *appcontext.ContextInfo, ingredientID uuid.UUID) *contract.Response {
unitsResponse, err := s.converterProcessor.GetUnitsByIngredientID(ctx, apctx.OrganizationID, ingredientID)
if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.IngredientUnitConverterServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
}
contractResponse := transformer.IngredientUnitsModelResponseToResponse(unitsResponse)
return contract.BuildSuccessResponse(contractResponse)
}

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
import (
"apskel-pos-be/internal/appcontext"
"context"
"fmt"
"time"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/processor"
"apskel-pos-be/internal/repository"
"apskel-pos-be/internal/util"
"github.com/google/uuid"
)
@ -27,14 +31,22 @@ type OrderService interface {
}
type OrderServiceImpl struct {
orderProcessor processor.OrderProcessor
tableRepo repository.TableRepositoryInterface
orderProcessor processor.OrderProcessor
tableRepo repository.TableRepositoryInterface
orderIngredientTransactionService *OrderIngredientTransactionService
orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor
productIngredientRepo repository.ProductIngredientRepository
txManager *repository.TxManager
}
func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repository.TableRepositoryInterface) *OrderServiceImpl {
func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repository.TableRepositoryInterface, orderIngredientTransactionService *OrderIngredientTransactionService, orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor, productIngredientRepo repository.ProductIngredientRepository, txManager *repository.TxManager) *OrderServiceImpl {
return &OrderServiceImpl{
orderProcessor: orderProcessor,
tableRepo: tableRepo,
orderProcessor: orderProcessor,
tableRepo: tableRepo,
orderIngredientTransactionService: orderIngredientTransactionService,
orderIngredientTransactionProcessor: orderIngredientTransactionProcessor,
productIngredientRepo: productIngredientRepo,
txManager: txManager,
}
}
@ -49,47 +61,133 @@ func (s *OrderServiceImpl) CreateOrder(ctx context.Context, req *models.CreateOr
}
}
response, err := s.orderProcessor.CreateOrder(ctx, req, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to create order: %w", err)
}
var response *models.OrderResponse
var ingredientTransactions []*contract.CreateOrderIngredientTransactionRequest
if req.TableID != nil {
if err := s.occupyTableWithOrder(ctx, *req.TableID, response.ID); err != nil {
fmt.Printf("Warning: failed to occupy table %s with order %s: %v\n", *req.TableID, response.ID, err)
// Use transaction to ensure atomicity
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
// Create the order
orderResp, err := s.orderProcessor.CreateOrder(txCtx, req, organizationID)
if err != nil {
return fmt.Errorf("failed to create order: %w", err)
}
response = orderResp
// Create ingredient transactions for each order item
ingredientTransactions, err = s.createIngredientTransactions(txCtx, response.ID, response.OrderItems)
if err != nil {
return fmt.Errorf("failed to create ingredient transactions: %w", err)
}
// Bulk create ingredient transactions
if len(ingredientTransactions) > 0 {
_, err = s.orderIngredientTransactionService.BulkCreateOrderIngredientTransactions(txCtx, ingredientTransactions)
if err != nil {
return fmt.Errorf("failed to bulk create ingredient transactions: %w", err)
}
}
// Occupy table if specified
if req.TableID != nil {
if err := s.occupyTableWithOrder(txCtx, *req.TableID, response.ID); err != nil {
// Log warning but don't fail the transaction
fmt.Printf("Warning: failed to occupy table %s with order %s: %v\n", *req.TableID, response.ID, err)
}
}
return nil
})
if err != nil {
return nil, err
}
return response, nil
}
// createIngredientTransactions creates ingredient transactions for order items efficiently
func (s *OrderServiceImpl) createIngredientTransactions(ctx context.Context, orderID uuid.UUID, orderItems []models.OrderItemResponse) ([]*contract.CreateOrderIngredientTransactionRequest, error) {
appCtx := appcontext.FromGinContext(ctx)
organizationID := appCtx.OrganizationID
var allTransactions []*contract.CreateOrderIngredientTransactionRequest
for _, orderItem := range orderItems {
// Get product ingredients for this product
productIngredients, err := s.productIngredientRepo.GetByProductID(ctx, orderItem.ProductID, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get product ingredients for product %s: %w", orderItem.ProductID, err)
}
if len(productIngredients) == 0 {
continue // Skip if no ingredients
}
// Calculate waste quantities
transactions, err := s.calculateWasteQuantities(productIngredients, float64(orderItem.Quantity))
if err != nil {
return nil, fmt.Errorf("failed to calculate waste quantities for product %s: %w", err)
}
// Set common fields for all transactions
for _, transaction := range transactions {
transaction.OrderID = orderID
transaction.OrderItemID = &orderItem.ID
transaction.ProductID = orderItem.ProductID
transaction.ProductVariantID = orderItem.ProductVariantID
}
allTransactions = append(allTransactions, transactions...)
}
return allTransactions, nil
}
func (s *OrderServiceImpl) AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) {
// Validate inputs
if orderID == uuid.Nil {
return nil, fmt.Errorf("invalid order ID")
}
// Validate request
if err := s.validateAddToOrderRequest(req); err != nil {
return nil, fmt.Errorf("validation error: %w", err)
}
// Process adding items to order
response, err := s.orderProcessor.AddToOrder(ctx, orderID, req)
var response *models.AddToOrderResponse
var ingredientTransactions []*contract.CreateOrderIngredientTransactionRequest
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
addResp, err := s.orderProcessor.AddToOrder(txCtx, orderID, req)
if err != nil {
return fmt.Errorf("failed to add items to order: %w", err)
}
response = addResp
ingredientTransactions, err = s.createIngredientTransactions(txCtx, orderID, response.AddedItems)
if err != nil {
return fmt.Errorf("failed to create ingredient transactions: %w", err)
}
if len(ingredientTransactions) > 0 {
_, err = s.orderIngredientTransactionService.BulkCreateOrderIngredientTransactions(txCtx, ingredientTransactions)
if err != nil {
return fmt.Errorf("failed to bulk create ingredient transactions: %w", err)
}
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to add items to order: %w", err)
return nil, err
}
return response, nil
}
func (s *OrderServiceImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) {
// Validate request
if err := s.validateUpdateOrderRequest(req); err != nil {
return nil, fmt.Errorf("validation error: %w", err)
}
// Process order update
response, err := s.orderProcessor.UpdateOrder(ctx, id, req)
if err != nil {
return nil, fmt.Errorf("failed to update order: %w", err)
@ -137,9 +235,7 @@ func (s *OrderServiceImpl) VoidOrder(ctx context.Context, req *models.VoidOrderR
return fmt.Errorf("failed to void order: %w", err)
}
// Release table if order is voided
if err := s.handleTableReleaseOnVoid(ctx, req.OrderID); err != nil {
// Log the error but don't fail the void operation
fmt.Printf("Warning: failed to handle table release for voided order %s: %v\n", req.OrderID, err)
}
@ -547,3 +643,79 @@ func (s *OrderServiceImpl) handleTableReleaseOnVoid(ctx context.Context, orderID
return nil
}
func (s *OrderServiceImpl) createOrderIngredientTransactions(ctx context.Context, order *models.Order, orderItems []*models.OrderItem) error {
for _, orderItem := range orderItems {
productIngredients, err := s.productIngredientRepo.GetByProductID(ctx, orderItem.ProductID, order.OrganizationID)
if err != nil {
return fmt.Errorf("failed to get product ingredients for product %s: %w", orderItem.ProductID, err)
}
if len(productIngredients) == 0 {
continue // Skip if no ingredients
}
// Calculate waste quantities using the utility function
transactions, err := s.calculateWasteQuantities(productIngredients, float64(orderItem.Quantity))
if err != nil {
return fmt.Errorf("failed to calculate waste quantities for product %s: %w", orderItem.ProductID, err)
}
// Set common fields for all transactions
for _, transaction := range transactions {
transaction.OrderID = order.ID
transaction.OrderItemID = &orderItem.ID
transaction.ProductID = orderItem.ProductID
transaction.ProductVariantID = orderItem.ProductVariantID
}
// Bulk create transactions
if len(transactions) > 0 {
_, err := s.orderIngredientTransactionService.BulkCreateOrderIngredientTransactions(ctx, transactions)
if err != nil {
return fmt.Errorf("failed to create order ingredient transactions for product %s: %w", orderItem.ProductID, err)
}
}
}
return nil
}
// calculateWasteQuantities calculates gross, net, and waste quantities for product ingredients
func (s *OrderServiceImpl) calculateWasteQuantities(productIngredients []*entities.ProductIngredient, quantity float64) ([]*contract.CreateOrderIngredientTransactionRequest, error) {
if len(productIngredients) == 0 {
return []*contract.CreateOrderIngredientTransactionRequest{}, nil
}
transactions := make([]*contract.CreateOrderIngredientTransactionRequest, 0, len(productIngredients))
for _, pi := range productIngredients {
// Calculate net quantity (actual quantity needed for the product)
netQty := pi.Quantity * quantity
// Calculate gross quantity (including waste)
wasteMultiplier := 1 + (pi.WastePercentage / 100)
grossQty := netQty * wasteMultiplier
// Calculate waste quantity
wasteQty := grossQty - netQty
// Get unit name from ingredient
unitName := "unit" // default
if pi.Ingredient != nil && pi.Ingredient.Unit != nil {
unitName = pi.Ingredient.Unit.Name
}
transaction := &contract.CreateOrderIngredientTransactionRequest{
IngredientID: pi.IngredientID,
GrossQty: util.RoundToDecimalPlaces(grossQty, 3),
NetQty: util.RoundToDecimalPlaces(netQty, 3),
WasteQty: util.RoundToDecimalPlaces(wasteQty, 3),
Unit: unitName,
}
transactions = append(transactions, transaction)
}
return transactions, nil
}

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

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