diff --git a/internal/app/app.go b/internal/app/app.go index ba3166e..5b65de8 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -80,6 +80,18 @@ func (a *App) Initialize(cfg *config.Config) error { services.unitService, services.ingredientService, services.productRecipeService, + services.vendorService, + validators.vendorValidator, + services.purchaseOrderService, + validators.purchaseOrderValidator, + services.unitConverterService, + validators.unitConverterValidator, + services.chartOfAccountTypeService, + validators.chartOfAccountTypeValidator, + services.chartOfAccountService, + validators.chartOfAccountValidator, + services.accountService, + validators.accountValidator, ) return nil @@ -125,77 +137,95 @@ 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 - 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 + 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), - 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), + 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 - 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 + fileClient processor.FileClient + inventoryMovementService service.InventoryMovementService } func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors { @@ -203,48 +233,60 @@ 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), - 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), + 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 + 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 } func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services { @@ -268,27 +310,39 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con unitService := service.NewUnitService(processors.unitProcessor) ingredientService := service.NewIngredientService(processors.ingredientProcessor) productRecipeService := service.NewProductRecipeService(processors.productRecipeProcessor) + vendorService := service.NewVendorService(processors.vendorProcessor) + purchaseOrderService := service.NewPurchaseOrderService(processors.purchaseOrderProcessor) + unitConverterService := service.NewIngredientUnitConverterService(processors.unitConverterProcessor) + chartOfAccountTypeService := service.NewChartOfAccountTypeService(processors.chartOfAccountTypeProcessor) + chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor) + accountService := service.NewAccountService(processors.accountProcessor) 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, + 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, } } @@ -303,33 +357,45 @@ 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 + 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 } 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(), + 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), } } diff --git a/internal/constants/default_chart_of_accounts.json b/internal/constants/default_chart_of_accounts.json new file mode 100644 index 0000000..45b1a48 --- /dev/null +++ b/internal/constants/default_chart_of_accounts.json @@ -0,0 +1,272 @@ +{ + "chart_of_accounts": [ + { + "name": "Current Assets", + "code": "1000", + "chart_of_account_type": "ASSET", + "parent_code": null, + "is_system": true, + "accounts": [ + { + "name": "Cash on Hand", + "number": "1001", + "account_type": "cash", + "opening_balance": 0.00, + "description": "Physical cash available at the outlet" + }, + { + "name": "Petty Cash", + "number": "1002", + "account_type": "cash", + "opening_balance": 0.00, + "description": "Small amount of cash for minor expenses" + }, + { + "name": "Bank Account - Main", + "number": "1003", + "account_type": "bank", + "opening_balance": 0.00, + "description": "Primary business bank account" + }, + { + "name": "Digital Wallet", + "number": "1004", + "account_type": "wallet", + "opening_balance": 0.00, + "description": "Digital payment wallet" + } + ] + }, + { + "name": "Inventory", + "code": "1100", + "chart_of_account_type": "ASSET", + "parent_code": "1000", + "is_system": true, + "accounts": [ + { + "name": "Raw Materials", + "number": "1101", + "account_type": "asset", + "opening_balance": 0.00, + "description": "Raw materials and ingredients inventory" + }, + { + "name": "Finished Goods", + "number": "1102", + "account_type": "asset", + "opening_balance": 0.00, + "description": "Finished products ready for sale" + }, + { + "name": "Work in Progress", + "number": "1103", + "account_type": "asset", + "opening_balance": 0.00, + "description": "Products in production process" + } + ] + }, + { + "name": "Fixed Assets", + "code": "1500", + "chart_of_account_type": "ASSET", + "parent_code": null, + "is_system": true, + "accounts": [ + { + "name": "Equipment", + "number": "1501", + "account_type": "asset", + "opening_balance": 0.00, + "description": "Business equipment and machinery" + }, + { + "name": "Furniture & Fixtures", + "number": "1502", + "account_type": "asset", + "opening_balance": 0.00, + "description": "Furniture and fixtures" + } + ] + }, + { + "name": "Current Liabilities", + "code": "2000", + "chart_of_account_type": "LIABILITY", + "parent_code": null, + "is_system": true, + "accounts": [ + { + "name": "Accounts Payable", + "number": "2001", + "account_type": "liability", + "opening_balance": 0.00, + "description": "Amounts owed to suppliers and vendors" + }, + { + "name": "Accrued Expenses", + "number": "2002", + "account_type": "liability", + "opening_balance": 0.00, + "description": "Expenses incurred but not yet paid" + }, + { + "name": "Sales Tax Payable", + "number": "2003", + "account_type": "liability", + "opening_balance": 0.00, + "description": "Sales tax collected but not yet remitted" + } + ] + }, + { + "name": "Owner's Equity", + "code": "3000", + "chart_of_account_type": "EQUITY", + "parent_code": null, + "is_system": true, + "accounts": [ + { + "name": "Owner's Capital", + "number": "3001", + "account_type": "equity", + "opening_balance": 0.00, + "description": "Owner's initial investment in the business" + }, + { + "name": "Retained Earnings", + "number": "3002", + "account_type": "equity", + "opening_balance": 0.00, + "description": "Accumulated profits retained in the business" + } + ] + }, + { + "name": "Revenue", + "code": "4000", + "chart_of_account_type": "REVENUE", + "parent_code": null, + "is_system": true, + "accounts": [ + { + "name": "Sales Revenue", + "number": "4001", + "account_type": "revenue", + "opening_balance": 0.00, + "description": "Revenue from product sales" + }, + { + "name": "Service Revenue", + "number": "4002", + "account_type": "revenue", + "opening_balance": 0.00, + "description": "Revenue from services provided" + }, + { + "name": "Other Income", + "number": "4003", + "account_type": "revenue", + "opening_balance": 0.00, + "description": "Other sources of income" + } + ] + }, + { + "name": "Cost of Goods Sold", + "code": "5000", + "chart_of_account_type": "EXPENSE", + "parent_code": null, + "is_system": true, + "accounts": [ + { + "name": "Raw Materials Cost", + "number": "5001", + "account_type": "expense", + "opening_balance": 0.00, + "description": "Cost of raw materials used in production" + }, + { + "name": "Direct Labor Cost", + "number": "5002", + "account_type": "expense", + "opening_balance": 0.00, + "description": "Direct labor costs for production" + }, + { + "name": "Manufacturing Overhead", + "number": "5003", + "account_type": "expense", + "opening_balance": 0.00, + "description": "Manufacturing overhead costs" + } + ] + }, + { + "name": "Operating Expenses", + "code": "6000", + "chart_of_account_type": "EXPENSE", + "parent_code": null, + "is_system": true, + "accounts": [ + { + "name": "Rent Expense", + "number": "6001", + "account_type": "expense", + "opening_balance": 0.00, + "description": "Rent for business premises" + }, + { + "name": "Utilities Expense", + "number": "6002", + "account_type": "expense", + "opening_balance": 0.00, + "description": "Electricity, water, and other utilities" + }, + { + "name": "Salaries & Wages", + "number": "6003", + "account_type": "expense", + "opening_balance": 0.00, + "description": "Employee salaries and wages" + }, + { + "name": "Marketing Expense", + "number": "6004", + "account_type": "expense", + "opening_balance": 0.00, + "description": "Marketing and advertising expenses" + }, + { + "name": "Office Supplies", + "number": "6005", + "account_type": "expense", + "opening_balance": 0.00, + "description": "Office supplies and stationery" + }, + { + "name": "Professional Services", + "number": "6006", + "account_type": "expense", + "opening_balance": 0.00, + "description": "Legal, accounting, and consulting fees" + }, + { + "name": "Insurance Expense", + "number": "6007", + "account_type": "expense", + "opening_balance": 0.00, + "description": "Business insurance premiums" + }, + { + "name": "Depreciation Expense", + "number": "6008", + "account_type": "expense", + "opening_balance": 0.00, + "description": "Depreciation of fixed assets" + } + ] + } + ] +} diff --git a/internal/constants/error.go b/internal/constants/error.go index 4de95bb..17d86ee 100644 --- a/internal/constants/error.go +++ b/internal/constants/error.go @@ -15,30 +15,33 @@ const ( ) const ( - RequestEntity = "request" - UserServiceEntity = "user_service" - OrganizationServiceEntity = "organization_service" - CategoryServiceEntity = "category_service" - ProductServiceEntity = "product_service" - ProductVariantServiceEntity = "product_variant_service" - InventoryServiceEntity = "inventory_service" - OrderServiceEntity = "order_service" - CustomerServiceEntity = "customer_service" - UserValidatorEntity = "user_validator" - AuthHandlerEntity = "auth_handler" - UserHandlerEntity = "user_handler" - CategoryHandlerEntity = "category_handler" - ProductHandlerEntity = "product_handler" - ProductVariantHandlerEntity = "product_variant_handler" - InventoryHandlerEntity = "inventory_handler" - OrderValidatorEntity = "order_validator" - OrderHandlerEntity = "order_handler" - OrganizationValidatorEntity = "organization_validator" - OrgHandlerEntity = "organization_handler" - PaymentMethodValidatorEntity = "payment_method_validator" - PaymentMethodHandlerEntity = "payment_method_handler" - OutletServiceEntity = "outlet_service" - TableEntity = "table" + RequestEntity = "request" + UserServiceEntity = "user_service" + OrganizationServiceEntity = "organization_service" + CategoryServiceEntity = "category_service" + ProductServiceEntity = "product_service" + ProductVariantServiceEntity = "product_variant_service" + InventoryServiceEntity = "inventory_service" + OrderServiceEntity = "order_service" + CustomerServiceEntity = "customer_service" + UserValidatorEntity = "user_validator" + AuthHandlerEntity = "auth_handler" + UserHandlerEntity = "user_handler" + CategoryHandlerEntity = "category_handler" + ProductHandlerEntity = "product_handler" + ProductVariantHandlerEntity = "product_variant_handler" + InventoryHandlerEntity = "inventory_handler" + OrderValidatorEntity = "order_validator" + OrderHandlerEntity = "order_handler" + OrganizationValidatorEntity = "organization_validator" + OrgHandlerEntity = "organization_handler" + PaymentMethodValidatorEntity = "payment_method_validator" + PaymentMethodHandlerEntity = "payment_method_handler" + OutletServiceEntity = "outlet_service" + VendorServiceEntity = "vendor_service" + PurchaseOrderServiceEntity = "purchase_order_service" + IngredientUnitConverterServiceEntity = "ingredient_unit_converter_service" + TableEntity = "table" ) var HttpErrorMap = map[string]int{ diff --git a/internal/contract/account_contract.go b/internal/contract/account_contract.go new file mode 100644 index 0000000..fdd7e64 --- /dev/null +++ b/internal/contract/account_contract.go @@ -0,0 +1,19 @@ +package contract + +import ( + "context" + + "github.com/google/uuid" +) + +type AccountContract interface { + CreateAccount(ctx context.Context, req *CreateAccountRequest) (*AccountResponse, error) + GetAccountByID(ctx context.Context, id uuid.UUID) (*AccountResponse, error) + UpdateAccount(ctx context.Context, id uuid.UUID, req *UpdateAccountRequest) (*AccountResponse, error) + DeleteAccount(ctx context.Context, id uuid.UUID) error + ListAccounts(ctx context.Context, req *ListAccountsRequest) ([]AccountResponse, int, error) + GetAccountsByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]AccountResponse, error) + GetAccountsByChartOfAccount(ctx context.Context, chartOfAccountID uuid.UUID) ([]AccountResponse, error) + UpdateAccountBalance(ctx context.Context, id uuid.UUID, req *UpdateAccountBalanceRequest) error + GetAccountBalance(ctx context.Context, id uuid.UUID) (float64, error) +} diff --git a/internal/contract/account_request.go b/internal/contract/account_request.go new file mode 100644 index 0000000..b77dcdb --- /dev/null +++ b/internal/contract/account_request.go @@ -0,0 +1,57 @@ +package contract + +import ( + "github.com/google/uuid" +) + +type CreateAccountRequest struct { + ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"` + Name string `json:"name" validate:"required,min=1,max=255"` + Number string `json:"number" validate:"required,min=1,max=50"` + AccountType string `json:"account_type" validate:"required,oneof=cash wallet bank credit debit asset liability equity revenue expense"` + OpeningBalance float64 `json:"opening_balance"` + Description *string `json:"description"` +} + +type UpdateAccountRequest struct { + ChartOfAccountID *uuid.UUID `json:"chart_of_account_id"` + Name *string `json:"name" validate:"omitempty,min=1,max=255"` + Number *string `json:"number" validate:"omitempty,min=1,max=50"` + AccountType *string `json:"account_type" validate:"omitempty,oneof=cash wallet bank credit debit asset liability equity revenue expense"` + OpeningBalance *float64 `json:"opening_balance"` + Description *string `json:"description"` + IsActive *bool `json:"is_active"` +} + +type AccountResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` + ChartOfAccountID uuid.UUID `json:"chart_of_account_id"` + Name string `json:"name"` + Number string `json:"number"` + AccountType string `json:"account_type"` + OpeningBalance float64 `json:"opening_balance"` + CurrentBalance float64 `json:"current_balance"` + Description *string `json:"description"` + IsActive bool `json:"is_active"` + IsSystem bool `json:"is_system"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ChartOfAccount *ChartOfAccountResponse `json:"chart_of_account,omitempty"` +} + +type ListAccountsRequest struct { + OrganizationID *uuid.UUID `form:"organization_id"` + OutletID *uuid.UUID `form:"outlet_id"` + ChartOfAccountID *uuid.UUID `form:"chart_of_account_id"` + AccountType *string `form:"account_type"` + IsActive *bool `form:"is_active"` + IsSystem *bool `form:"is_system"` + Page int `form:"page,default=1"` + Limit int `form:"limit,default=10"` +} + +type UpdateAccountBalanceRequest struct { + Amount float64 `json:"amount" binding:"required"` +} diff --git a/internal/contract/chart_of_account_contract.go b/internal/contract/chart_of_account_contract.go new file mode 100644 index 0000000..9a78ff8 --- /dev/null +++ b/internal/contract/chart_of_account_contract.go @@ -0,0 +1,17 @@ +package contract + +import ( + "context" + + "github.com/google/uuid" +) + +type ChartOfAccountContract interface { + CreateChartOfAccount(ctx context.Context, req *CreateChartOfAccountRequest) (*ChartOfAccountResponse, error) + GetChartOfAccountByID(ctx context.Context, id uuid.UUID) (*ChartOfAccountResponse, error) + UpdateChartOfAccount(ctx context.Context, id uuid.UUID, req *UpdateChartOfAccountRequest) (*ChartOfAccountResponse, error) + DeleteChartOfAccount(ctx context.Context, id uuid.UUID) error + ListChartOfAccounts(ctx context.Context, req *ListChartOfAccountsRequest) ([]ChartOfAccountResponse, int, error) + GetChartOfAccountsByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]ChartOfAccountResponse, error) + GetChartOfAccountsByType(ctx context.Context, organizationID uuid.UUID, chartOfAccountTypeID uuid.UUID, outletID *uuid.UUID) ([]ChartOfAccountResponse, error) +} diff --git a/internal/contract/chart_of_account_request.go b/internal/contract/chart_of_account_request.go new file mode 100644 index 0000000..cb3c461 --- /dev/null +++ b/internal/contract/chart_of_account_request.go @@ -0,0 +1,51 @@ +package contract + +import ( + "github.com/google/uuid" +) + +type CreateChartOfAccountRequest struct { + ChartOfAccountTypeID uuid.UUID `json:"chart_of_account_type_id" validate:"required"` + ParentID *uuid.UUID `json:"parent_id"` + Name string `json:"name" validate:"required,min=1,max=255"` + Code string `json:"code" validate:"required,min=1,max=20"` + Description *string `json:"description"` +} + +type UpdateChartOfAccountRequest struct { + ChartOfAccountTypeID *uuid.UUID `json:"chart_of_account_type_id"` + ParentID *uuid.UUID `json:"parent_id"` + Name *string `json:"name" validate:"omitempty,min=1,max=255"` + Code *string `json:"code" validate:"omitempty,min=1,max=20"` + Description *string `json:"description"` + IsActive *bool `json:"is_active"` +} + +type ChartOfAccountResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` + ChartOfAccountTypeID uuid.UUID `json:"chart_of_account_type_id"` + ParentID *uuid.UUID `json:"parent_id"` + Name string `json:"name"` + Code string `json:"code"` + Description *string `json:"description"` + IsActive bool `json:"is_active"` + IsSystem bool `json:"is_system"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ChartOfAccountType *ChartOfAccountTypeResponse `json:"chart_of_account_type,omitempty"` + Parent *ChartOfAccountResponse `json:"parent,omitempty"` + Children []ChartOfAccountResponse `json:"children,omitempty"` +} + +type ListChartOfAccountsRequest struct { + OrganizationID *uuid.UUID `form:"organization_id"` + OutletID *uuid.UUID `form:"outlet_id"` + ChartOfAccountTypeID *uuid.UUID `form:"chart_of_account_type_id"` + ParentID *uuid.UUID `form:"parent_id"` + IsActive *bool `form:"is_active"` + IsSystem *bool `form:"is_system"` + Page int `form:"page,default=1"` + Limit int `form:"limit,default=10"` +} diff --git a/internal/contract/chart_of_account_type_contract.go b/internal/contract/chart_of_account_type_contract.go new file mode 100644 index 0000000..1864b78 --- /dev/null +++ b/internal/contract/chart_of_account_type_contract.go @@ -0,0 +1,15 @@ +package contract + +import ( + "context" + + "github.com/google/uuid" +) + +type ChartOfAccountTypeContract interface { + CreateChartOfAccountType(ctx context.Context, req *CreateChartOfAccountTypeRequest) (*ChartOfAccountTypeResponse, error) + GetChartOfAccountTypeByID(ctx context.Context, id uuid.UUID) (*ChartOfAccountTypeResponse, error) + UpdateChartOfAccountType(ctx context.Context, id uuid.UUID, req *UpdateChartOfAccountTypeRequest) (*ChartOfAccountTypeResponse, error) + DeleteChartOfAccountType(ctx context.Context, id uuid.UUID) error + ListChartOfAccountTypes(ctx context.Context, filters map[string]interface{}, page, limit int) ([]ChartOfAccountTypeResponse, int, error) +} diff --git a/internal/contract/chart_of_account_type_request.go b/internal/contract/chart_of_account_type_request.go new file mode 100644 index 0000000..a558799 --- /dev/null +++ b/internal/contract/chart_of_account_type_request.go @@ -0,0 +1,28 @@ +package contract + +import ( + "github.com/google/uuid" +) + +type CreateChartOfAccountTypeRequest struct { + Name string `json:"name" validate:"required,min=1,max=100"` + Code string `json:"code" validate:"required,min=1,max=10"` + Description *string `json:"description"` +} + +type UpdateChartOfAccountTypeRequest struct { + Name *string `json:"name" validate:"omitempty,min=1,max=100"` + Code *string `json:"code" validate:"omitempty,min=1,max=10"` + Description *string `json:"description"` + IsActive *bool `json:"is_active"` +} + +type ChartOfAccountTypeResponse struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + Description *string `json:"description"` + IsActive bool `json:"is_active"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} diff --git a/internal/contract/ingredient_unit_converter_contract.go b/internal/contract/ingredient_unit_converter_contract.go new file mode 100644 index 0000000..4d61dad --- /dev/null +++ b/internal/contract/ingredient_unit_converter_contract.go @@ -0,0 +1,76 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +// Request DTOs +type CreateIngredientUnitConverterRequest struct { + IngredientID uuid.UUID `json:"ingredient_id" validate:"required"` + FromUnitID uuid.UUID `json:"from_unit_id" validate:"required"` + ToUnitID uuid.UUID `json:"to_unit_id" validate:"required"` + ConversionFactor float64 `json:"conversion_factor" validate:"required,gt=0"` + IsActive *bool `json:"is_active,omitempty" validate:"omitempty"` +} + +type UpdateIngredientUnitConverterRequest struct { + FromUnitID *uuid.UUID `json:"from_unit_id,omitempty" validate:"omitempty"` + ToUnitID *uuid.UUID `json:"to_unit_id,omitempty" validate:"omitempty"` + ConversionFactor *float64 `json:"conversion_factor,omitempty" validate:"omitempty,gt=0"` + IsActive *bool `json:"is_active,omitempty" validate:"omitempty"` +} + +type ListIngredientUnitConvertersRequest struct { + IngredientID *uuid.UUID `json:"ingredient_id,omitempty"` + FromUnitID *uuid.UUID `json:"from_unit_id,omitempty"` + ToUnitID *uuid.UUID `json:"to_unit_id,omitempty"` + IsActive *bool `json:"is_active,omitempty"` + Search string `json:"search,omitempty"` + Page int `json:"page" validate:"required,min=1"` + Limit int `json:"limit" validate:"required,min=1,max=100"` +} + +type ConvertUnitRequest struct { + IngredientID uuid.UUID `json:"ingredient_id" validate:"required"` + FromUnitID uuid.UUID `json:"from_unit_id" validate:"required"` + ToUnitID uuid.UUID `json:"to_unit_id" validate:"required"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` +} + +// Response DTOs +type IngredientUnitConverterResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + IngredientID uuid.UUID `json:"ingredient_id"` + FromUnitID uuid.UUID `json:"from_unit_id"` + ToUnitID uuid.UUID `json:"to_unit_id"` + ConversionFactor float64 `json:"conversion_factor"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + CreatedBy uuid.UUID `json:"created_by"` + UpdatedBy uuid.UUID `json:"updated_by"` + Ingredient *IngredientResponse `json:"ingredient,omitempty"` + FromUnit *UnitResponse `json:"from_unit,omitempty"` + ToUnit *UnitResponse `json:"to_unit,omitempty"` +} + +type ConvertUnitResponse struct { + FromQuantity float64 `json:"from_quantity"` + FromUnit *UnitResponse `json:"from_unit"` + ToQuantity float64 `json:"to_quantity"` + ToUnit *UnitResponse `json:"to_unit"` + ConversionFactor float64 `json:"conversion_factor"` + Ingredient *IngredientResponse `json:"ingredient,omitempty"` +} + +type ListIngredientUnitConvertersResponse struct { + Converters []IngredientUnitConverterResponse `json:"converters"` + TotalCount int `json:"total_count"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` +} + diff --git a/internal/contract/purchase_order_contract.go b/internal/contract/purchase_order_contract.go new file mode 100644 index 0000000..4f22d22 --- /dev/null +++ b/internal/contract/purchase_order_contract.go @@ -0,0 +1,117 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +type CreatePurchaseOrderRequest struct { + VendorID uuid.UUID `json:"vendor_id" validate:"required"` + PONumber string `json:"po_number" validate:"required,min=1,max=50"` + TransactionDate time.Time `json:"transaction_date" validate:"required"` + DueDate time.Time `json:"due_date" validate:"required"` + Reference *string `json:"reference,omitempty" validate:"omitempty,max=100"` + Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved received cancelled"` + Message *string `json:"message,omitempty" validate:"omitempty"` + Items []CreatePurchaseOrderItemRequest `json:"items" validate:"required,min=1,dive"` + AttachmentFileIDs []uuid.UUID `json:"attachment_file_ids,omitempty"` +} + +type CreatePurchaseOrderItemRequest struct { + IngredientID uuid.UUID `json:"ingredient_id" validate:"required"` + Description *string `json:"description,omitempty" validate:"omitempty"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` + UnitID uuid.UUID `json:"unit_id" validate:"required"` + Amount float64 `json:"amount" validate:"required,gte=0"` +} + +type UpdatePurchaseOrderRequest struct { + VendorID *uuid.UUID `json:"vendor_id,omitempty" validate:"omitempty"` + PONumber *string `json:"po_number,omitempty" validate:"omitempty,min=1,max=50"` + TransactionDate *time.Time `json:"transaction_date,omitempty" validate:"omitempty"` + DueDate *time.Time `json:"due_date,omitempty" validate:"omitempty"` + Reference *string `json:"reference,omitempty" validate:"omitempty,max=100"` + Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved received cancelled"` + Message *string `json:"message,omitempty" validate:"omitempty"` + Items []UpdatePurchaseOrderItemRequest `json:"items,omitempty" validate:"omitempty,dive"` + AttachmentFileIDs []uuid.UUID `json:"attachment_file_ids,omitempty"` +} + +type UpdatePurchaseOrderItemRequest struct { + ID *uuid.UUID `json:"id,omitempty"` // For existing items + IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"` + Description *string `json:"description,omitempty" validate:"omitempty"` + Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"` + UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"` + Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"` +} + +type PurchaseOrderResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + VendorID uuid.UUID `json:"vendor_id"` + PONumber string `json:"po_number"` + TransactionDate time.Time `json:"transaction_date"` + DueDate time.Time `json:"due_date"` + Reference *string `json:"reference"` + Status string `json:"status"` + Message *string `json:"message"` + TotalAmount float64 `json:"total_amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Vendor *VendorResponse `json:"vendor,omitempty"` + Items []PurchaseOrderItemResponse `json:"items,omitempty"` + Attachments []PurchaseOrderAttachmentResponse `json:"attachments,omitempty"` +} + +type PurchaseOrderItemResponse struct { + ID uuid.UUID `json:"id"` + PurchaseOrderID uuid.UUID `json:"purchase_order_id"` + IngredientID uuid.UUID `json:"ingredient_id"` + Description *string `json:"description"` + Quantity float64 `json:"quantity"` + UnitID uuid.UUID `json:"unit_id"` + Amount float64 `json:"amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Ingredient *IngredientResponse `json:"ingredient,omitempty"` + Unit *UnitResponse `json:"unit,omitempty"` +} + +type PurchaseOrderAttachmentResponse struct { + ID uuid.UUID `json:"id"` + PurchaseOrderID uuid.UUID `json:"purchase_order_id"` + FileID uuid.UUID `json:"file_id"` + CreatedAt time.Time `json:"created_at"` + File *FileResponse `json:"file,omitempty"` +} + +type ListPurchaseOrdersRequest struct { + Page int `json:"page" validate:"min=1"` + Limit int `json:"limit" validate:"min=1,max=100"` + Search string `json:"search,omitempty"` + Status string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved received cancelled"` + VendorID *uuid.UUID `json:"vendor_id,omitempty"` + StartDate *time.Time `json:"start_date,omitempty"` + EndDate *time.Time `json:"end_date,omitempty"` +} + +type ListPurchaseOrdersResponse struct { + PurchaseOrders []PurchaseOrderResponse `json:"purchase_orders"` + TotalCount int `json:"total_count"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` +} + +// Helper types for ingredient and unit responses +type IngredientResponse struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` +} + +type UnitResponse struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` +} diff --git a/internal/contract/vendor_contract.go b/internal/contract/vendor_contract.go new file mode 100644 index 0000000..9deca2e --- /dev/null +++ b/internal/contract/vendor_contract.go @@ -0,0 +1,62 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateVendorRequest struct { + Name string `json:"name" validate:"required,min=1,max=255"` + Email *string `json:"email,omitempty" validate:"omitempty,email"` + PhoneNumber *string `json:"phone_number,omitempty" validate:"omitempty"` + Address *string `json:"address,omitempty" validate:"omitempty"` + ContactPerson *string `json:"contact_person,omitempty" validate:"omitempty,max=255"` + TaxNumber *string `json:"tax_number,omitempty" validate:"omitempty,max=50"` + PaymentTerms *string `json:"payment_terms,omitempty" validate:"omitempty,max=100"` + Notes *string `json:"notes,omitempty" validate:"omitempty"` + IsActive *bool `json:"is_active,omitempty"` +} + +type UpdateVendorRequest struct { + Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` + Email *string `json:"email,omitempty" validate:"omitempty,email"` + PhoneNumber *string `json:"phone_number,omitempty" validate:"omitempty"` + Address *string `json:"address,omitempty" validate:"omitempty"` + ContactPerson *string `json:"contact_person,omitempty" validate:"omitempty,max=255"` + TaxNumber *string `json:"tax_number,omitempty" validate:"omitempty,max=50"` + PaymentTerms *string `json:"payment_terms,omitempty" validate:"omitempty,max=100"` + Notes *string `json:"notes,omitempty" validate:"omitempty"` + IsActive *bool `json:"is_active,omitempty"` +} + +type VendorResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + Name string `json:"name"` + Email *string `json:"email"` + PhoneNumber *string `json:"phone_number"` + Address *string `json:"address"` + ContactPerson *string `json:"contact_person"` + TaxNumber *string `json:"tax_number"` + PaymentTerms *string `json:"payment_terms"` + Notes *string `json:"notes"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListVendorsRequest struct { + Page int `json:"page" validate:"min=1"` + Limit int `json:"limit" validate:"min=1,max=100"` + Search string `json:"search,omitempty"` + IsActive *bool `json:"is_active,omitempty"` +} + +type ListVendorsResponse struct { + Vendors []VendorResponse `json:"vendors"` + TotalCount int `json:"total_count"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` +} diff --git a/internal/entities/account.go b/internal/entities/account.go new file mode 100644 index 0000000..a8561f9 --- /dev/null +++ b/internal/entities/account.go @@ -0,0 +1,55 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type AccountType string + +const ( + AccountTypeCash AccountType = "cash" + AccountTypeWallet AccountType = "wallet" + AccountTypeBank AccountType = "bank" + AccountTypeCredit AccountType = "credit" + AccountTypeDebit AccountType = "debit" + AccountTypeAsset AccountType = "asset" + AccountTypeLiability AccountType = "liability" + AccountTypeEquity AccountType = "equity" + AccountTypeRevenue AccountType = "revenue" + AccountTypeExpense AccountType = "expense" +) + +type Account 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"` + ChartOfAccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"chart_of_account_id" validate:"required"` + Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"` + Number string `gorm:"not null;size:50" json:"number" validate:"required,min=1,max=50"` + AccountType AccountType `gorm:"not null;size:20" json:"account_type" validate:"required,oneof=cash wallet bank credit debit asset liability equity revenue expense"` + OpeningBalance float64 `gorm:"type:decimal(15,2);default:0.00" json:"opening_balance"` + CurrentBalance float64 `gorm:"type:decimal(15,2);default:0.00" json:"current_balance"` + Description *string `gorm:"type:text" json:"description"` + IsActive bool `gorm:"default:true" json:"is_active"` + IsSystem bool `gorm:"default:false" json:"is_system"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` + Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"` + ChartOfAccount ChartOfAccount `gorm:"foreignKey:ChartOfAccountID" json:"chart_of_account,omitempty"` +} + +func (a *Account) BeforeCreate(tx *gorm.DB) error { + if a.ID == uuid.Nil { + a.ID = uuid.New() + } + return nil +} + +func (Account) TableName() string { + return "accounts" +} diff --git a/internal/entities/chart_of_account.go b/internal/entities/chart_of_account.go new file mode 100644 index 0000000..f8d021d --- /dev/null +++ b/internal/entities/chart_of_account.go @@ -0,0 +1,41 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type ChartOfAccount 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"` + ChartOfAccountTypeID uuid.UUID `gorm:"type:uuid;not null;index" json:"chart_of_account_type_id" validate:"required"` + ParentID *uuid.UUID `gorm:"type:uuid;index" json:"parent_id"` + Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"` + Code string `gorm:"not null;size:20" json:"code" validate:"required,min=1,max=20"` + Description *string `gorm:"type:text" json:"description"` + IsActive bool `gorm:"default:true" json:"is_active"` + IsSystem bool `gorm:"default:false" json:"is_system"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` + Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"` + ChartOfAccountType ChartOfAccountType `gorm:"foreignKey:ChartOfAccountTypeID" json:"chart_of_account_type,omitempty"` + Parent *ChartOfAccount `gorm:"foreignKey:ParentID" json:"parent,omitempty"` + Children []ChartOfAccount `gorm:"foreignKey:ParentID" json:"children,omitempty"` + Accounts []Account `gorm:"foreignKey:ChartOfAccountID" json:"accounts,omitempty"` +} + +func (c *ChartOfAccount) BeforeCreate(tx *gorm.DB) error { + if c.ID == uuid.Nil { + c.ID = uuid.New() + } + return nil +} + +func (ChartOfAccount) TableName() string { + return "chart_of_accounts" +} diff --git a/internal/entities/chart_of_account_type.go b/internal/entities/chart_of_account_type.go new file mode 100644 index 0000000..efc3970 --- /dev/null +++ b/internal/entities/chart_of_account_type.go @@ -0,0 +1,31 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type ChartOfAccountType struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + Name string `gorm:"not null;size:100" json:"name" validate:"required,min=1,max=100"` + Code string `gorm:"not null;size:10;unique" json:"code" validate:"required,min=1,max=10"` + Description *string `gorm:"type:text" json:"description"` + IsActive bool `gorm:"default:true" json:"is_active"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + ChartOfAccounts []ChartOfAccount `gorm:"foreignKey:ChartOfAccountTypeID" json:"chart_of_accounts,omitempty"` +} + +func (c *ChartOfAccountType) BeforeCreate(tx *gorm.DB) error { + if c.ID == uuid.Nil { + c.ID = uuid.New() + } + return nil +} + +func (ChartOfAccountType) TableName() string { + return "chart_of_account_types" +} diff --git a/internal/entities/entities.go b/internal/entities/entities.go index 23a4a91..1805974 100644 --- a/internal/entities/entities.go +++ b/internal/entities/entities.go @@ -18,6 +18,11 @@ func GetAllEntities() []interface{} { &Payment{}, &Customer{}, &Table{}, + &Vendor{}, + &PurchaseOrder{}, + &PurchaseOrderItem{}, + &PurchaseOrderAttachment{}, + &IngredientUnitConverter{}, // Analytics entities are not database tables, they are query results } } diff --git a/internal/entities/ingredient_unit_converter.go b/internal/entities/ingredient_unit_converter.go new file mode 100644 index 0000000..d2a3465 --- /dev/null +++ b/internal/entities/ingredient_unit_converter.go @@ -0,0 +1,42 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" +) + +type IngredientUnitConverter struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"` + IngredientID uuid.UUID `gorm:"type:uuid;not null" json:"ingredient_id"` + FromUnitID uuid.UUID `gorm:"type:uuid;not null" json:"from_unit_id"` + ToUnitID uuid.UUID `gorm:"type:uuid;not null" json:"to_unit_id"` + ConversionFactor float64 `gorm:"type:decimal(15,6);not null" json:"conversion_factor"` + IsActive bool `gorm:"default:true" json:"is_active"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + CreatedBy uuid.UUID `gorm:"type:uuid;not null" json:"created_by"` + UpdatedBy uuid.UUID `gorm:"type:uuid;not null" json:"updated_by"` + + // Relationships + Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` + Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"` + FromUnit *Unit `gorm:"foreignKey:FromUnitID" json:"from_unit,omitempty"` + ToUnit *Unit `gorm:"foreignKey:ToUnitID" json:"to_unit,omitempty"` + CreatedByUser *User `gorm:"foreignKey:CreatedBy" json:"created_by_user,omitempty"` + UpdatedByUser *User `gorm:"foreignKey:UpdatedBy" json:"updated_by_user,omitempty"` +} + +func (IngredientUnitConverter) TableName() string { + return "ingredient_unit_converters" +} + +// BeforeCreate hook to set default values +func (iuc *IngredientUnitConverter) BeforeCreate() error { + if iuc.ID == uuid.Nil { + iuc.ID = uuid.New() + } + return nil +} + diff --git a/internal/entities/purchase_order.go b/internal/entities/purchase_order.go new file mode 100644 index 0000000..f445ef3 --- /dev/null +++ b/internal/entities/purchase_order.go @@ -0,0 +1,91 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + + "gorm.io/gorm" +) + +type PurchaseOrder struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id" validate:"required"` + VendorID uuid.UUID `gorm:"type:uuid;not null" json:"vendor_id" validate:"required"` + PONumber string `gorm:"not null;size:50" json:"po_number" validate:"required,min=1,max=50"` + TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date" validate:"required"` + DueDate time.Time `gorm:"type:date;not null" json:"due_date" validate:"required"` + Reference *string `gorm:"size:100" json:"reference" validate:"omitempty,max=100"` + Status string `gorm:"not null;size:20;default:'draft'" json:"status" validate:"required,oneof=draft sent approved received cancelled"` + Message *string `gorm:"type:text" json:"message" validate:"omitempty"` + TotalAmount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"total_amount"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` + Vendor *Vendor `gorm:"foreignKey:VendorID" json:"vendor,omitempty"` + Items []PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderID" json:"items,omitempty"` + Attachments []PurchaseOrderAttachment `gorm:"foreignKey:PurchaseOrderID" json:"attachments,omitempty"` +} + +func (po *PurchaseOrder) BeforeCreate(tx *gorm.DB) error { + if po.ID == uuid.Nil { + id := uuid.New() + po.ID = id + } + return nil +} + +func (PurchaseOrder) TableName() string { + return "purchase_orders" +} + +type PurchaseOrderItem struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + PurchaseOrderID uuid.UUID `gorm:"type:uuid;not null" json:"purchase_order_id" validate:"required"` + IngredientID uuid.UUID `gorm:"type:uuid;not null" json:"ingredient_id" validate:"required"` + Description *string `gorm:"type:text" json:"description" validate:"omitempty"` + Quantity float64 `gorm:"type:decimal(10,3);not null" json:"quantity" validate:"required,gt=0"` + UnitID uuid.UUID `gorm:"type:uuid;not null" json:"unit_id" validate:"required"` + Amount float64 `gorm:"type:decimal(15,2);not null" json:"amount" validate:"required,gte=0"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + PurchaseOrder *PurchaseOrder `gorm:"foreignKey:PurchaseOrderID" json:"purchase_order,omitempty"` + Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"` + Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"` +} + +func (poi *PurchaseOrderItem) BeforeCreate(tx *gorm.DB) error { + if poi.ID == uuid.Nil { + id := uuid.New() + poi.ID = id + } + return nil +} + +func (PurchaseOrderItem) TableName() string { + return "purchase_order_items" +} + +type PurchaseOrderAttachment struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + PurchaseOrderID uuid.UUID `gorm:"type:uuid;not null" json:"purchase_order_id" validate:"required"` + FileID uuid.UUID `gorm:"type:uuid;not null" json:"file_id" validate:"required"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + + PurchaseOrder *PurchaseOrder `gorm:"foreignKey:PurchaseOrderID" json:"purchase_order,omitempty"` + File *File `gorm:"foreignKey:FileID" json:"file,omitempty"` +} + +func (poa *PurchaseOrderAttachment) BeforeCreate(tx *gorm.DB) error { + if poa.ID == uuid.Nil { + id := uuid.New() + poa.ID = id + } + return nil +} + +func (PurchaseOrderAttachment) TableName() string { + return "purchase_order_attachments" +} diff --git a/internal/entities/vendor.go b/internal/entities/vendor.go new file mode 100644 index 0000000..9b358a4 --- /dev/null +++ b/internal/entities/vendor.go @@ -0,0 +1,39 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + + "gorm.io/gorm" +) + +type Vendor struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id" validate:"required"` + Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"` + Email *string `gorm:"size:255" json:"email" validate:"omitempty,email"` + PhoneNumber *string `gorm:"size:20" json:"phone_number" validate:"omitempty"` + Address *string `gorm:"type:text" json:"address" validate:"omitempty"` + ContactPerson *string `gorm:"size:255" json:"contact_person" validate:"omitempty,max=255"` + TaxNumber *string `gorm:"size:50" json:"tax_number" validate:"omitempty,max=50"` + PaymentTerms *string `gorm:"size:100" json:"payment_terms" validate:"omitempty,max=100"` + Notes *string `gorm:"type:text" json:"notes" validate:"omitempty"` + IsActive bool `gorm:"not null;default:true" json:"is_active"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` +} + +func (v *Vendor) BeforeCreate(tx *gorm.DB) error { + if v.ID == uuid.Nil { + id := uuid.New() + v.ID = id + } + return nil +} + +func (Vendor) TableName() string { + return "vendors" +} diff --git a/internal/handler/account_handler.go b/internal/handler/account_handler.go new file mode 100644 index 0000000..18b5e9a --- /dev/null +++ b/internal/handler/account_handler.go @@ -0,0 +1,197 @@ +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 AccountHandler struct { + service contract.AccountContract + validator validator.AccountValidator +} + +func NewAccountHandler(service contract.AccountContract, validator validator.AccountValidator) *AccountHandler { + return &AccountHandler{ + service: service, + validator: validator, + } +} + +func (h *AccountHandler) CreateAccount(c *gin.Context) { + var req contract.CreateAccountRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "AccountHandler") + return + } + + response, err := h.service.CreateAccount(c, &req) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "AccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "AccountHandler") +} + +func (h *AccountHandler) GetAccountByID(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"}}), "AccountHandler") + return + } + + response, err := h.service.GetAccountByID(c, id) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "AccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "AccountHandler") +} + +func (h *AccountHandler) UpdateAccount(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"}}), "AccountHandler") + return + } + + var req contract.UpdateAccountRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "AccountHandler") + return + } + + response, err := h.service.UpdateAccount(c, id, &req) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "AccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "AccountHandler") +} + +func (h *AccountHandler) DeleteAccount(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"}}), "AccountHandler") + return + } + + err = h.service.DeleteAccount(c, id) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "AccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(gin.H{"message": "Account deleted successfully"}), "AccountHandler") +} + +func (h *AccountHandler) ListAccounts(c *gin.Context) { + var req contract.ListAccountsRequest + if err := c.ShouldBindQuery(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "AccountHandler") + return + } + + response, total, err := h.service.ListAccounts(c, &req) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "AccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(gin.H{ + "data": response, + "total": total, + "page": req.Page, + "limit": req.Limit, + }), "AccountHandler") +} + +func (h *AccountHandler) GetAccountsByOrganization(c *gin.Context) { + organizationIDStr := c.Param("organization_id") + organizationID, err := uuid.Parse(organizationIDStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid organization ID format"}}), "AccountHandler") + return + } + + var outletID *uuid.UUID + if outletIDStr := c.Query("outlet_id"); outletIDStr != "" { + if parsedOutletID, err := uuid.Parse(outletIDStr); err == nil { + outletID = &parsedOutletID + } + } + + response, err := h.service.GetAccountsByOrganization(c, organizationID, outletID) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "AccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "AccountHandler") +} + +func (h *AccountHandler) GetAccountsByChartOfAccount(c *gin.Context) { + chartOfAccountIDStr := c.Param("chart_of_account_id") + chartOfAccountID, err := uuid.Parse(chartOfAccountIDStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid chart of account ID format"}}), "AccountHandler") + return + } + + response, err := h.service.GetAccountsByChartOfAccount(c, chartOfAccountID) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "AccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "AccountHandler") +} + +func (h *AccountHandler) UpdateAccountBalance(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"}}), "AccountHandler") + return + } + + var req contract.UpdateAccountBalanceRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "AccountHandler") + return + } + + err = h.service.UpdateAccountBalance(c, id, &req) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "AccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(gin.H{"message": "Account balance updated successfully"}), "AccountHandler") +} + +func (h *AccountHandler) GetAccountBalance(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"}}), "AccountHandler") + return + } + + balance, err := h.service.GetAccountBalance(c, id) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "AccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(gin.H{"balance": balance}), "AccountHandler") +} diff --git a/internal/handler/chart_of_account_handler.go b/internal/handler/chart_of_account_handler.go new file mode 100644 index 0000000..f641d98 --- /dev/null +++ b/internal/handler/chart_of_account_handler.go @@ -0,0 +1,171 @@ +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 ChartOfAccountHandler struct { + service contract.ChartOfAccountContract + validator validator.ChartOfAccountValidator +} + +func NewChartOfAccountHandler(service contract.ChartOfAccountContract, validator validator.ChartOfAccountValidator) *ChartOfAccountHandler { + return &ChartOfAccountHandler{ + service: service, + validator: validator, + } +} + +func (h *ChartOfAccountHandler) CreateChartOfAccount(c *gin.Context) { + var req contract.CreateChartOfAccountRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountHandler") + return + } + + response, err := h.service.CreateChartOfAccount(c, &req) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "ChartOfAccountHandler") +} + +func (h *ChartOfAccountHandler) GetChartOfAccountByID(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"}}), "ChartOfAccountHandler") + return + } + + response, err := h.service.GetChartOfAccountByID(c, id) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "ChartOfAccountHandler") +} + +func (h *ChartOfAccountHandler) UpdateChartOfAccount(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"}}), "ChartOfAccountHandler") + return + } + + var req contract.UpdateChartOfAccountRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountHandler") + return + } + + response, err := h.service.UpdateChartOfAccount(c, id, &req) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "ChartOfAccountHandler") +} + +func (h *ChartOfAccountHandler) DeleteChartOfAccount(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"}}), "ChartOfAccountHandler") + return + } + + err = h.service.DeleteChartOfAccount(c, id) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(gin.H{"message": "Chart of account deleted successfully"}), "ChartOfAccountHandler") +} + +func (h *ChartOfAccountHandler) ListChartOfAccounts(c *gin.Context) { + var req contract.ListChartOfAccountsRequest + if err := c.ShouldBindQuery(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountHandler") + return + } + + response, total, err := h.service.ListChartOfAccounts(c, &req) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(gin.H{ + "data": response, + "total": total, + "page": req.Page, + "limit": req.Limit, + }), "ChartOfAccountHandler") +} + +func (h *ChartOfAccountHandler) GetChartOfAccountsByOrganization(c *gin.Context) { + organizationIDStr := c.Param("organization_id") + organizationID, err := uuid.Parse(organizationIDStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid organization ID format"}}), "ChartOfAccountHandler") + return + } + + var outletID *uuid.UUID + if outletIDStr := c.Query("outlet_id"); outletIDStr != "" { + if parsedOutletID, err := uuid.Parse(outletIDStr); err == nil { + outletID = &parsedOutletID + } + } + + response, err := h.service.GetChartOfAccountsByOrganization(c, organizationID, outletID) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "ChartOfAccountHandler") +} + +func (h *ChartOfAccountHandler) GetChartOfAccountsByType(c *gin.Context) { + organizationIDStr := c.Param("organization_id") + organizationID, err := uuid.Parse(organizationIDStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid organization ID format"}}), "ChartOfAccountHandler") + return + } + + typeIDStr := c.Param("type_id") + typeID, err := uuid.Parse(typeIDStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid type ID format"}}), "ChartOfAccountHandler") + return + } + + var outletID *uuid.UUID + if outletIDStr := c.Query("outlet_id"); outletIDStr != "" { + if parsedOutletID, err := uuid.Parse(outletIDStr); err == nil { + outletID = &parsedOutletID + } + } + + response, err := h.service.GetChartOfAccountsByType(c, organizationID, typeID, outletID) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "ChartOfAccountHandler") +} diff --git a/internal/handler/chart_of_account_type_handler.go b/internal/handler/chart_of_account_type_handler.go new file mode 100644 index 0000000..a9cc3ff --- /dev/null +++ b/internal/handler/chart_of_account_type_handler.go @@ -0,0 +1,124 @@ +package handler + +import ( + "strconv" + + "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 ChartOfAccountTypeHandler struct { + service contract.ChartOfAccountTypeContract + validator validator.ChartOfAccountTypeValidator +} + +func NewChartOfAccountTypeHandler(service contract.ChartOfAccountTypeContract, validator validator.ChartOfAccountTypeValidator) *ChartOfAccountTypeHandler { + return &ChartOfAccountTypeHandler{ + service: service, + validator: validator, + } +} + +func (h *ChartOfAccountTypeHandler) CreateChartOfAccountType(c *gin.Context) { + var req contract.CreateChartOfAccountTypeRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountTypeHandler") + return + } + + response, err := h.service.CreateChartOfAccountType(c, &req) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountTypeHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "ChartOfAccountTypeHandler") +} + +func (h *ChartOfAccountTypeHandler) GetChartOfAccountTypeByID(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"}}), "ChartOfAccountTypeHandler") + return + } + + response, err := h.service.GetChartOfAccountTypeByID(c, id) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountTypeHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "ChartOfAccountTypeHandler") +} + +func (h *ChartOfAccountTypeHandler) UpdateChartOfAccountType(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"}}), "ChartOfAccountTypeHandler") + return + } + + var req contract.UpdateChartOfAccountTypeRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountTypeHandler") + return + } + + response, err := h.service.UpdateChartOfAccountType(c, id, &req) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountTypeHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "ChartOfAccountTypeHandler") +} + +func (h *ChartOfAccountTypeHandler) DeleteChartOfAccountType(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"}}), "ChartOfAccountTypeHandler") + return + } + + err = h.service.DeleteChartOfAccountType(c, id) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountTypeHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(gin.H{"message": "Chart of account type deleted successfully"}), "ChartOfAccountTypeHandler") +} + +func (h *ChartOfAccountTypeHandler) ListChartOfAccountTypes(c *gin.Context) { + // Parse query parameters + filters := make(map[string]interface{}) + + if isActive := c.Query("is_active"); isActive != "" { + if isActiveBool, err := strconv.ParseBool(isActive); err == nil { + filters["is_active"] = isActiveBool + } + } + + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) + + response, total, err := h.service.ListChartOfAccountTypes(c, filters, page, limit) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "ChartOfAccountTypeHandler") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(gin.H{ + "data": response, + "total": total, + "page": page, + "limit": limit, + }), "ChartOfAccountTypeHandler") +} diff --git a/internal/handler/ingredient_unit_converter_handler.go b/internal/handler/ingredient_unit_converter_handler.go new file mode 100644 index 0000000..dc616c5 --- /dev/null +++ b/internal/handler/ingredient_unit_converter_handler.go @@ -0,0 +1,256 @@ +package handler + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/util" + "apskel-pos-be/internal/validator" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type IngredientUnitConverterHandler struct { + converterService service.IngredientUnitConverterService + converterValidator validator.IngredientUnitConverterValidator +} + +func NewIngredientUnitConverterHandler( + converterService service.IngredientUnitConverterService, + converterValidator validator.IngredientUnitConverterValidator, +) *IngredientUnitConverterHandler { + return &IngredientUnitConverterHandler{ + converterService: converterService, + converterValidator: converterValidator, + } +} + +func (h *IngredientUnitConverterHandler) CreateIngredientUnitConverter(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.CreateIngredientUnitConverterRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("IngredientUnitConverterHandler::CreateIngredientUnitConverter -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientUnitConverterHandler::CreateIngredientUnitConverter") + return + } + + validationError, validationErrorCode := h.converterValidator.ValidateCreateIngredientUnitConverterRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientUnitConverterHandler::CreateIngredientUnitConverter") + return + } + + converterResponse := h.converterService.CreateIngredientUnitConverter(ctx, contextInfo, &req) + if converterResponse.HasErrors() { + errorResp := converterResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("IngredientUnitConverterHandler::CreateIngredientUnitConverter -> Failed to create ingredient unit converter from service") + } + + util.HandleResponse(c.Writer, c.Request, converterResponse, "IngredientUnitConverterHandler::CreateIngredientUnitConverter") +} + +func (h *IngredientUnitConverterHandler) UpdateIngredientUnitConverter(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + converterIDStr := c.Param("id") + converterID, err := uuid.Parse(converterIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("IngredientUnitConverterHandler::UpdateIngredientUnitConverter -> Invalid converter ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid converter ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientUnitConverterHandler::UpdateIngredientUnitConverter") + return + } + + var req contract.UpdateIngredientUnitConverterRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(ctx).WithError(err).Error("IngredientUnitConverterHandler::UpdateIngredientUnitConverter -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientUnitConverterHandler::UpdateIngredientUnitConverter") + return + } + + validationError, validationErrorCode := h.converterValidator.ValidateUpdateIngredientUnitConverterRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientUnitConverterHandler::UpdateIngredientUnitConverter") + return + } + + converterResponse := h.converterService.UpdateIngredientUnitConverter(ctx, contextInfo, converterID, &req) + if converterResponse.HasErrors() { + errorResp := converterResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("IngredientUnitConverterHandler::UpdateIngredientUnitConverter -> Failed to update ingredient unit converter from service") + } + + util.HandleResponse(c.Writer, c.Request, converterResponse, "IngredientUnitConverterHandler::UpdateIngredientUnitConverter") +} + +func (h *IngredientUnitConverterHandler) DeleteIngredientUnitConverter(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + converterIDStr := c.Param("id") + converterID, err := uuid.Parse(converterIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("IngredientUnitConverterHandler::DeleteIngredientUnitConverter -> Invalid converter ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid converter ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientUnitConverterHandler::DeleteIngredientUnitConverter") + return + } + + converterResponse := h.converterService.DeleteIngredientUnitConverter(ctx, contextInfo, converterID) + if converterResponse.HasErrors() { + errorResp := converterResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("IngredientUnitConverterHandler::DeleteIngredientUnitConverter -> Failed to delete ingredient unit converter from service") + } + + util.HandleResponse(c.Writer, c.Request, converterResponse, "IngredientUnitConverterHandler::DeleteIngredientUnitConverter") +} + +func (h *IngredientUnitConverterHandler) GetIngredientUnitConverter(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + converterIDStr := c.Param("id") + converterID, err := uuid.Parse(converterIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("IngredientUnitConverterHandler::GetIngredientUnitConverter -> Invalid converter ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid converter ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientUnitConverterHandler::GetIngredientUnitConverter") + return + } + + converterResponse := h.converterService.GetIngredientUnitConverter(ctx, contextInfo, converterID) + if converterResponse.HasErrors() { + errorResp := converterResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("IngredientUnitConverterHandler::GetIngredientUnitConverter -> Failed to get ingredient unit converter from service") + } + + util.HandleResponse(c.Writer, c.Request, converterResponse, "IngredientUnitConverterHandler::GetIngredientUnitConverter") +} + +func (h *IngredientUnitConverterHandler) ListIngredientUnitConverters(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + req := &contract.ListIngredientUnitConvertersRequest{ + Page: 1, + Limit: 10, + } + + // Parse query parameters + if pageStr := c.Query("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil { + req.Page = page + } + } + + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil { + req.Limit = limit + } + } + + if search := c.Query("search"); search != "" { + req.Search = search + } + + if ingredientIDStr := c.Query("ingredient_id"); ingredientIDStr != "" { + if ingredientID, err := uuid.Parse(ingredientIDStr); err == nil { + req.IngredientID = &ingredientID + } + } + + if fromUnitIDStr := c.Query("from_unit_id"); fromUnitIDStr != "" { + if fromUnitID, err := uuid.Parse(fromUnitIDStr); err == nil { + req.FromUnitID = &fromUnitID + } + } + + if toUnitIDStr := c.Query("to_unit_id"); toUnitIDStr != "" { + if toUnitID, err := uuid.Parse(toUnitIDStr); err == nil { + req.ToUnitID = &toUnitID + } + } + + if isActiveStr := c.Query("is_active"); isActiveStr != "" { + if isActive, err := strconv.ParseBool(isActiveStr); err == nil { + req.IsActive = &isActive + } + } + + validationError, validationErrorCode := h.converterValidator.ValidateListIngredientUnitConvertersRequest(req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientUnitConverterHandler::ListIngredientUnitConverters") + return + } + + converterResponse := h.converterService.ListIngredientUnitConverters(ctx, contextInfo, req) + if converterResponse.HasErrors() { + errorResp := converterResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("IngredientUnitConverterHandler::ListIngredientUnitConverters -> Failed to list ingredient unit converters from service") + } + + util.HandleResponse(c.Writer, c.Request, converterResponse, "IngredientUnitConverterHandler::ListIngredientUnitConverters") +} + +func (h *IngredientUnitConverterHandler) GetConvertersForIngredient(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::GetConvertersForIngredient -> 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::GetConvertersForIngredient") + return + } + + converterResponse := h.converterService.GetConvertersForIngredient(ctx, contextInfo, ingredientID) + if converterResponse.HasErrors() { + errorResp := converterResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("IngredientUnitConverterHandler::GetConvertersForIngredient -> Failed to get converters for ingredient from service") + } + + util.HandleResponse(c.Writer, c.Request, converterResponse, "IngredientUnitConverterHandler::GetConvertersForIngredient") +} + +func (h *IngredientUnitConverterHandler) ConvertUnit(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.ConvertUnitRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("IngredientUnitConverterHandler::ConvertUnit -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientUnitConverterHandler::ConvertUnit") + return + } + + validationError, validationErrorCode := h.converterValidator.ValidateConvertUnitRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientUnitConverterHandler::ConvertUnit") + return + } + + converterResponse := h.converterService.ConvertUnit(ctx, contextInfo, &req) + if converterResponse.HasErrors() { + errorResp := converterResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("IngredientUnitConverterHandler::ConvertUnit -> Failed to convert unit from service") + } + + util.HandleResponse(c.Writer, c.Request, converterResponse, "IngredientUnitConverterHandler::ConvertUnit") +} + diff --git a/internal/handler/purchase_order_handler.go b/internal/handler/purchase_order_handler.go new file mode 100644 index 0000000..6be1cff --- /dev/null +++ b/internal/handler/purchase_order_handler.go @@ -0,0 +1,267 @@ +package handler + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/util" + "strconv" + "time" + + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/validator" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type PurchaseOrderHandler struct { + purchaseOrderService service.PurchaseOrderService + purchaseOrderValidator validator.PurchaseOrderValidator +} + +func NewPurchaseOrderHandler( + purchaseOrderService service.PurchaseOrderService, + purchaseOrderValidator validator.PurchaseOrderValidator, +) *PurchaseOrderHandler { + return &PurchaseOrderHandler{ + purchaseOrderService: purchaseOrderService, + purchaseOrderValidator: purchaseOrderValidator, + } +} + +func (h *PurchaseOrderHandler) CreatePurchaseOrder(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.CreatePurchaseOrderRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("PurchaseOrderHandler::CreatePurchaseOrder -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseOrderHandler::CreatePurchaseOrder") + return + } + + validationError, validationErrorCode := h.purchaseOrderValidator.ValidateCreatePurchaseOrderRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseOrderHandler::CreatePurchaseOrder") + return + } + + poResponse := h.purchaseOrderService.CreatePurchaseOrder(ctx, contextInfo, &req) + if poResponse.HasErrors() { + errorResp := poResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("PurchaseOrderHandler::CreatePurchaseOrder -> Failed to create purchase order from service") + } + + util.HandleResponse(c.Writer, c.Request, poResponse, "PurchaseOrderHandler::CreatePurchaseOrder") +} + +func (h *PurchaseOrderHandler) UpdatePurchaseOrder(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + poIDStr := c.Param("id") + poID, err := uuid.Parse(poIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("PurchaseOrderHandler::UpdatePurchaseOrder -> Invalid purchase order ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase order ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseOrderHandler::UpdatePurchaseOrder") + return + } + + var req contract.UpdatePurchaseOrderRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(ctx).WithError(err).Error("PurchaseOrderHandler::UpdatePurchaseOrder -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseOrderHandler::UpdatePurchaseOrder") + return + } + + validationError, validationErrorCode := h.purchaseOrderValidator.ValidateUpdatePurchaseOrderRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseOrderHandler::UpdatePurchaseOrder") + return + } + + poResponse := h.purchaseOrderService.UpdatePurchaseOrder(ctx, contextInfo, poID, &req) + if poResponse.HasErrors() { + errorResp := poResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("PurchaseOrderHandler::UpdatePurchaseOrder -> Failed to update purchase order from service") + } + + util.HandleResponse(c.Writer, c.Request, poResponse, "PurchaseOrderHandler::UpdatePurchaseOrder") +} + +func (h *PurchaseOrderHandler) DeletePurchaseOrder(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + poIDStr := c.Param("id") + poID, err := uuid.Parse(poIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("PurchaseOrderHandler::DeletePurchaseOrder -> Invalid purchase order ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase order ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseOrderHandler::DeletePurchaseOrder") + return + } + + poResponse := h.purchaseOrderService.DeletePurchaseOrder(ctx, contextInfo, poID) + if poResponse.HasErrors() { + errorResp := poResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("PurchaseOrderHandler::DeletePurchaseOrder -> Failed to delete purchase order from service") + } + + util.HandleResponse(c.Writer, c.Request, poResponse, "PurchaseOrderHandler::DeletePurchaseOrder") +} + +func (h *PurchaseOrderHandler) GetPurchaseOrder(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + poIDStr := c.Param("id") + poID, err := uuid.Parse(poIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("PurchaseOrderHandler::GetPurchaseOrder -> Invalid purchase order ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase order ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseOrderHandler::GetPurchaseOrder") + return + } + + poResponse := h.purchaseOrderService.GetPurchaseOrderByID(ctx, contextInfo, poID) + if poResponse.HasErrors() { + errorResp := poResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("PurchaseOrderHandler::GetPurchaseOrder -> Failed to get purchase order from service") + } + + util.HandleResponse(c.Writer, c.Request, poResponse, "PurchaseOrderHandler::GetPurchaseOrder") +} + +func (h *PurchaseOrderHandler) ListPurchaseOrders(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + req := &contract.ListPurchaseOrdersRequest{ + Page: 1, + Limit: 10, + } + + // Parse query parameters + if pageStr := c.Query("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil { + req.Page = page + } + } + + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil { + req.Limit = limit + } + } + + if search := c.Query("search"); search != "" { + req.Search = search + } + + if status := c.Query("status"); status != "" { + req.Status = status + } + + if vendorIDStr := c.Query("vendor_id"); vendorIDStr != "" { + if vendorID, err := uuid.Parse(vendorIDStr); err == nil { + req.VendorID = &vendorID + } + } + + if startDateStr := c.Query("start_date"); startDateStr != "" { + if startDate, err := time.Parse("2006-01-02", startDateStr); err == nil { + req.StartDate = &startDate + } + } + + if endDateStr := c.Query("end_date"); endDateStr != "" { + if endDate, err := time.Parse("2006-01-02", endDateStr); err == nil { + req.EndDate = &endDate + } + } + + validationError, validationErrorCode := h.purchaseOrderValidator.ValidateListPurchaseOrdersRequest(req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseOrderHandler::ListPurchaseOrders") + return + } + + poResponse := h.purchaseOrderService.ListPurchaseOrders(ctx, contextInfo, req) + if poResponse.HasErrors() { + errorResp := poResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("PurchaseOrderHandler::ListPurchaseOrders -> Failed to list purchase orders from service") + } + + util.HandleResponse(c.Writer, c.Request, poResponse, "PurchaseOrderHandler::ListPurchaseOrders") +} + +func (h *PurchaseOrderHandler) GetPurchaseOrdersByStatus(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + status := c.Param("status") + if status == "" { + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Status parameter is required") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseOrderHandler::GetPurchaseOrdersByStatus") + return + } + + poResponse := h.purchaseOrderService.GetPurchaseOrdersByStatus(ctx, contextInfo, status) + if poResponse.HasErrors() { + errorResp := poResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("PurchaseOrderHandler::GetPurchaseOrdersByStatus -> Failed to get purchase orders by status from service") + } + + util.HandleResponse(c.Writer, c.Request, poResponse, "PurchaseOrderHandler::GetPurchaseOrdersByStatus") +} + +func (h *PurchaseOrderHandler) GetOverduePurchaseOrders(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + poResponse := h.purchaseOrderService.GetOverduePurchaseOrders(ctx, contextInfo) + if poResponse.HasErrors() { + errorResp := poResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("PurchaseOrderHandler::GetOverduePurchaseOrders -> Failed to get overdue purchase orders from service") + } + + util.HandleResponse(c.Writer, c.Request, poResponse, "PurchaseOrderHandler::GetOverduePurchaseOrders") +} + +func (h *PurchaseOrderHandler) UpdatePurchaseOrderStatus(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + poIDStr := c.Param("id") + poID, err := uuid.Parse(poIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("PurchaseOrderHandler::UpdatePurchaseOrderStatus -> Invalid purchase order ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase order ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseOrderHandler::UpdatePurchaseOrderStatus") + return + } + + status := c.Param("status") + if status == "" { + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Status parameter is required") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseOrderHandler::UpdatePurchaseOrderStatus") + return + } + + poResponse := h.purchaseOrderService.UpdatePurchaseOrderStatus(ctx, contextInfo, poID, status) + if poResponse.HasErrors() { + errorResp := poResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("PurchaseOrderHandler::UpdatePurchaseOrderStatus -> Failed to update purchase order status from service") + } + + util.HandleResponse(c.Writer, c.Request, poResponse, "PurchaseOrderHandler::UpdatePurchaseOrderStatus") +} diff --git a/internal/handler/vendor_handler.go b/internal/handler/vendor_handler.go new file mode 100644 index 0000000..918302f --- /dev/null +++ b/internal/handler/vendor_handler.go @@ -0,0 +1,201 @@ +package handler + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/util" + "strconv" + + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/validator" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type VendorHandler struct { + vendorService service.VendorService + vendorValidator validator.VendorValidator +} + +func NewVendorHandler( + vendorService service.VendorService, + vendorValidator validator.VendorValidator, +) *VendorHandler { + return &VendorHandler{ + vendorService: vendorService, + vendorValidator: vendorValidator, + } +} + +func (h *VendorHandler) CreateVendor(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.CreateVendorRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("VendorHandler::CreateVendor -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "VendorHandler::CreateVendor") + return + } + + validationError, validationErrorCode := h.vendorValidator.ValidateCreateVendorRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "VendorHandler::CreateVendor") + return + } + + vendorResponse := h.vendorService.CreateVendor(ctx, contextInfo, &req) + if vendorResponse.HasErrors() { + errorResp := vendorResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("VendorHandler::CreateVendor -> Failed to create vendor from service") + } + + util.HandleResponse(c.Writer, c.Request, vendorResponse, "VendorHandler::CreateVendor") +} + +func (h *VendorHandler) UpdateVendor(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + vendorIDStr := c.Param("id") + vendorID, err := uuid.Parse(vendorIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("VendorHandler::UpdateVendor -> Invalid vendor ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid vendor ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "VendorHandler::UpdateVendor") + return + } + + var req contract.UpdateVendorRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(ctx).WithError(err).Error("VendorHandler::UpdateVendor -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "VendorHandler::UpdateVendor") + return + } + + validationError, validationErrorCode := h.vendorValidator.ValidateUpdateVendorRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "VendorHandler::UpdateVendor") + return + } + + vendorResponse := h.vendorService.UpdateVendor(ctx, contextInfo, vendorID, &req) + if vendorResponse.HasErrors() { + errorResp := vendorResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("VendorHandler::UpdateVendor -> Failed to update vendor from service") + } + + util.HandleResponse(c.Writer, c.Request, vendorResponse, "VendorHandler::UpdateVendor") +} + +func (h *VendorHandler) DeleteVendor(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + vendorIDStr := c.Param("id") + vendorID, err := uuid.Parse(vendorIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("VendorHandler::DeleteVendor -> Invalid vendor ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid vendor ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "VendorHandler::DeleteVendor") + return + } + + vendorResponse := h.vendorService.DeleteVendor(ctx, contextInfo, vendorID) + if vendorResponse.HasErrors() { + errorResp := vendorResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("VendorHandler::DeleteVendor -> Failed to delete vendor from service") + } + + util.HandleResponse(c.Writer, c.Request, vendorResponse, "VendorHandler::DeleteVendor") +} + +func (h *VendorHandler) GetVendor(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + vendorIDStr := c.Param("id") + vendorID, err := uuid.Parse(vendorIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("VendorHandler::GetVendor -> Invalid vendor ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid vendor ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "VendorHandler::GetVendor") + return + } + + vendorResponse := h.vendorService.GetVendorByID(ctx, contextInfo, vendorID) + if vendorResponse.HasErrors() { + errorResp := vendorResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("VendorHandler::GetVendor -> Failed to get vendor from service") + } + + util.HandleResponse(c.Writer, c.Request, vendorResponse, "VendorHandler::GetVendor") +} + +func (h *VendorHandler) ListVendors(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + req := &contract.ListVendorsRequest{ + Page: 1, + Limit: 10, + } + + // Parse query parameters + if pageStr := c.Query("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil { + req.Page = page + } + } + + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil { + req.Limit = limit + } + } + + if search := c.Query("search"); search != "" { + req.Search = search + } + + if isActiveStr := c.Query("is_active"); isActiveStr != "" { + if isActive, err := strconv.ParseBool(isActiveStr); err == nil { + req.IsActive = &isActive + } + } + + validationError, validationErrorCode := h.vendorValidator.ValidateListVendorsRequest(req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "VendorHandler::ListVendors") + return + } + + vendorResponse := h.vendorService.ListVendors(ctx, contextInfo, req) + if vendorResponse.HasErrors() { + errorResp := vendorResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("VendorHandler::ListVendors -> Failed to list vendors from service") + } + + util.HandleResponse(c.Writer, c.Request, vendorResponse, "VendorHandler::ListVendors") +} + +func (h *VendorHandler) GetActiveVendors(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + vendorResponse := h.vendorService.GetActiveVendors(ctx, contextInfo) + if vendorResponse.HasErrors() { + errorResp := vendorResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("VendorHandler::GetActiveVendors -> Failed to get active vendors from service") + } + + util.HandleResponse(c.Writer, c.Request, vendorResponse, "VendorHandler::GetActiveVendors") +} diff --git a/internal/mappers/account_mapper.go b/internal/mappers/account_mapper.go new file mode 100644 index 0000000..0a93f6d --- /dev/null +++ b/internal/mappers/account_mapper.go @@ -0,0 +1,73 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + +func AccountEntityToResponse(entity *entities.Account) *models.AccountResponse { + response := &models.AccountResponse{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + OutletID: entity.OutletID, + ChartOfAccountID: entity.ChartOfAccountID, + Name: entity.Name, + Number: entity.Number, + AccountType: string(entity.AccountType), + OpeningBalance: entity.OpeningBalance, + CurrentBalance: entity.CurrentBalance, + Description: entity.Description, + IsActive: entity.IsActive, + IsSystem: entity.IsSystem, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } + + if entity.ChartOfAccount.ID != uuid.Nil { + response.ChartOfAccount = ChartOfAccountEntityToResponse(&entity.ChartOfAccount) + } + + return response +} + +func AccountCreateRequestToEntity(req *models.CreateAccountRequest, organizationID uuid.UUID, outletID *uuid.UUID) *entities.Account { + return &entities.Account{ + OrganizationID: organizationID, + OutletID: outletID, + ChartOfAccountID: req.ChartOfAccountID, + Name: req.Name, + Number: req.Number, + AccountType: entities.AccountType(req.AccountType), + OpeningBalance: req.OpeningBalance, + CurrentBalance: req.OpeningBalance, // Initialize current balance with opening balance + Description: req.Description, + IsActive: true, + IsSystem: false, + } +} + +func AccountUpdateRequestToEntity(entity *entities.Account, req *models.UpdateAccountRequest) { + if req.ChartOfAccountID != nil { + entity.ChartOfAccountID = *req.ChartOfAccountID + } + if req.Name != nil { + entity.Name = *req.Name + } + if req.Number != nil { + entity.Number = *req.Number + } + if req.AccountType != nil { + entity.AccountType = entities.AccountType(*req.AccountType) + } + if req.OpeningBalance != nil { + entity.OpeningBalance = *req.OpeningBalance + } + if req.Description != nil { + entity.Description = req.Description + } + if req.IsActive != nil { + entity.IsActive = *req.IsActive + } +} diff --git a/internal/mappers/chart_of_account_mapper.go b/internal/mappers/chart_of_account_mapper.go new file mode 100644 index 0000000..c991742 --- /dev/null +++ b/internal/mappers/chart_of_account_mapper.go @@ -0,0 +1,77 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + +func ChartOfAccountEntityToResponse(entity *entities.ChartOfAccount) *models.ChartOfAccountResponse { + response := &models.ChartOfAccountResponse{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + OutletID: entity.OutletID, + ChartOfAccountTypeID: entity.ChartOfAccountTypeID, + ParentID: entity.ParentID, + Name: entity.Name, + Code: entity.Code, + Description: entity.Description, + IsActive: entity.IsActive, + IsSystem: entity.IsSystem, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } + + if entity.ChartOfAccountType.ID != uuid.Nil { + response.ChartOfAccountType = ChartOfAccountTypeEntityToResponse(&entity.ChartOfAccountType) + } + + if entity.Parent != nil { + response.Parent = ChartOfAccountEntityToResponse(entity.Parent) + } + + if len(entity.Children) > 0 { + response.Children = make([]models.ChartOfAccountResponse, len(entity.Children)) + for i, child := range entity.Children { + response.Children[i] = *ChartOfAccountEntityToResponse(&child) + } + } + + return response +} + +func ChartOfAccountCreateRequestToEntity(req *models.CreateChartOfAccountRequest, organizationID uuid.UUID, outletID *uuid.UUID) *entities.ChartOfAccount { + return &entities.ChartOfAccount{ + OrganizationID: organizationID, + OutletID: outletID, + ChartOfAccountTypeID: req.ChartOfAccountTypeID, + ParentID: req.ParentID, + Name: req.Name, + Code: req.Code, + Description: req.Description, + IsActive: true, + IsSystem: false, + } +} + +func ChartOfAccountUpdateRequestToEntity(entity *entities.ChartOfAccount, req *models.UpdateChartOfAccountRequest) { + if req.ChartOfAccountTypeID != nil { + entity.ChartOfAccountTypeID = *req.ChartOfAccountTypeID + } + if req.ParentID != nil { + entity.ParentID = req.ParentID + } + if req.Name != nil { + entity.Name = *req.Name + } + if req.Code != nil { + entity.Code = *req.Code + } + if req.Description != nil { + entity.Description = req.Description + } + if req.IsActive != nil { + entity.IsActive = *req.IsActive + } +} diff --git a/internal/mappers/chart_of_account_type_mapper.go b/internal/mappers/chart_of_account_type_mapper.go new file mode 100644 index 0000000..60f4c86 --- /dev/null +++ b/internal/mappers/chart_of_account_type_mapper.go @@ -0,0 +1,42 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +func ChartOfAccountTypeEntityToResponse(entity *entities.ChartOfAccountType) *models.ChartOfAccountTypeResponse { + return &models.ChartOfAccountTypeResponse{ + ID: entity.ID, + Name: entity.Name, + Code: entity.Code, + Description: entity.Description, + IsActive: entity.IsActive, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func ChartOfAccountTypeCreateRequestToEntity(req *models.CreateChartOfAccountTypeRequest) *entities.ChartOfAccountType { + return &entities.ChartOfAccountType{ + Name: req.Name, + Code: req.Code, + Description: req.Description, + IsActive: true, + } +} + +func ChartOfAccountTypeUpdateRequestToEntity(entity *entities.ChartOfAccountType, req *models.UpdateChartOfAccountTypeRequest) { + if req.Name != nil { + entity.Name = *req.Name + } + if req.Code != nil { + entity.Code = *req.Code + } + if req.Description != nil { + entity.Description = req.Description + } + if req.IsActive != nil { + entity.IsActive = *req.IsActive + } +} diff --git a/internal/mappers/contract_mapper.go b/internal/mappers/contract_mapper.go new file mode 100644 index 0000000..51adb48 --- /dev/null +++ b/internal/mappers/contract_mapper.go @@ -0,0 +1,168 @@ +package mappers + +import ( + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" + "time" +) + +// Chart of Account Type Mappers +func ContractToModelCreateChartOfAccountTypeRequest(req *contract.CreateChartOfAccountTypeRequest) *models.CreateChartOfAccountTypeRequest { + return &models.CreateChartOfAccountTypeRequest{ + Name: req.Name, + Code: req.Code, + Description: req.Description, + } +} + +func ContractToModelUpdateChartOfAccountTypeRequest(req *contract.UpdateChartOfAccountTypeRequest) *models.UpdateChartOfAccountTypeRequest { + return &models.UpdateChartOfAccountTypeRequest{ + Name: req.Name, + Code: req.Code, + Description: req.Description, + IsActive: req.IsActive, + } +} + +func ModelToContractChartOfAccountTypeResponse(resp *models.ChartOfAccountTypeResponse) *contract.ChartOfAccountTypeResponse { + return &contract.ChartOfAccountTypeResponse{ + ID: resp.ID, + Name: resp.Name, + Code: resp.Code, + Description: resp.Description, + IsActive: resp.IsActive, + CreatedAt: resp.CreatedAt.Format(time.RFC3339), + UpdatedAt: resp.UpdatedAt.Format(time.RFC3339), + } +} + +// Chart of Account Mappers +func ContractToModelCreateChartOfAccountRequest(req *contract.CreateChartOfAccountRequest) *models.CreateChartOfAccountRequest { + return &models.CreateChartOfAccountRequest{ + ChartOfAccountTypeID: req.ChartOfAccountTypeID, + ParentID: req.ParentID, + Name: req.Name, + Code: req.Code, + Description: req.Description, + } +} + +func ContractToModelUpdateChartOfAccountRequest(req *contract.UpdateChartOfAccountRequest) *models.UpdateChartOfAccountRequest { + return &models.UpdateChartOfAccountRequest{ + ChartOfAccountTypeID: req.ChartOfAccountTypeID, + ParentID: req.ParentID, + Name: req.Name, + Code: req.Code, + Description: req.Description, + IsActive: req.IsActive, + } +} + +func ContractToModelListChartOfAccountsRequest(req *contract.ListChartOfAccountsRequest) *models.ListChartOfAccountsRequest { + return &models.ListChartOfAccountsRequest{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + ChartOfAccountTypeID: req.ChartOfAccountTypeID, + ParentID: req.ParentID, + IsActive: req.IsActive, + IsSystem: req.IsSystem, + Page: req.Page, + Limit: req.Limit, + } +} + +func ModelToContractChartOfAccountResponse(resp *models.ChartOfAccountResponse) *contract.ChartOfAccountResponse { + contractResp := &contract.ChartOfAccountResponse{ + ID: resp.ID, + OrganizationID: resp.OrganizationID, + OutletID: resp.OutletID, + ChartOfAccountTypeID: resp.ChartOfAccountTypeID, + ParentID: resp.ParentID, + Name: resp.Name, + Code: resp.Code, + Description: resp.Description, + IsActive: resp.IsActive, + IsSystem: resp.IsSystem, + CreatedAt: resp.CreatedAt.Format(time.RFC3339), + UpdatedAt: resp.UpdatedAt.Format(time.RFC3339), + } + + if resp.ChartOfAccountType != nil { + contractResp.ChartOfAccountType = ModelToContractChartOfAccountTypeResponse(resp.ChartOfAccountType) + } + + if resp.Parent != nil { + contractResp.Parent = ModelToContractChartOfAccountResponse(resp.Parent) + } + + if len(resp.Children) > 0 { + contractResp.Children = make([]contract.ChartOfAccountResponse, len(resp.Children)) + for i, child := range resp.Children { + contractResp.Children[i] = *ModelToContractChartOfAccountResponse(&child) + } + } + + return contractResp +} + +// Account Mappers +func ContractToModelCreateAccountRequest(req *contract.CreateAccountRequest) *models.CreateAccountRequest { + return &models.CreateAccountRequest{ + ChartOfAccountID: req.ChartOfAccountID, + Name: req.Name, + Number: req.Number, + AccountType: req.AccountType, + OpeningBalance: req.OpeningBalance, + Description: req.Description, + } +} + +func ContractToModelUpdateAccountRequest(req *contract.UpdateAccountRequest) *models.UpdateAccountRequest { + return &models.UpdateAccountRequest{ + ChartOfAccountID: req.ChartOfAccountID, + Name: req.Name, + Number: req.Number, + AccountType: req.AccountType, + OpeningBalance: req.OpeningBalance, + Description: req.Description, + IsActive: req.IsActive, + } +} + +func ContractToModelListAccountsRequest(req *contract.ListAccountsRequest) *models.ListAccountsRequest { + return &models.ListAccountsRequest{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + ChartOfAccountID: req.ChartOfAccountID, + AccountType: req.AccountType, + IsActive: req.IsActive, + IsSystem: req.IsSystem, + Page: req.Page, + Limit: req.Limit, + } +} + +func ModelToContractAccountResponse(resp *models.AccountResponse) *contract.AccountResponse { + contractResp := &contract.AccountResponse{ + ID: resp.ID, + OrganizationID: resp.OrganizationID, + OutletID: resp.OutletID, + ChartOfAccountID: resp.ChartOfAccountID, + Name: resp.Name, + Number: resp.Number, + AccountType: resp.AccountType, + OpeningBalance: resp.OpeningBalance, + CurrentBalance: resp.CurrentBalance, + Description: resp.Description, + IsActive: resp.IsActive, + IsSystem: resp.IsSystem, + CreatedAt: resp.CreatedAt.Format(time.RFC3339), + UpdatedAt: resp.UpdatedAt.Format(time.RFC3339), + } + + if resp.ChartOfAccount != nil { + contractResp.ChartOfAccount = ModelToContractChartOfAccountResponse(resp.ChartOfAccount) + } + + return contractResp +} diff --git a/internal/mappers/ingredient_mapper.go b/internal/mappers/ingredient_mapper.go index 6d26cb7..03fd646 100644 --- a/internal/mappers/ingredient_mapper.go +++ b/internal/mappers/ingredient_mapper.go @@ -74,3 +74,26 @@ func MapIngredientModelsToEntities(models []*models.Ingredient) []*entities.Ingr return entities } + +// Entity to Response conversions +func MapIngredientEntityToResponse(entity *entities.Ingredient) *models.IngredientResponse { + if entity == nil { + return nil + } + + return &models.IngredientResponse{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + OutletID: entity.OutletID, + Name: entity.Name, + UnitID: entity.UnitID, + Cost: entity.Cost, + Stock: entity.Stock, + IsSemiFinished: entity.IsSemiFinished, + IsActive: entity.IsActive, + Metadata: entity.Metadata, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + Unit: MapUnitEntityToModel(entity.Unit), + } +} diff --git a/internal/mappers/ingredient_unit_converter_mapper.go b/internal/mappers/ingredient_unit_converter_mapper.go new file mode 100644 index 0000000..fa6ed90 --- /dev/null +++ b/internal/mappers/ingredient_unit_converter_mapper.go @@ -0,0 +1,91 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +// Entity to Model conversions +func IngredientUnitConverterEntityToModel(entity *entities.IngredientUnitConverter) *models.IngredientUnitConverter { + if entity == nil { + return nil + } + + model := &models.IngredientUnitConverter{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + IngredientID: entity.IngredientID, + FromUnitID: entity.FromUnitID, + ToUnitID: entity.ToUnitID, + ConversionFactor: entity.ConversionFactor, + IsActive: entity.IsActive, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + CreatedBy: entity.CreatedBy, + UpdatedBy: entity.UpdatedBy, + } + + // Map related entities + if entity.Ingredient != nil { + model.Ingredient = MapIngredientEntityToModel(entity.Ingredient) + } + if entity.FromUnit != nil { + model.FromUnit = MapUnitEntityToModel(entity.FromUnit) + } + if entity.ToUnit != nil { + model.ToUnit = MapUnitEntityToModel(entity.ToUnit) + } + + return model +} + +// Entity to Response conversions +func IngredientUnitConverterEntityToResponse(entity *entities.IngredientUnitConverter) *models.IngredientUnitConverterResponse { + if entity == nil { + return nil + } + + response := &models.IngredientUnitConverterResponse{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + IngredientID: entity.IngredientID, + FromUnitID: entity.FromUnitID, + ToUnitID: entity.ToUnitID, + ConversionFactor: entity.ConversionFactor, + IsActive: entity.IsActive, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + CreatedBy: entity.CreatedBy, + UpdatedBy: entity.UpdatedBy, + } + + // Map related entities + if entity.Ingredient != nil { + response.Ingredient = MapIngredientEntityToResponse(entity.Ingredient) + } + if entity.FromUnit != nil { + response.FromUnit = MapUnitEntityToResponse(entity.FromUnit) + } + if entity.ToUnit != nil { + response.ToUnit = MapUnitEntityToResponse(entity.ToUnit) + } + + return response +} + +// Batch conversion methods +func IngredientUnitConverterEntitiesToModels(entities []*entities.IngredientUnitConverter) []*models.IngredientUnitConverter { + models := make([]*models.IngredientUnitConverter, len(entities)) + for i, entity := range entities { + models[i] = IngredientUnitConverterEntityToModel(entity) + } + return models +} + +func IngredientUnitConverterEntitiesToResponses(entities []*entities.IngredientUnitConverter) []*models.IngredientUnitConverterResponse { + responses := make([]*models.IngredientUnitConverterResponse, len(entities)) + for i, entity := range entities { + responses[i] = IngredientUnitConverterEntityToResponse(entity) + } + return responses +} diff --git a/internal/mappers/purchase_order_mapper.go b/internal/mappers/purchase_order_mapper.go new file mode 100644 index 0000000..8b3cf9e --- /dev/null +++ b/internal/mappers/purchase_order_mapper.go @@ -0,0 +1,273 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +func PurchaseOrderEntityToModel(entity *entities.PurchaseOrder) *models.PurchaseOrder { + if entity == nil { + return nil + } + + return &models.PurchaseOrder{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + VendorID: entity.VendorID, + PONumber: entity.PONumber, + TransactionDate: entity.TransactionDate, + DueDate: entity.DueDate, + Reference: entity.Reference, + Status: entity.Status, + Message: entity.Message, + TotalAmount: entity.TotalAmount, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func PurchaseOrderModelToEntity(model *models.PurchaseOrder) *entities.PurchaseOrder { + if model == nil { + return nil + } + + return &entities.PurchaseOrder{ + ID: model.ID, + OrganizationID: model.OrganizationID, + VendorID: model.VendorID, + PONumber: model.PONumber, + TransactionDate: model.TransactionDate, + DueDate: model.DueDate, + Reference: model.Reference, + Status: model.Status, + Message: model.Message, + TotalAmount: model.TotalAmount, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +func PurchaseOrderEntityToResponse(entity *entities.PurchaseOrder) *models.PurchaseOrderResponse { + if entity == nil { + return nil + } + + response := &models.PurchaseOrderResponse{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + VendorID: entity.VendorID, + PONumber: entity.PONumber, + TransactionDate: entity.TransactionDate, + DueDate: entity.DueDate, + Reference: entity.Reference, + Status: entity.Status, + Message: entity.Message, + TotalAmount: entity.TotalAmount, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } + + // Map vendor if present + if entity.Vendor != nil { + response.Vendor = VendorEntityToResponse(entity.Vendor) + } + + // Map items if present + if entity.Items != nil { + response.Items = PurchaseOrderItemEntitiesToResponses(entity.Items) + } + + // Map attachments if present + if entity.Attachments != nil { + response.Attachments = PurchaseOrderAttachmentEntitiesToResponses(entity.Attachments) + } + + return response +} + +func PurchaseOrderItemEntityToModel(entity *entities.PurchaseOrderItem) *models.PurchaseOrderItem { + if entity == nil { + return nil + } + + return &models.PurchaseOrderItem{ + ID: entity.ID, + PurchaseOrderID: entity.PurchaseOrderID, + IngredientID: entity.IngredientID, + Description: entity.Description, + Quantity: entity.Quantity, + UnitID: entity.UnitID, + Amount: entity.Amount, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func PurchaseOrderItemModelToEntity(model *models.PurchaseOrderItem) *entities.PurchaseOrderItem { + if model == nil { + return nil + } + + return &entities.PurchaseOrderItem{ + ID: model.ID, + PurchaseOrderID: model.PurchaseOrderID, + IngredientID: model.IngredientID, + Description: model.Description, + Quantity: model.Quantity, + UnitID: model.UnitID, + Amount: model.Amount, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *models.PurchaseOrderItemResponse { + if entity == nil { + return nil + } + + response := &models.PurchaseOrderItemResponse{ + ID: entity.ID, + PurchaseOrderID: entity.PurchaseOrderID, + IngredientID: entity.IngredientID, + Description: entity.Description, + Quantity: entity.Quantity, + UnitID: entity.UnitID, + Amount: entity.Amount, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } + + // Map ingredient if present + if entity.Ingredient != nil { + response.Ingredient = &models.IngredientResponse{ + ID: entity.Ingredient.ID, + Name: entity.Ingredient.Name, + } + } + + // Map unit if present + if entity.Unit != nil { + response.Unit = &models.UnitResponse{ + ID: entity.Unit.ID, + Name: entity.Unit.Name, + } + } + + return response +} + +func PurchaseOrderAttachmentEntityToModel(entity *entities.PurchaseOrderAttachment) *models.PurchaseOrderAttachment { + if entity == nil { + return nil + } + + return &models.PurchaseOrderAttachment{ + ID: entity.ID, + PurchaseOrderID: entity.PurchaseOrderID, + FileID: entity.FileID, + CreatedAt: entity.CreatedAt, + } +} + +func PurchaseOrderAttachmentModelToEntity(model *models.PurchaseOrderAttachment) *entities.PurchaseOrderAttachment { + if model == nil { + return nil + } + + return &entities.PurchaseOrderAttachment{ + ID: model.ID, + PurchaseOrderID: model.PurchaseOrderID, + FileID: model.FileID, + CreatedAt: model.CreatedAt, + } +} + +func PurchaseOrderAttachmentEntityToResponse(entity *entities.PurchaseOrderAttachment) *models.PurchaseOrderAttachmentResponse { + if entity == nil { + return nil + } + + response := &models.PurchaseOrderAttachmentResponse{ + ID: entity.ID, + PurchaseOrderID: entity.PurchaseOrderID, + FileID: entity.FileID, + CreatedAt: entity.CreatedAt, + } + + // Map file if present + if entity.File != nil { + response.File = &models.FileResponse{ + ID: entity.File.ID, + FileName: entity.File.FileName, + OriginalName: entity.File.OriginalName, + FileURL: entity.File.FileURL, + FileSize: entity.File.FileSize, + MimeType: entity.File.MimeType, + FileType: entity.File.FileType, + IsPublic: entity.File.IsPublic, + CreatedAt: entity.File.CreatedAt, + UpdatedAt: entity.File.UpdatedAt, + } + } + + return response +} + +// Batch conversion methods +func PurchaseOrderEntitiesToModels(entities []*entities.PurchaseOrder) []*models.PurchaseOrder { + if entities == nil { + return nil + } + + models := make([]*models.PurchaseOrder, len(entities)) + for i, entity := range entities { + models[i] = PurchaseOrderEntityToModel(entity) + } + return models +} + +func PurchaseOrderEntitiesToResponses(entities []*entities.PurchaseOrder) []models.PurchaseOrderResponse { + if entities == nil { + return nil + } + + responses := make([]models.PurchaseOrderResponse, len(entities)) + for i, entity := range entities { + response := PurchaseOrderEntityToResponse(entity) + if response != nil { + responses[i] = *response + } + } + return responses +} + +func PurchaseOrderItemEntitiesToResponses(entities []entities.PurchaseOrderItem) []models.PurchaseOrderItemResponse { + if entities == nil { + return nil + } + + responses := make([]models.PurchaseOrderItemResponse, len(entities)) + for i, entity := range entities { + response := PurchaseOrderItemEntityToResponse(&entity) + if response != nil { + responses[i] = *response + } + } + return responses +} + +func PurchaseOrderAttachmentEntitiesToResponses(entities []entities.PurchaseOrderAttachment) []models.PurchaseOrderAttachmentResponse { + if entities == nil { + return nil + } + + responses := make([]models.PurchaseOrderAttachmentResponse, len(entities)) + for i, entity := range entities { + response := PurchaseOrderAttachmentEntityToResponse(&entity) + if response != nil { + responses[i] = *response + } + } + return responses +} diff --git a/internal/mappers/unit_mapper.go b/internal/mappers/unit_mapper.go index a98af08..39c7ac7 100644 --- a/internal/mappers/unit_mapper.go +++ b/internal/mappers/unit_mapper.go @@ -66,3 +66,22 @@ func MapUnitModelsToEntities(models []*models.Unit) []*entities.Unit { return entities } + +// Entity to Response conversions +func MapUnitEntityToResponse(entity *entities.Unit) *models.UnitResponse { + if entity == nil { + return nil + } + + return &models.UnitResponse{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + OutletID: entity.OutletID, + Name: entity.Name, + Abbreviation: entity.Abbreviation, + IsActive: entity.IsActive, + DeletedAt: entity.DeletedAt, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} diff --git a/internal/mappers/vendor_mapper.go b/internal/mappers/vendor_mapper.go new file mode 100644 index 0000000..99f54bd --- /dev/null +++ b/internal/mappers/vendor_mapper.go @@ -0,0 +1,96 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +func VendorEntityToModel(entity *entities.Vendor) *models.Vendor { + if entity == nil { + return nil + } + + return &models.Vendor{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + Name: entity.Name, + Email: entity.Email, + PhoneNumber: entity.PhoneNumber, + Address: entity.Address, + ContactPerson: entity.ContactPerson, + TaxNumber: entity.TaxNumber, + PaymentTerms: entity.PaymentTerms, + Notes: entity.Notes, + IsActive: entity.IsActive, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func VendorModelToEntity(model *models.Vendor) *entities.Vendor { + if model == nil { + return nil + } + + return &entities.Vendor{ + ID: model.ID, + OrganizationID: model.OrganizationID, + Name: model.Name, + Email: model.Email, + PhoneNumber: model.PhoneNumber, + Address: model.Address, + ContactPerson: model.ContactPerson, + TaxNumber: model.TaxNumber, + PaymentTerms: model.PaymentTerms, + Notes: model.Notes, + IsActive: model.IsActive, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +func VendorEntityToResponse(entity *entities.Vendor) *models.VendorResponse { + if entity == nil { + return nil + } + + return &models.VendorResponse{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + Name: entity.Name, + Email: entity.Email, + PhoneNumber: entity.PhoneNumber, + Address: entity.Address, + ContactPerson: entity.ContactPerson, + TaxNumber: entity.TaxNumber, + PaymentTerms: entity.PaymentTerms, + Notes: entity.Notes, + IsActive: entity.IsActive, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func VendorEntitiesToModels(entities []*entities.Vendor) []*models.Vendor { + if entities == nil { + return nil + } + + models := make([]*models.Vendor, len(entities)) + for i, entity := range entities { + models[i] = VendorEntityToModel(entity) + } + return models +} + +func VendorEntitiesToResponses(entities []*entities.Vendor) []*models.VendorResponse { + if entities == nil { + return nil + } + + responses := make([]*models.VendorResponse, len(entities)) + for i, entity := range entities { + responses[i] = VendorEntityToResponse(entity) + } + return responses +} diff --git a/internal/models/account.go b/internal/models/account.go new file mode 100644 index 0000000..8c943d6 --- /dev/null +++ b/internal/models/account.go @@ -0,0 +1,55 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type AccountResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` + ChartOfAccountID uuid.UUID `json:"chart_of_account_id"` + Name string `json:"name"` + Number string `json:"number"` + AccountType string `json:"account_type"` + OpeningBalance float64 `json:"opening_balance"` + CurrentBalance float64 `json:"current_balance"` + Description *string `json:"description"` + IsActive bool `json:"is_active"` + IsSystem bool `json:"is_system"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ChartOfAccount *ChartOfAccountResponse `json:"chart_of_account,omitempty"` +} + +type CreateAccountRequest struct { + ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"` + Name string `json:"name" validate:"required,min=1,max=255"` + Number string `json:"number" validate:"required,min=1,max=50"` + AccountType string `json:"account_type" validate:"required,oneof=cash wallet bank credit debit asset liability equity revenue expense"` + OpeningBalance float64 `json:"opening_balance"` + Description *string `json:"description"` +} + +type UpdateAccountRequest struct { + ChartOfAccountID *uuid.UUID `json:"chart_of_account_id"` + Name *string `json:"name" validate:"omitempty,min=1,max=255"` + Number *string `json:"number" validate:"omitempty,min=1,max=50"` + AccountType *string `json:"account_type" validate:"omitempty,oneof=cash wallet bank credit debit asset liability equity revenue expense"` + OpeningBalance *float64 `json:"opening_balance"` + Description *string `json:"description"` + IsActive *bool `json:"is_active"` +} + +type ListAccountsRequest struct { + OrganizationID *uuid.UUID `form:"organization_id"` + OutletID *uuid.UUID `form:"outlet_id"` + ChartOfAccountID *uuid.UUID `form:"chart_of_account_id"` + AccountType *string `form:"account_type"` + IsActive *bool `form:"is_active"` + IsSystem *bool `form:"is_system"` + Page int `form:"page,default=1"` + Limit int `form:"limit,default=10"` +} diff --git a/internal/models/chart_of_account.go b/internal/models/chart_of_account.go new file mode 100644 index 0000000..3f5fa9e --- /dev/null +++ b/internal/models/chart_of_account.go @@ -0,0 +1,53 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type ChartOfAccountResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` + ChartOfAccountTypeID uuid.UUID `json:"chart_of_account_type_id"` + ParentID *uuid.UUID `json:"parent_id"` + Name string `json:"name"` + Code string `json:"code"` + Description *string `json:"description"` + IsActive bool `json:"is_active"` + IsSystem bool `json:"is_system"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ChartOfAccountType *ChartOfAccountTypeResponse `json:"chart_of_account_type,omitempty"` + Parent *ChartOfAccountResponse `json:"parent,omitempty"` + Children []ChartOfAccountResponse `json:"children,omitempty"` +} + +type CreateChartOfAccountRequest struct { + ChartOfAccountTypeID uuid.UUID `json:"chart_of_account_type_id" validate:"required"` + ParentID *uuid.UUID `json:"parent_id"` + Name string `json:"name" validate:"required,min=1,max=255"` + Code string `json:"code" validate:"required,min=1,max=20"` + Description *string `json:"description"` +} + +type UpdateChartOfAccountRequest struct { + ChartOfAccountTypeID *uuid.UUID `json:"chart_of_account_type_id"` + ParentID *uuid.UUID `json:"parent_id"` + Name *string `json:"name" validate:"omitempty,min=1,max=255"` + Code *string `json:"code" validate:"omitempty,min=1,max=20"` + Description *string `json:"description"` + IsActive *bool `json:"is_active"` +} + +type ListChartOfAccountsRequest struct { + OrganizationID *uuid.UUID `form:"organization_id"` + OutletID *uuid.UUID `form:"outlet_id"` + ChartOfAccountTypeID *uuid.UUID `form:"chart_of_account_type_id"` + ParentID *uuid.UUID `form:"parent_id"` + IsActive *bool `form:"is_active"` + IsSystem *bool `form:"is_system"` + Page int `form:"page,default=1"` + Limit int `form:"limit,default=10"` +} diff --git a/internal/models/chart_of_account_type.go b/internal/models/chart_of_account_type.go new file mode 100644 index 0000000..fcfaab7 --- /dev/null +++ b/internal/models/chart_of_account_type.go @@ -0,0 +1,30 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type ChartOfAccountTypeResponse struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + Description *string `json:"description"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreateChartOfAccountTypeRequest struct { + Name string `json:"name" validate:"required,min=1,max=100"` + Code string `json:"code" validate:"required,min=1,max=10"` + Description *string `json:"description"` +} + +type UpdateChartOfAccountTypeRequest struct { + Name *string `json:"name" validate:"omitempty,min=1,max=100"` + Code *string `json:"code" validate:"omitempty,min=1,max=10"` + Description *string `json:"description"` + IsActive *bool `json:"is_active"` +} diff --git a/internal/models/ingredient_unit_converter.go b/internal/models/ingredient_unit_converter.go new file mode 100644 index 0000000..3a287e0 --- /dev/null +++ b/internal/models/ingredient_unit_converter.go @@ -0,0 +1,96 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +// IngredientUnitConverter represents the unit converter for ingredients +type IngredientUnitConverter struct { + ID uuid.UUID + OrganizationID uuid.UUID + IngredientID uuid.UUID + FromUnitID uuid.UUID + ToUnitID uuid.UUID + ConversionFactor float64 + IsActive bool + CreatedAt time.Time + UpdatedAt time.Time + CreatedBy uuid.UUID + UpdatedBy uuid.UUID + + // Related entities + Ingredient *Ingredient + FromUnit *Unit + ToUnit *Unit +} + +// Request DTOs +type CreateIngredientUnitConverterRequest struct { + IngredientID uuid.UUID `validate:"required"` + FromUnitID uuid.UUID `validate:"required"` + ToUnitID uuid.UUID `validate:"required"` + ConversionFactor float64 `validate:"required,gt=0"` + IsActive *bool `validate:"omitempty"` +} + +type UpdateIngredientUnitConverterRequest struct { + FromUnitID *uuid.UUID `validate:"omitempty"` + ToUnitID *uuid.UUID `validate:"omitempty"` + ConversionFactor *float64 `validate:"omitempty,gt=0"` + IsActive *bool `validate:"omitempty"` +} + +type ListIngredientUnitConvertersRequest struct { + IngredientID *uuid.UUID + FromUnitID *uuid.UUID + ToUnitID *uuid.UUID + IsActive *bool + Search string + Page int `validate:"required,min=1"` + Limit int `validate:"required,min=1,max=100"` +} + +type ConvertUnitRequest struct { + IngredientID uuid.UUID `validate:"required"` + FromUnitID uuid.UUID `validate:"required"` + ToUnitID uuid.UUID `validate:"required"` + Quantity float64 `validate:"required,gt=0"` +} + +// Response DTOs +type IngredientUnitConverterResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + IngredientID uuid.UUID `json:"ingredient_id"` + FromUnitID uuid.UUID `json:"from_unit_id"` + ToUnitID uuid.UUID `json:"to_unit_id"` + ConversionFactor float64 `json:"conversion_factor"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + CreatedBy uuid.UUID `json:"created_by"` + UpdatedBy uuid.UUID `json:"updated_by"` + Ingredient *IngredientResponse `json:"ingredient,omitempty"` + FromUnit *UnitResponse `json:"from_unit,omitempty"` + ToUnit *UnitResponse `json:"to_unit,omitempty"` +} + +type ConvertUnitResponse struct { + FromQuantity float64 `json:"from_quantity"` + FromUnit *UnitResponse `json:"from_unit"` + ToQuantity float64 `json:"to_quantity"` + ToUnit *UnitResponse `json:"to_unit"` + ConversionFactor float64 `json:"conversion_factor"` + Ingredient *IngredientResponse `json:"ingredient,omitempty"` +} + +type ListIngredientUnitConvertersResponse struct { + Converters []IngredientUnitConverterResponse `json:"converters"` + TotalCount int `json:"total_count"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` +} + diff --git a/internal/models/models.go b/internal/models/models.go index 1e3dc03..6e69339 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -26,5 +26,10 @@ func GetAllModelNames() []string { "PaymentMethod", "Payment", "Customer", + "Vendor", + "PurchaseOrder", + "PurchaseOrderItem", + "PurchaseOrderAttachment", + "IngredientUnitConverter", } } diff --git a/internal/models/purchase_order.go b/internal/models/purchase_order.go new file mode 100644 index 0000000..eebf009 --- /dev/null +++ b/internal/models/purchase_order.go @@ -0,0 +1,140 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type PurchaseOrder struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + VendorID uuid.UUID `json:"vendor_id"` + PONumber string `json:"po_number"` + TransactionDate time.Time `json:"transaction_date"` + DueDate time.Time `json:"due_date"` + Reference *string `json:"reference"` + Status string `json:"status"` + Message *string `json:"message"` + TotalAmount float64 `json:"total_amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type PurchaseOrderItem struct { + ID uuid.UUID `json:"id"` + PurchaseOrderID uuid.UUID `json:"purchase_order_id"` + IngredientID uuid.UUID `json:"ingredient_id"` + Description *string `json:"description"` + Quantity float64 `json:"quantity"` + UnitID uuid.UUID `json:"unit_id"` + Amount float64 `json:"amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type PurchaseOrderAttachment struct { + ID uuid.UUID `json:"id"` + PurchaseOrderID uuid.UUID `json:"purchase_order_id"` + FileID uuid.UUID `json:"file_id"` + CreatedAt time.Time `json:"created_at"` +} + +type PurchaseOrderResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + VendorID uuid.UUID `json:"vendor_id"` + PONumber string `json:"po_number"` + TransactionDate time.Time `json:"transaction_date"` + DueDate time.Time `json:"due_date"` + Reference *string `json:"reference"` + Status string `json:"status"` + Message *string `json:"message"` + TotalAmount float64 `json:"total_amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Vendor *VendorResponse `json:"vendor,omitempty"` + Items []PurchaseOrderItemResponse `json:"items,omitempty"` + Attachments []PurchaseOrderAttachmentResponse `json:"attachments,omitempty"` +} + +type PurchaseOrderItemResponse struct { + ID uuid.UUID `json:"id"` + PurchaseOrderID uuid.UUID `json:"purchase_order_id"` + IngredientID uuid.UUID `json:"ingredient_id"` + Description *string `json:"description"` + Quantity float64 `json:"quantity"` + UnitID uuid.UUID `json:"unit_id"` + Amount float64 `json:"amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Ingredient *IngredientResponse `json:"ingredient,omitempty"` + Unit *UnitResponse `json:"unit,omitempty"` +} + +type PurchaseOrderAttachmentResponse struct { + ID uuid.UUID `json:"id"` + PurchaseOrderID uuid.UUID `json:"purchase_order_id"` + FileID uuid.UUID `json:"file_id"` + CreatedAt time.Time `json:"created_at"` + File *FileResponse `json:"file,omitempty"` +} + +type CreatePurchaseOrderRequest struct { + VendorID uuid.UUID `json:"vendor_id"` + PONumber string `json:"po_number"` + TransactionDate time.Time `json:"transaction_date"` + DueDate time.Time `json:"due_date"` + Reference *string `json:"reference,omitempty"` + Status *string `json:"status,omitempty"` + Message *string `json:"message,omitempty"` + Items []CreatePurchaseOrderItemRequest `json:"items"` + AttachmentFileIDs []uuid.UUID `json:"attachment_file_ids,omitempty"` +} + +type CreatePurchaseOrderItemRequest struct { + IngredientID uuid.UUID `json:"ingredient_id"` + Description *string `json:"description,omitempty"` + Quantity float64 `json:"quantity"` + UnitID uuid.UUID `json:"unit_id"` + Amount float64 `json:"amount"` +} + +type UpdatePurchaseOrderRequest struct { + VendorID *uuid.UUID `json:"vendor_id,omitempty"` + PONumber *string `json:"po_number,omitempty"` + TransactionDate *time.Time `json:"transaction_date,omitempty"` + DueDate *time.Time `json:"due_date,omitempty"` + Reference *string `json:"reference,omitempty"` + Status *string `json:"status,omitempty"` + Message *string `json:"message,omitempty"` + Items []UpdatePurchaseOrderItemRequest `json:"items,omitempty"` + AttachmentFileIDs []uuid.UUID `json:"attachment_file_ids,omitempty"` +} + +type UpdatePurchaseOrderItemRequest struct { + ID *uuid.UUID `json:"id,omitempty"` // For existing items + IngredientID *uuid.UUID `json:"ingredient_id,omitempty"` + Description *string `json:"description,omitempty"` + Quantity *float64 `json:"quantity,omitempty"` + UnitID *uuid.UUID `json:"unit_id,omitempty"` + Amount *float64 `json:"amount,omitempty"` +} + +type ListPurchaseOrdersRequest struct { + Page int `json:"page" validate:"min=1"` + Limit int `json:"limit" validate:"min=1,max=100"` + Search string `json:"search,omitempty"` + Status string `json:"status,omitempty"` + VendorID *uuid.UUID `json:"vendor_id,omitempty"` + StartDate *time.Time `json:"start_date,omitempty"` + EndDate *time.Time `json:"end_date,omitempty"` +} + +type ListPurchaseOrdersResponse struct { + PurchaseOrders []PurchaseOrderResponse `json:"purchase_orders"` + TotalCount int `json:"total_count"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` +} diff --git a/internal/models/vendor.go b/internal/models/vendor.go new file mode 100644 index 0000000..b4aaf8a --- /dev/null +++ b/internal/models/vendor.go @@ -0,0 +1,78 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type Vendor struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + Name string `json:"name"` + Email *string `json:"email"` + PhoneNumber *string `json:"phone_number"` + Address *string `json:"address"` + ContactPerson *string `json:"contact_person"` + TaxNumber *string `json:"tax_number"` + PaymentTerms *string `json:"payment_terms"` + Notes *string `json:"notes"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type VendorResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + Name string `json:"name"` + Email *string `json:"email"` + PhoneNumber *string `json:"phone_number"` + Address *string `json:"address"` + ContactPerson *string `json:"contact_person"` + TaxNumber *string `json:"tax_number"` + PaymentTerms *string `json:"payment_terms"` + Notes *string `json:"notes"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreateVendorRequest struct { + Name string `json:"name"` + Email *string `json:"email,omitempty"` + PhoneNumber *string `json:"phone_number,omitempty"` + Address *string `json:"address,omitempty"` + ContactPerson *string `json:"contact_person,omitempty"` + TaxNumber *string `json:"tax_number,omitempty"` + PaymentTerms *string `json:"payment_terms,omitempty"` + Notes *string `json:"notes,omitempty"` + IsActive *bool `json:"is_active,omitempty"` +} + +type UpdateVendorRequest struct { + Name *string `json:"name,omitempty"` + Email *string `json:"email,omitempty"` + PhoneNumber *string `json:"phone_number,omitempty"` + Address *string `json:"address,omitempty"` + ContactPerson *string `json:"contact_person,omitempty"` + TaxNumber *string `json:"tax_number,omitempty"` + PaymentTerms *string `json:"payment_terms,omitempty"` + Notes *string `json:"notes,omitempty"` + IsActive *bool `json:"is_active,omitempty"` +} + +type ListVendorsRequest struct { + Page int `json:"page"` + Limit int `json:"limit"` + Search string `json:"search,omitempty"` + IsActive *bool `json:"is_active,omitempty"` +} + +type ListVendorsResponse struct { + Vendors []*VendorResponse `json:"vendors"` + TotalCount int `json:"total_count"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` +} diff --git a/internal/processor/account_processor.go b/internal/processor/account_processor.go new file mode 100644 index 0000000..00fd05b --- /dev/null +++ b/internal/processor/account_processor.go @@ -0,0 +1,207 @@ +package processor + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + + "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) + UpdateAccount(ctx context.Context, id uuid.UUID, req *models.UpdateAccountRequest) (*models.AccountResponse, error) + DeleteAccount(ctx context.Context, id uuid.UUID) error + ListAccounts(ctx context.Context, req *models.ListAccountsRequest) ([]models.AccountResponse, int, error) + GetAccountsByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]models.AccountResponse, error) + GetAccountsByChartOfAccount(ctx context.Context, chartOfAccountID uuid.UUID) ([]models.AccountResponse, error) + UpdateAccountBalance(ctx context.Context, id uuid.UUID, amount float64) error + GetAccountBalance(ctx context.Context, id uuid.UUID) (float64, error) +} + +type AccountProcessorImpl struct { + accountRepo AccountRepository + chartOfAccountRepo ChartOfAccountRepository +} + +func NewAccountProcessorImpl(accountRepo AccountRepository, chartOfAccountRepo ChartOfAccountRepository) *AccountProcessorImpl { + return &AccountProcessorImpl{ + accountRepo: accountRepo, + chartOfAccountRepo: chartOfAccountRepo, + } +} + +func (p *AccountProcessorImpl) CreateAccount(ctx context.Context, req *models.CreateAccountRequest) (*models.AccountResponse, error) { + // Get organization and outlet from context + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + var outletID *uuid.UUID + if appCtx.OutletID != uuid.Nil { + outletID = &appCtx.OutletID + } + + // Check if account number already exists for this organization/outlet + existing, err := p.accountRepo.GetByNumber(ctx, organizationID, req.Number, outletID) + if err == nil && existing != nil { + return nil, fmt.Errorf("account with number %s already exists", req.Number) + } + + // Validate chart of account exists + _, err = p.chartOfAccountRepo.GetByID(ctx, req.ChartOfAccountID) + if err != nil { + return nil, fmt.Errorf("chart of account not found: %w", err) + } + + entity := mappers.AccountCreateRequestToEntity(req, organizationID, outletID) + err = p.accountRepo.Create(ctx, entity) + if err != nil { + return nil, fmt.Errorf("failed to create account: %w", err) + } + + return mappers.AccountEntityToResponse(entity), nil +} + +func (p *AccountProcessorImpl) GetAccountByID(ctx context.Context, id uuid.UUID) (*models.AccountResponse, error) { + entity, err := p.accountRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("account not found: %w", err) + } + + return mappers.AccountEntityToResponse(entity), nil +} + +func (p *AccountProcessorImpl) UpdateAccount(ctx context.Context, id uuid.UUID, req *models.UpdateAccountRequest) (*models.AccountResponse, error) { + entity, err := p.accountRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("account not found: %w", err) + } + + // Check if new number already exists (if number is being updated) + if req.Number != nil && *req.Number != entity.Number { + existing, err := p.accountRepo.GetByNumber(ctx, entity.OrganizationID, *req.Number, entity.OutletID) + if err == nil && existing != nil { + return nil, fmt.Errorf("account with number %s already exists", *req.Number) + } + } + + // Validate chart of account exists if provided + if req.ChartOfAccountID != nil { + _, err = p.chartOfAccountRepo.GetByID(ctx, *req.ChartOfAccountID) + if err != nil { + return nil, fmt.Errorf("chart of account not found: %w", err) + } + } + + mappers.AccountUpdateRequestToEntity(entity, req) + err = p.accountRepo.Update(ctx, entity) + if err != nil { + return nil, fmt.Errorf("failed to update account: %w", err) + } + + return mappers.AccountEntityToResponse(entity), nil +} + +func (p *AccountProcessorImpl) DeleteAccount(ctx context.Context, id uuid.UUID) error { + entity, err := p.accountRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("account not found: %w", err) + } + + // Prevent deletion of system accounts + if entity.IsSystem { + return fmt.Errorf("cannot delete system account") + } + + err = p.accountRepo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete account: %w", err) + } + + return nil +} + +func (p *AccountProcessorImpl) ListAccounts(ctx context.Context, req *models.ListAccountsRequest) ([]models.AccountResponse, int, error) { + // Get organization and outlet from context + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + var outletID *uuid.UUID + if appCtx.OutletID != uuid.Nil { + outletID = &appCtx.OutletID + } + + filterEntity := &entities.Account{ + OrganizationID: organizationID, + OutletID: outletID, + ChartOfAccountID: *req.ChartOfAccountID, + AccountType: entities.AccountType(*req.AccountType), + } + + entities, total, err := p.accountRepo.List(ctx, filterEntity) + if err != nil { + return nil, 0, fmt.Errorf("failed to list accounts: %w", err) + } + + responses := make([]models.AccountResponse, len(entities)) + for i, entity := range entities { + responses[i] = *mappers.AccountEntityToResponse(entity) + } + + return responses, total, nil +} + +func (p *AccountProcessorImpl) GetAccountsByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]models.AccountResponse, error) { + entities, err := p.accountRepo.GetByOrganization(ctx, organizationID, outletID) + if err != nil { + return nil, fmt.Errorf("failed to get accounts by organization: %w", err) + } + + responses := make([]models.AccountResponse, len(entities)) + for i, entity := range entities { + responses[i] = *mappers.AccountEntityToResponse(entity) + } + + return responses, nil +} + +func (p *AccountProcessorImpl) GetAccountsByChartOfAccount(ctx context.Context, chartOfAccountID uuid.UUID) ([]models.AccountResponse, error) { + entities, err := p.accountRepo.GetByChartOfAccount(ctx, chartOfAccountID) + if err != nil { + return nil, fmt.Errorf("failed to get accounts by chart of account: %w", err) + } + + responses := make([]models.AccountResponse, len(entities)) + for i, entity := range entities { + responses[i] = *mappers.AccountEntityToResponse(entity) + } + + return responses, nil +} + +func (p *AccountProcessorImpl) UpdateAccountBalance(ctx context.Context, id uuid.UUID, amount float64) error { + _, err := p.accountRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("account not found: %w", err) + } + + err = p.accountRepo.UpdateBalance(ctx, id, amount) + if err != nil { + return fmt.Errorf("failed to update account balance: %w", err) + } + + return nil +} + +func (p *AccountProcessorImpl) GetAccountBalance(ctx context.Context, id uuid.UUID) (float64, error) { + balance, err := p.accountRepo.GetBalance(ctx, id) + if err != nil { + return 0, fmt.Errorf("failed to get account balance: %w", err) + } + + return balance, nil +} diff --git a/internal/processor/chart_of_account_processor.go b/internal/processor/chart_of_account_processor.go new file mode 100644 index 0000000..fc10571 --- /dev/null +++ b/internal/processor/chart_of_account_processor.go @@ -0,0 +1,206 @@ +package processor + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + + +type ChartOfAccountProcessor interface { + CreateChartOfAccount(ctx context.Context, req *models.CreateChartOfAccountRequest) (*models.ChartOfAccountResponse, error) + GetChartOfAccountByID(ctx context.Context, id uuid.UUID) (*models.ChartOfAccountResponse, error) + UpdateChartOfAccount(ctx context.Context, id uuid.UUID, req *models.UpdateChartOfAccountRequest) (*models.ChartOfAccountResponse, error) + DeleteChartOfAccount(ctx context.Context, id uuid.UUID) error + ListChartOfAccounts(ctx context.Context, req *models.ListChartOfAccountsRequest) ([]models.ChartOfAccountResponse, int, error) + GetChartOfAccountsByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]models.ChartOfAccountResponse, error) + GetChartOfAccountsByType(ctx context.Context, organizationID uuid.UUID, chartOfAccountTypeID uuid.UUID, outletID *uuid.UUID) ([]models.ChartOfAccountResponse, error) + CreateDefaultChartOfAccounts(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) error +} + +type ChartOfAccountProcessorImpl struct { + chartOfAccountRepo ChartOfAccountRepository + chartOfAccountTypeRepo ChartOfAccountTypeRepository +} + +func NewChartOfAccountProcessorImpl(chartOfAccountRepo ChartOfAccountRepository, chartOfAccountTypeRepo ChartOfAccountTypeRepository) *ChartOfAccountProcessorImpl { + return &ChartOfAccountProcessorImpl{ + chartOfAccountRepo: chartOfAccountRepo, + chartOfAccountTypeRepo: chartOfAccountTypeRepo, + } +} + +func (p *ChartOfAccountProcessorImpl) CreateChartOfAccount(ctx context.Context, req *models.CreateChartOfAccountRequest) (*models.ChartOfAccountResponse, error) { + // Get organization and outlet from context + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + var outletID *uuid.UUID + if appCtx.OutletID != uuid.Nil { + outletID = &appCtx.OutletID + } + + // Check if code already exists for this organization/outlet + existing, err := p.chartOfAccountRepo.GetByCode(ctx, organizationID, req.Code, outletID) + if err == nil && existing != nil { + return nil, fmt.Errorf("chart of account with code %s already exists", req.Code) + } + + // Validate parent exists if provided + if req.ParentID != nil { + _, err := p.chartOfAccountRepo.GetByID(ctx, *req.ParentID) + if err != nil { + return nil, fmt.Errorf("parent chart of account not found: %w", err) + } + } + + // Validate chart of account type exists + _, err = p.chartOfAccountTypeRepo.GetByID(ctx, req.ChartOfAccountTypeID) + if err != nil { + return nil, fmt.Errorf("chart of account type not found: %w", err) + } + + entity := mappers.ChartOfAccountCreateRequestToEntity(req, organizationID, outletID) + err = p.chartOfAccountRepo.Create(ctx, entity) + if err != nil { + return nil, fmt.Errorf("failed to create chart of account: %w", err) + } + + return mappers.ChartOfAccountEntityToResponse(entity), nil +} + +func (p *ChartOfAccountProcessorImpl) GetChartOfAccountByID(ctx context.Context, id uuid.UUID) (*models.ChartOfAccountResponse, error) { + entity, err := p.chartOfAccountRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("chart of account not found: %w", err) + } + + return mappers.ChartOfAccountEntityToResponse(entity), nil +} + +func (p *ChartOfAccountProcessorImpl) UpdateChartOfAccount(ctx context.Context, id uuid.UUID, req *models.UpdateChartOfAccountRequest) (*models.ChartOfAccountResponse, error) { + entity, err := p.chartOfAccountRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("chart of account not found: %w", err) + } + + // Check if new code already exists (if code is being updated) + if req.Code != nil && *req.Code != entity.Code { + existing, err := p.chartOfAccountRepo.GetByCode(ctx, entity.OrganizationID, *req.Code, entity.OutletID) + if err == nil && existing != nil { + return nil, fmt.Errorf("chart of account with code %s already exists", *req.Code) + } + } + + // Validate parent exists if provided + if req.ParentID != nil { + _, err := p.chartOfAccountRepo.GetByID(ctx, *req.ParentID) + if err != nil { + return nil, fmt.Errorf("parent chart of account not found: %w", err) + } + } + + // Validate chart of account type exists if provided + if req.ChartOfAccountTypeID != nil { + _, err = p.chartOfAccountTypeRepo.GetByID(ctx, *req.ChartOfAccountTypeID) + if err != nil { + return nil, fmt.Errorf("chart of account type not found: %w", err) + } + } + + mappers.ChartOfAccountUpdateRequestToEntity(entity, req) + err = p.chartOfAccountRepo.Update(ctx, entity) + if err != nil { + return nil, fmt.Errorf("failed to update chart of account: %w", err) + } + + return mappers.ChartOfAccountEntityToResponse(entity), nil +} + +func (p *ChartOfAccountProcessorImpl) DeleteChartOfAccount(ctx context.Context, id uuid.UUID) error { + entity, err := p.chartOfAccountRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("chart of account not found: %w", err) + } + + // Prevent deletion of system accounts + if entity.IsSystem { + return fmt.Errorf("cannot delete system chart of account") + } + + err = p.chartOfAccountRepo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete chart of account: %w", err) + } + + return nil +} + +func (p *ChartOfAccountProcessorImpl) ListChartOfAccounts(ctx context.Context, req *models.ListChartOfAccountsRequest) ([]models.ChartOfAccountResponse, int, error) { + // Get organization and outlet from context + appCtx := appcontext.FromGinContext(ctx) + organizationID := appCtx.OrganizationID + var outletID *uuid.UUID + if appCtx.OutletID != uuid.Nil { + outletID = &appCtx.OutletID + } + + filterEntity := &entities.ChartOfAccount{ + OrganizationID: organizationID, + OutletID: outletID, + ChartOfAccountTypeID: *req.ChartOfAccountTypeID, + ParentID: req.ParentID, + } + + entities, total, err := p.chartOfAccountRepo.List(ctx, filterEntity) + if err != nil { + return nil, 0, fmt.Errorf("failed to list chart of accounts: %w", err) + } + + responses := make([]models.ChartOfAccountResponse, len(entities)) + for i, entity := range entities { + responses[i] = *mappers.ChartOfAccountEntityToResponse(entity) + } + + return responses, total, nil +} + +func (p *ChartOfAccountProcessorImpl) GetChartOfAccountsByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]models.ChartOfAccountResponse, error) { + entities, err := p.chartOfAccountRepo.GetByOrganization(ctx, organizationID, outletID) + if err != nil { + return nil, fmt.Errorf("failed to get chart of accounts by organization: %w", err) + } + + responses := make([]models.ChartOfAccountResponse, len(entities)) + for i, entity := range entities { + responses[i] = *mappers.ChartOfAccountEntityToResponse(entity) + } + + return responses, nil +} + +func (p *ChartOfAccountProcessorImpl) GetChartOfAccountsByType(ctx context.Context, organizationID uuid.UUID, chartOfAccountTypeID uuid.UUID, outletID *uuid.UUID) ([]models.ChartOfAccountResponse, error) { + entities, err := p.chartOfAccountRepo.GetByType(ctx, organizationID, chartOfAccountTypeID, outletID) + if err != nil { + return nil, fmt.Errorf("failed to get chart of accounts by type: %w", err) + } + + responses := make([]models.ChartOfAccountResponse, len(entities)) + for i, entity := range entities { + responses[i] = *mappers.ChartOfAccountEntityToResponse(entity) + } + + return responses, nil +} + +func (p *ChartOfAccountProcessorImpl) CreateDefaultChartOfAccounts(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) error { + // This method will be implemented to create default chart of accounts + // based on the JSON template when a new organization/outlet is created + // For now, we'll return nil as this is a placeholder + return nil +} diff --git a/internal/processor/chart_of_account_type_processor.go b/internal/processor/chart_of_account_type_processor.go new file mode 100644 index 0000000..486be1d --- /dev/null +++ b/internal/processor/chart_of_account_type_processor.go @@ -0,0 +1,106 @@ +package processor + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + + "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) + UpdateChartOfAccountType(ctx context.Context, id uuid.UUID, req *models.UpdateChartOfAccountTypeRequest) (*models.ChartOfAccountTypeResponse, error) + DeleteChartOfAccountType(ctx context.Context, id uuid.UUID) error + ListChartOfAccountTypes(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.ChartOfAccountTypeResponse, int, error) +} + +type ChartOfAccountTypeProcessorImpl struct { + chartOfAccountTypeRepo ChartOfAccountTypeRepository +} + +func NewChartOfAccountTypeProcessorImpl(chartOfAccountTypeRepo ChartOfAccountTypeRepository) *ChartOfAccountTypeProcessorImpl { + return &ChartOfAccountTypeProcessorImpl{ + chartOfAccountTypeRepo: chartOfAccountTypeRepo, + } +} + +func (p *ChartOfAccountTypeProcessorImpl) CreateChartOfAccountType(ctx context.Context, req *models.CreateChartOfAccountTypeRequest) (*models.ChartOfAccountTypeResponse, error) { + // Check if code already exists + existing, err := p.chartOfAccountTypeRepo.GetByCode(ctx, req.Code) + if err == nil && existing != nil { + return nil, fmt.Errorf("chart of account type with code %s already exists", req.Code) + } + + entity := mappers.ChartOfAccountTypeCreateRequestToEntity(req) + err = p.chartOfAccountTypeRepo.Create(ctx, entity) + if err != nil { + return nil, fmt.Errorf("failed to create chart of account type: %w", err) + } + + return mappers.ChartOfAccountTypeEntityToResponse(entity), nil +} + +func (p *ChartOfAccountTypeProcessorImpl) GetChartOfAccountTypeByID(ctx context.Context, id uuid.UUID) (*models.ChartOfAccountTypeResponse, error) { + entity, err := p.chartOfAccountTypeRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("chart of account type not found: %w", err) + } + + return mappers.ChartOfAccountTypeEntityToResponse(entity), nil +} + +func (p *ChartOfAccountTypeProcessorImpl) UpdateChartOfAccountType(ctx context.Context, id uuid.UUID, req *models.UpdateChartOfAccountTypeRequest) (*models.ChartOfAccountTypeResponse, error) { + entity, err := p.chartOfAccountTypeRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("chart of account type not found: %w", err) + } + + // Check if new code already exists (if code is being updated) + if req.Code != nil && *req.Code != entity.Code { + existing, err := p.chartOfAccountTypeRepo.GetByCode(ctx, *req.Code) + if err == nil && existing != nil { + return nil, fmt.Errorf("chart of account type with code %s already exists", *req.Code) + } + } + + mappers.ChartOfAccountTypeUpdateRequestToEntity(entity, req) + err = p.chartOfAccountTypeRepo.Update(ctx, entity) + if err != nil { + return nil, fmt.Errorf("failed to update chart of account type: %w", err) + } + + return mappers.ChartOfAccountTypeEntityToResponse(entity), nil +} + +func (p *ChartOfAccountTypeProcessorImpl) DeleteChartOfAccountType(ctx context.Context, id uuid.UUID) error { + _, err := p.chartOfAccountTypeRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("chart of account type not found: %w", err) + } + + err = p.chartOfAccountTypeRepo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete chart of account type: %w", err) + } + + return nil +} + +func (p *ChartOfAccountTypeProcessorImpl) ListChartOfAccountTypes(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.ChartOfAccountTypeResponse, int, error) { + entities, total, err := p.chartOfAccountTypeRepo.List(ctx, filters, page, limit) + if err != nil { + return nil, 0, fmt.Errorf("failed to list chart of account types: %w", err) + } + + responses := make([]models.ChartOfAccountTypeResponse, len(entities)) + for i, entity := range entities { + responses[i] = *mappers.ChartOfAccountTypeEntityToResponse(entity) + } + + return responses, total, nil +} diff --git a/internal/processor/ingredient_unit_converter_processor.go b/internal/processor/ingredient_unit_converter_processor.go new file mode 100644 index 0000000..f344f24 --- /dev/null +++ b/internal/processor/ingredient_unit_converter_processor.go @@ -0,0 +1,259 @@ +package processor + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + +type IngredientUnitConverterRepository interface { + Create(ctx context.Context, converter *entities.IngredientUnitConverter) error + GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.IngredientUnitConverter, error) + GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.IngredientUnitConverter, error) + Update(ctx context.Context, converter *entities.IngredientUnitConverter) 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.IngredientUnitConverter, int, error) + GetByIngredientAndUnits(ctx context.Context, ingredientID, fromUnitID, toUnitID, organizationID uuid.UUID) (*entities.IngredientUnitConverter, error) + GetConvertersForIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.IngredientUnitConverter, error) + GetActiveConverters(ctx context.Context, organizationID uuid.UUID) ([]*entities.IngredientUnitConverter, error) + ConvertQuantity(ctx context.Context, ingredientID, fromUnitID, toUnitID, organizationID uuid.UUID, quantity float64) (float64, error) +} + +type IngredientUnitConverterProcessor interface { + CreateIngredientUnitConverter(ctx context.Context, organizationID, userID uuid.UUID, req *models.CreateIngredientUnitConverterRequest) (*models.IngredientUnitConverterResponse, error) + UpdateIngredientUnitConverter(ctx context.Context, id, organizationID, userID uuid.UUID, req *models.UpdateIngredientUnitConverterRequest) (*models.IngredientUnitConverterResponse, error) + DeleteIngredientUnitConverter(ctx context.Context, id, organizationID uuid.UUID) error + GetIngredientUnitConverterByID(ctx context.Context, id, organizationID uuid.UUID) (*models.IngredientUnitConverterResponse, error) + ListIngredientUnitConverters(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.IngredientUnitConverterResponse, int, error) + GetConvertersForIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*models.IngredientUnitConverterResponse, error) + ConvertUnit(ctx context.Context, organizationID uuid.UUID, req *models.ConvertUnitRequest) (*models.ConvertUnitResponse, error) +} + +type IngredientUnitConverterProcessorImpl struct { + converterRepo IngredientUnitConverterRepository + ingredientRepo IngredientRepository + unitRepo UnitRepository +} + +func NewIngredientUnitConverterProcessorImpl( + converterRepo IngredientUnitConverterRepository, + ingredientRepo IngredientRepository, + unitRepo UnitRepository, +) *IngredientUnitConverterProcessorImpl { + return &IngredientUnitConverterProcessorImpl{ + converterRepo: converterRepo, + ingredientRepo: ingredientRepo, + unitRepo: unitRepo, + } +} + +func (p *IngredientUnitConverterProcessorImpl) CreateIngredientUnitConverter(ctx context.Context, organizationID, userID uuid.UUID, req *models.CreateIngredientUnitConverterRequest) (*models.IngredientUnitConverterResponse, error) { + // Validate ingredient exists + _, err := p.ingredientRepo.GetByID(ctx, req.IngredientID, organizationID) + if err != nil { + return nil, fmt.Errorf("ingredient not found: %w", err) + } + + // Validate units exist + _, err = p.unitRepo.GetByID(ctx, req.FromUnitID, organizationID) + if err != nil { + return nil, fmt.Errorf("from unit not found: %w", err) + } + + _, err = p.unitRepo.GetByID(ctx, req.ToUnitID, organizationID) + if err != nil { + return nil, fmt.Errorf("to unit not found: %w", err) + } + + // Check if converter already exists + existingConverter, err := p.converterRepo.GetByIngredientAndUnits(ctx, req.IngredientID, req.FromUnitID, req.ToUnitID, organizationID) + if err == nil && existingConverter != nil { + return nil, fmt.Errorf("converter already exists for this ingredient and unit combination") + } + + // Set default values + isActive := true + if req.IsActive != nil { + isActive = *req.IsActive + } + + // Create entity + converter := &entities.IngredientUnitConverter{ + OrganizationID: organizationID, + IngredientID: req.IngredientID, + FromUnitID: req.FromUnitID, + ToUnitID: req.ToUnitID, + ConversionFactor: req.ConversionFactor, + IsActive: isActive, + CreatedBy: userID, + UpdatedBy: userID, + } + + err = p.converterRepo.Create(ctx, converter) + if err != nil { + return nil, fmt.Errorf("failed to create ingredient unit converter: %w", err) + } + + // Get the created converter with relationships + createdConverter, err := p.converterRepo.GetByID(ctx, converter.ID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get created converter: %w", err) + } + + return mappers.IngredientUnitConverterEntityToResponse(createdConverter), nil +} + +func (p *IngredientUnitConverterProcessorImpl) UpdateIngredientUnitConverter(ctx context.Context, id, organizationID, userID uuid.UUID, req *models.UpdateIngredientUnitConverterRequest) (*models.IngredientUnitConverterResponse, error) { + // Get existing converter + converter, err := p.converterRepo.GetByID(ctx, id, organizationID) + if err != nil { + return nil, fmt.Errorf("ingredient unit converter not found: %w", err) + } + + // Update fields if provided + if req.FromUnitID != nil { + // Validate new unit exists + _, err = p.unitRepo.GetByID(ctx, *req.FromUnitID, organizationID) + if err != nil { + return nil, fmt.Errorf("from unit not found: %w", err) + } + converter.FromUnitID = *req.FromUnitID + } + + if req.ToUnitID != nil { + // Validate new unit exists + _, err = p.unitRepo.GetByID(ctx, *req.ToUnitID, organizationID) + if err != nil { + return nil, fmt.Errorf("to unit not found: %w", err) + } + converter.ToUnitID = *req.ToUnitID + } + + if req.ConversionFactor != nil { + converter.ConversionFactor = *req.ConversionFactor + } + + if req.IsActive != nil { + converter.IsActive = *req.IsActive + } + + converter.UpdatedBy = userID + + err = p.converterRepo.Update(ctx, converter) + if err != nil { + return nil, fmt.Errorf("failed to update ingredient unit converter: %w", err) + } + + // Get the updated converter with relationships + updatedConverter, err := p.converterRepo.GetByID(ctx, converter.ID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get updated converter: %w", err) + } + + return mappers.IngredientUnitConverterEntityToResponse(updatedConverter), nil +} + +func (p *IngredientUnitConverterProcessorImpl) DeleteIngredientUnitConverter(ctx context.Context, id, organizationID uuid.UUID) error { + // Check if converter exists + _, err := p.converterRepo.GetByID(ctx, id, organizationID) + if err != nil { + return fmt.Errorf("ingredient unit converter not found: %w", err) + } + + err = p.converterRepo.Delete(ctx, id, organizationID) + if err != nil { + return fmt.Errorf("failed to delete ingredient unit converter: %w", err) + } + + return nil +} + +func (p *IngredientUnitConverterProcessorImpl) GetIngredientUnitConverterByID(ctx context.Context, id, organizationID uuid.UUID) (*models.IngredientUnitConverterResponse, error) { + converter, err := p.converterRepo.GetByID(ctx, id, organizationID) + if err != nil { + return nil, fmt.Errorf("ingredient unit converter not found: %w", err) + } + + return mappers.IngredientUnitConverterEntityToResponse(converter), nil +} + +func (p *IngredientUnitConverterProcessorImpl) ListIngredientUnitConverters(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.IngredientUnitConverterResponse, int, error) { + converters, total, err := p.converterRepo.List(ctx, organizationID, filters, page, limit) + if err != nil { + return nil, 0, fmt.Errorf("failed to list ingredient unit converters: %w", err) + } + + responses := make([]*models.IngredientUnitConverterResponse, len(converters)) + for i, converter := range converters { + responses[i] = mappers.IngredientUnitConverterEntityToResponse(converter) + } + + return responses, total, nil +} + +func (p *IngredientUnitConverterProcessorImpl) GetConvertersForIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*models.IngredientUnitConverterResponse, error) { + converters, err := p.converterRepo.GetConvertersForIngredient(ctx, ingredientID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get converters for ingredient: %w", err) + } + + responses := make([]*models.IngredientUnitConverterResponse, len(converters)) + for i, converter := range converters { + responses[i] = mappers.IngredientUnitConverterEntityToResponse(converter) + } + + return responses, nil +} + +func (p *IngredientUnitConverterProcessorImpl) ConvertUnit(ctx context.Context, organizationID uuid.UUID, req *models.ConvertUnitRequest) (*models.ConvertUnitResponse, error) { + // Get ingredient and units for response + ingredient, err := p.ingredientRepo.GetByID(ctx, req.IngredientID, organizationID) + if err != nil { + return nil, fmt.Errorf("ingredient not found: %w", err) + } + + fromUnit, err := p.unitRepo.GetByID(ctx, req.FromUnitID, organizationID) + if err != nil { + return nil, fmt.Errorf("from unit not found: %w", err) + } + + toUnit, err := p.unitRepo.GetByID(ctx, req.ToUnitID, organizationID) + if err != nil { + return nil, fmt.Errorf("to unit not found: %w", err) + } + + // Convert quantity + convertedQuantity, err := p.converterRepo.ConvertQuantity(ctx, req.IngredientID, req.FromUnitID, req.ToUnitID, organizationID, req.Quantity) + if err != nil { + return nil, fmt.Errorf("failed to convert quantity: %w", err) + } + + // Get conversion factor for response + converter, err := p.converterRepo.GetByIngredientAndUnits(ctx, req.IngredientID, req.FromUnitID, req.ToUnitID, organizationID) + var conversionFactor float64 + if err == nil { + conversionFactor = converter.ConversionFactor + } else { + // Try reverse converter + reverseConverter, err := p.converterRepo.GetByIngredientAndUnits(ctx, req.IngredientID, req.ToUnitID, req.FromUnitID, organizationID) + if err == nil { + conversionFactor = 1.0 / reverseConverter.ConversionFactor + } + } + + response := &models.ConvertUnitResponse{ + FromQuantity: req.Quantity, + FromUnit: mappers.MapUnitEntityToResponse(fromUnit), + ToQuantity: convertedQuantity, + ToUnit: mappers.MapUnitEntityToResponse(toUnit), + ConversionFactor: conversionFactor, + Ingredient: mappers.MapIngredientEntityToResponse(ingredient), + } + + return response, nil +} diff --git a/internal/processor/purchase_order_processor.go b/internal/processor/purchase_order_processor.go new file mode 100644 index 0000000..005cfe6 --- /dev/null +++ b/internal/processor/purchase_order_processor.go @@ -0,0 +1,441 @@ +package processor + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + +type PurchaseOrderProcessor interface { + CreatePurchaseOrder(ctx context.Context, organizationID uuid.UUID, req *models.CreatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) + UpdatePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) + DeletePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID) error + GetPurchaseOrderByID(ctx context.Context, id, organizationID uuid.UUID) (*models.PurchaseOrderResponse, error) + ListPurchaseOrders(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.PurchaseOrderResponse, int, error) + GetPurchaseOrdersByStatus(ctx context.Context, organizationID uuid.UUID, status string) ([]*models.PurchaseOrderResponse, error) + GetOverduePurchaseOrders(ctx context.Context, organizationID uuid.UUID) ([]*models.PurchaseOrderResponse, error) + UpdatePurchaseOrderStatus(ctx context.Context, id, organizationID, userID, outletID uuid.UUID, status string) (*models.PurchaseOrderResponse, error) +} + +type PurchaseOrderProcessorImpl struct { + purchaseOrderRepo PurchaseOrderRepository + vendorRepo VendorRepository + ingredientRepo IngredientRepository + unitRepo UnitRepository + fileRepo FileRepository + inventoryMovementService InventoryMovementService + unitConverterRepo IngredientUnitConverterRepository +} + +func NewPurchaseOrderProcessorImpl( + purchaseOrderRepo PurchaseOrderRepository, + vendorRepo VendorRepository, + ingredientRepo IngredientRepository, + unitRepo UnitRepository, + fileRepo FileRepository, + inventoryMovementService InventoryMovementService, + unitConverterRepo IngredientUnitConverterRepository, +) *PurchaseOrderProcessorImpl { + return &PurchaseOrderProcessorImpl{ + purchaseOrderRepo: purchaseOrderRepo, + vendorRepo: vendorRepo, + ingredientRepo: ingredientRepo, + unitRepo: unitRepo, + fileRepo: fileRepo, + inventoryMovementService: inventoryMovementService, + unitConverterRepo: unitConverterRepo, + } +} + +func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, organizationID uuid.UUID, req *models.CreatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) { + // Check if vendor exists and belongs to organization + _, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, req.VendorID, organizationID) + if err != nil { + return nil, fmt.Errorf("vendor not found: %w", err) + } + + // Check if PO number already exists in organization + existingPO, err := p.purchaseOrderRepo.GetByPONumber(ctx, req.PONumber, organizationID) + if err == nil && existingPO != nil { + return nil, fmt.Errorf("purchase order with PO number %s already exists in this organization", req.PONumber) + } + + // Validate ingredients and units exist + for i, item := range req.Items { + _, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID) + if err != nil { + return nil, fmt.Errorf("ingredient not found for item %d: %w", i, err) + } + + _, err = p.unitRepo.GetByID(ctx, item.UnitID, organizationID) + if err != nil { + return nil, fmt.Errorf("unit not found for item %d: %w", i, err) + } + } + + // Calculate total amount + totalAmount := 0.0 + for _, item := range req.Items { + totalAmount += item.Amount + } + + // Create purchase order entity + poEntity := &entities.PurchaseOrder{ + OrganizationID: organizationID, + VendorID: req.VendorID, + PONumber: req.PONumber, + TransactionDate: req.TransactionDate, + DueDate: req.DueDate, + Reference: req.Reference, + Status: "draft", // Default status + Message: req.Message, + TotalAmount: totalAmount, + } + + if req.Status != nil { + poEntity.Status = *req.Status + } + + // Create purchase order + err = p.purchaseOrderRepo.Create(ctx, poEntity) + if err != nil { + return nil, fmt.Errorf("failed to create purchase order: %w", err) + } + + // Create purchase order items + for _, itemReq := range req.Items { + itemEntity := &entities.PurchaseOrderItem{ + PurchaseOrderID: poEntity.ID, + IngredientID: itemReq.IngredientID, + Description: itemReq.Description, + Quantity: itemReq.Quantity, + UnitID: itemReq.UnitID, + Amount: itemReq.Amount, + } + + err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity) + if err != nil { + return nil, fmt.Errorf("failed to create purchase order item: %w", err) + } + } + + // Create attachments if provided + for _, fileID := range req.AttachmentFileIDs { + attachmentEntity := &entities.PurchaseOrderAttachment{ + PurchaseOrderID: poEntity.ID, + FileID: fileID, + } + + err = p.purchaseOrderRepo.CreateAttachment(ctx, attachmentEntity) + if err != nil { + return nil, fmt.Errorf("failed to create purchase order attachment: %w", err) + } + } + + // Get the created purchase order with all relations + createdPO, err := p.purchaseOrderRepo.GetByID(ctx, poEntity.ID) + if err != nil { + return nil, fmt.Errorf("failed to get created purchase order: %w", err) + } + + return mappers.PurchaseOrderEntityToResponse(createdPO), nil +} + +func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) { + // Get existing purchase order + poEntity, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID) + if err != nil { + return nil, fmt.Errorf("purchase order not found: %w", err) + } + + // Check if vendor exists and belongs to organization (if vendor is being updated) + if req.VendorID != nil { + _, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, *req.VendorID, organizationID) + if err != nil { + return nil, fmt.Errorf("vendor not found: %w", err) + } + poEntity.VendorID = *req.VendorID + } + + // Check if PO number already exists (if PO number is being updated) + if req.PONumber != nil && *req.PONumber != poEntity.PONumber { + existingPO, err := p.purchaseOrderRepo.GetByPONumber(ctx, *req.PONumber, organizationID) + if err == nil && existingPO != nil { + return nil, fmt.Errorf("purchase order with PO number %s already exists in this organization", *req.PONumber) + } + poEntity.PONumber = *req.PONumber + } + + // Update other fields + if req.TransactionDate != nil { + poEntity.TransactionDate = *req.TransactionDate + } + if req.DueDate != nil { + poEntity.DueDate = *req.DueDate + } + if req.Reference != nil { + poEntity.Reference = req.Reference + } + if req.Status != nil { + poEntity.Status = *req.Status + } + if req.Message != nil { + poEntity.Message = req.Message + } + + // Update items if provided + if req.Items != nil { + // Delete existing items + err = p.purchaseOrderRepo.DeleteItemsByPurchaseOrderID(ctx, poEntity.ID) + if err != nil { + return nil, fmt.Errorf("failed to delete existing items: %w", err) + } + + // Create new items + totalAmount := 0.0 + for _, itemReq := range req.Items { + // Validate ingredients and units exist + if itemReq.IngredientID != nil { + _, err := p.ingredientRepo.GetByID(ctx, *itemReq.IngredientID, organizationID) + if err != nil { + return nil, fmt.Errorf("ingredient not found: %w", err) + } + } + + if itemReq.UnitID != nil { + _, err := p.unitRepo.GetByID(ctx, *itemReq.UnitID, organizationID) + if err != nil { + return nil, fmt.Errorf("unit not found: %w", err) + } + } + + // Use existing values if not provided + ingredientID := poEntity.Items[0].IngredientID // This is a simplified approach + unitID := poEntity.Items[0].UnitID + quantity := poEntity.Items[0].Quantity + amount := poEntity.Items[0].Amount + description := poEntity.Items[0].Description + + if itemReq.IngredientID != nil { + ingredientID = *itemReq.IngredientID + } + if itemReq.UnitID != nil { + unitID = *itemReq.UnitID + } + if itemReq.Quantity != nil { + quantity = *itemReq.Quantity + } + if itemReq.Amount != nil { + amount = *itemReq.Amount + } + if itemReq.Description != nil { + description = itemReq.Description + } + + itemEntity := &entities.PurchaseOrderItem{ + PurchaseOrderID: poEntity.ID, + IngredientID: ingredientID, + Description: description, + Quantity: quantity, + UnitID: unitID, + Amount: amount, + } + + err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity) + if err != nil { + return nil, fmt.Errorf("failed to create purchase order item: %w", err) + } + + totalAmount += amount + } + + poEntity.TotalAmount = totalAmount + } + + // Update attachments if provided + if req.AttachmentFileIDs != nil { + // Delete existing attachments + err = p.purchaseOrderRepo.DeleteAttachmentsByPurchaseOrderID(ctx, poEntity.ID) + if err != nil { + return nil, fmt.Errorf("failed to delete existing attachments: %w", err) + } + + // Create new attachments + for _, fileID := range req.AttachmentFileIDs { + attachmentEntity := &entities.PurchaseOrderAttachment{ + PurchaseOrderID: poEntity.ID, + FileID: fileID, + } + + err = p.purchaseOrderRepo.CreateAttachment(ctx, attachmentEntity) + if err != nil { + return nil, fmt.Errorf("failed to create purchase order attachment: %w", err) + } + } + } + + // Update purchase order + err = p.purchaseOrderRepo.Update(ctx, poEntity) + if err != nil { + return nil, fmt.Errorf("failed to update purchase order: %w", err) + } + + // Get the updated purchase order with all relations + updatedPO, err := p.purchaseOrderRepo.GetByID(ctx, poEntity.ID) + if err != nil { + return nil, fmt.Errorf("failed to get updated purchase order: %w", err) + } + + return mappers.PurchaseOrderEntityToResponse(updatedPO), nil +} + +func (p *PurchaseOrderProcessorImpl) DeletePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID) error { + _, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID) + if err != nil { + return fmt.Errorf("purchase order not found: %w", err) + } + + err = p.purchaseOrderRepo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete purchase order: %w", err) + } + + return nil +} + +func (p *PurchaseOrderProcessorImpl) GetPurchaseOrderByID(ctx context.Context, id, organizationID uuid.UUID) (*models.PurchaseOrderResponse, error) { + poEntity, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID) + if err != nil { + return nil, fmt.Errorf("purchase order not found: %w", err) + } + + return mappers.PurchaseOrderEntityToResponse(poEntity), nil +} + +func (p *PurchaseOrderProcessorImpl) ListPurchaseOrders(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.PurchaseOrderResponse, int, error) { + offset := (page - 1) * limit + poEntities, total, err := p.purchaseOrderRepo.List(ctx, organizationID, filters, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to list purchase orders: %w", err) + } + + poResponses := make([]*models.PurchaseOrderResponse, len(poEntities)) + for i, poEntity := range poEntities { + poResponses[i] = mappers.PurchaseOrderEntityToResponse(poEntity) + } + + totalPages := int((total + int64(limit) - 1) / int64(limit)) + return poResponses, totalPages, nil +} + +func (p *PurchaseOrderProcessorImpl) GetPurchaseOrdersByStatus(ctx context.Context, organizationID uuid.UUID, status string) ([]*models.PurchaseOrderResponse, error) { + poEntities, err := p.purchaseOrderRepo.GetByStatus(ctx, organizationID, status) + if err != nil { + return nil, fmt.Errorf("failed to get purchase orders by status: %w", err) + } + + poResponses := make([]*models.PurchaseOrderResponse, len(poEntities)) + for i, poEntity := range poEntities { + poResponses[i] = mappers.PurchaseOrderEntityToResponse(poEntity) + } + + return poResponses, nil +} + +func (p *PurchaseOrderProcessorImpl) GetOverduePurchaseOrders(ctx context.Context, organizationID uuid.UUID) ([]*models.PurchaseOrderResponse, error) { + poEntities, err := p.purchaseOrderRepo.GetOverdue(ctx, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get overdue purchase orders: %w", err) + } + + poResponses := make([]*models.PurchaseOrderResponse, len(poEntities)) + for i, poEntity := range poEntities { + poResponses[i] = mappers.PurchaseOrderEntityToResponse(poEntity) + } + + return poResponses, nil +} + +func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Context, id, organizationID, userID, outletID uuid.UUID, status string) (*models.PurchaseOrderResponse, error) { + // Get the purchase order with items to check current status + po, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID) + if err != nil { + return nil, fmt.Errorf("purchase order not found: %w", err) + } + + // Check if status is changing to "received" and current status is not "received" + if status == "received" && po.Status != "received" { + // Get purchase order with items for inventory update + poWithItems, err := p.purchaseOrderRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get purchase order with items: %w", err) + } + + // Update inventory for each item + for _, item := range poWithItems.Items { + // Get ingredient to find its base unit + ingredient, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get ingredient %s: %w", item.IngredientID, err) + } + + // Convert quantity to ingredient's base unit if needed + quantityToAdd := item.Quantity + if item.UnitID != ingredient.UnitID { + // Convert from purchase unit to ingredient's base unit + convertedQuantity, err := p.unitConverterRepo.ConvertQuantity(ctx, item.IngredientID, item.UnitID, ingredient.UnitID, organizationID, item.Quantity) + if err != nil { + return nil, fmt.Errorf("failed to convert quantity for ingredient %s from unit %s to %s: %w", item.IngredientID, item.UnitID, ingredient.UnitID, err) + } + quantityToAdd = convertedQuantity + } + + // Calculate unit cost in ingredient's base unit + unitCost := 0.0 + if quantityToAdd > 0 { + unitCost = item.Amount / quantityToAdd + } + + // Create inventory movement for ingredient purchase + reason := fmt.Sprintf("Purchase order %s received", po.PONumber) + referenceType := entities.InventoryMovementReferenceTypePurchaseOrder + referenceID := &id + + err = p.inventoryMovementService.CreateIngredientMovement( + ctx, + item.IngredientID, + organizationID, + outletID, + userID, + entities.InventoryMovementTypePurchase, + quantityToAdd, + unitCost, + reason, + &referenceType, + referenceID, + ) + if err != nil { + return nil, fmt.Errorf("failed to create inventory movement for ingredient %s: %w", item.IngredientID, err) + } + } + } + + // Update the purchase order status + err = p.purchaseOrderRepo.UpdateStatus(ctx, id, status) + if err != nil { + return nil, fmt.Errorf("failed to update purchase order status: %w", err) + } + + // Get the updated purchase order + updatedPO, err := p.purchaseOrderRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get updated purchase order: %w", err) + } + + return mappers.PurchaseOrderEntityToResponse(updatedPO), nil +} diff --git a/internal/processor/purchase_order_repository.go b/internal/processor/purchase_order_repository.go new file mode 100644 index 0000000..98fa3d4 --- /dev/null +++ b/internal/processor/purchase_order_repository.go @@ -0,0 +1,32 @@ +package processor + +import ( + "apskel-pos-be/internal/entities" + "context" + + "github.com/google/uuid" +) + +type PurchaseOrderRepository interface { + Create(ctx context.Context, po *entities.PurchaseOrder) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.PurchaseOrder, error) + GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.PurchaseOrder, error) + Update(ctx context.Context, po *entities.PurchaseOrder) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.PurchaseOrder, int64, error) + Count(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}) (int64, error) + GetByPONumber(ctx context.Context, poNumber string, organizationID uuid.UUID) (*entities.PurchaseOrder, error) + GetByStatus(ctx context.Context, organizationID uuid.UUID, status string) ([]*entities.PurchaseOrder, error) + GetOverdue(ctx context.Context, organizationID uuid.UUID) ([]*entities.PurchaseOrder, error) + UpdateStatus(ctx context.Context, id uuid.UUID, status string) error + UpdateTotalAmount(ctx context.Context, id uuid.UUID, totalAmount float64) error + CreateItem(ctx context.Context, item *entities.PurchaseOrderItem) error + UpdateItem(ctx context.Context, item *entities.PurchaseOrderItem) error + DeleteItem(ctx context.Context, id uuid.UUID) error + DeleteItemsByPurchaseOrderID(ctx context.Context, purchaseOrderID uuid.UUID) error + GetItemsByPurchaseOrderID(ctx context.Context, purchaseOrderID uuid.UUID) ([]*entities.PurchaseOrderItem, error) + CreateAttachment(ctx context.Context, attachment *entities.PurchaseOrderAttachment) error + DeleteAttachment(ctx context.Context, id uuid.UUID) error + DeleteAttachmentsByPurchaseOrderID(ctx context.Context, purchaseOrderID uuid.UUID) error + GetAttachmentsByPurchaseOrderID(ctx context.Context, purchaseOrderID uuid.UUID) ([]*entities.PurchaseOrderAttachment, error) +} diff --git a/internal/processor/repository_interfaces.go b/internal/processor/repository_interfaces.go new file mode 100644 index 0000000..440db2b --- /dev/null +++ b/internal/processor/repository_interfaces.go @@ -0,0 +1,44 @@ +package processor + +import ( + "context" + + "apskel-pos-be/internal/entities" + + "github.com/google/uuid" +) + +// Repository interfaces for processors +type ChartOfAccountTypeRepository interface { + Create(ctx context.Context, chartOfAccountType *entities.ChartOfAccountType) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.ChartOfAccountType, error) + GetByCode(ctx context.Context, code string) (*entities.ChartOfAccountType, error) + Update(ctx context.Context, chartOfAccountType *entities.ChartOfAccountType) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, filters map[string]interface{}, page, limit int) ([]*entities.ChartOfAccountType, int, error) + GetActive(ctx context.Context) ([]*entities.ChartOfAccountType, error) +} + +type ChartOfAccountRepository interface { + Create(ctx context.Context, chartOfAccount *entities.ChartOfAccount) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.ChartOfAccount, error) + Update(ctx context.Context, chartOfAccount *entities.ChartOfAccount) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, req *entities.ChartOfAccount) ([]*entities.ChartOfAccount, int, error) + GetByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]*entities.ChartOfAccount, error) + GetByType(ctx context.Context, organizationID uuid.UUID, chartOfAccountTypeID uuid.UUID, outletID *uuid.UUID) ([]*entities.ChartOfAccount, error) + GetByCode(ctx context.Context, organizationID uuid.UUID, code string, outletID *uuid.UUID) (*entities.ChartOfAccount, error) +} + +type AccountRepository interface { + Create(ctx context.Context, account *entities.Account) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.Account, error) + Update(ctx context.Context, account *entities.Account) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, req *entities.Account) ([]*entities.Account, int, error) + GetByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]*entities.Account, error) + GetByChartOfAccount(ctx context.Context, chartOfAccountID uuid.UUID) ([]*entities.Account, error) + GetByNumber(ctx context.Context, organizationID uuid.UUID, number string, outletID *uuid.UUID) (*entities.Account, error) + UpdateBalance(ctx context.Context, id uuid.UUID, amount float64) error + GetBalance(ctx context.Context, id uuid.UUID) (float64, error) +} diff --git a/internal/processor/vendor_processor.go b/internal/processor/vendor_processor.go new file mode 100644 index 0000000..1244397 --- /dev/null +++ b/internal/processor/vendor_processor.go @@ -0,0 +1,177 @@ +package processor + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + +type VendorProcessor interface { + CreateVendor(ctx context.Context, organizationID uuid.UUID, req *models.CreateVendorRequest) (*models.VendorResponse, error) + UpdateVendor(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdateVendorRequest) (*models.VendorResponse, error) + DeleteVendor(ctx context.Context, id, organizationID uuid.UUID) error + GetVendorByID(ctx context.Context, id, organizationID uuid.UUID) (*models.VendorResponse, error) + ListVendors(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.VendorResponse, int, error) + GetActiveVendors(ctx context.Context, organizationID uuid.UUID) ([]*models.VendorResponse, error) +} + +type VendorProcessorImpl struct { + vendorRepo VendorRepository +} + +func NewVendorProcessorImpl(vendorRepo VendorRepository) *VendorProcessorImpl { + return &VendorProcessorImpl{ + vendorRepo: vendorRepo, + } +} + +func (p *VendorProcessorImpl) CreateVendor(ctx context.Context, organizationID uuid.UUID, req *models.CreateVendorRequest) (*models.VendorResponse, error) { + // Check if vendor with same name already exists in organization + if req.Name != "" { + existingVendor, err := p.vendorRepo.GetByName(ctx, req.Name, organizationID) + if err == nil && existingVendor != nil { + return nil, fmt.Errorf("vendor with name %s already exists in this organization", req.Name) + } + } + + // Check if vendor with same email already exists in organization + if req.Email != nil && *req.Email != "" { + existingVendor, err := p.vendorRepo.GetByEmail(ctx, *req.Email, organizationID) + if err == nil && existingVendor != nil { + return nil, fmt.Errorf("vendor with email %s already exists in this organization", *req.Email) + } + } + + vendorEntity := &entities.Vendor{ + OrganizationID: organizationID, + Name: req.Name, + Email: req.Email, + PhoneNumber: req.PhoneNumber, + Address: req.Address, + ContactPerson: req.ContactPerson, + TaxNumber: req.TaxNumber, + PaymentTerms: req.PaymentTerms, + Notes: req.Notes, + IsActive: true, // Default to active + } + + if req.IsActive != nil { + vendorEntity.IsActive = *req.IsActive + } + + err := p.vendorRepo.Create(ctx, vendorEntity) + if err != nil { + return nil, fmt.Errorf("failed to create vendor: %w", err) + } + + return mappers.VendorEntityToResponse(vendorEntity), nil +} + +func (p *VendorProcessorImpl) UpdateVendor(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdateVendorRequest) (*models.VendorResponse, error) { + vendorEntity, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, id, organizationID) + if err != nil { + return nil, fmt.Errorf("vendor not found: %w", err) + } + + // Check if vendor with same name already exists (excluding current vendor) + if req.Name != nil && *req.Name != "" && *req.Name != vendorEntity.Name { + existingVendor, err := p.vendorRepo.GetByName(ctx, *req.Name, organizationID) + if err == nil && existingVendor != nil && existingVendor.ID != id { + return nil, fmt.Errorf("vendor with name %s already exists in this organization", *req.Name) + } + } + + // Check if vendor with same email already exists (excluding current vendor) + if req.Email != nil && *req.Email != "" && (vendorEntity.Email == nil || *req.Email != *vendorEntity.Email) { + existingVendor, err := p.vendorRepo.GetByEmail(ctx, *req.Email, organizationID) + if err == nil && existingVendor != nil && existingVendor.ID != id { + return nil, fmt.Errorf("vendor with email %s already exists in this organization", *req.Email) + } + } + + // Update fields + if req.Name != nil { + vendorEntity.Name = *req.Name + } + if req.Email != nil { + vendorEntity.Email = req.Email + } + if req.PhoneNumber != nil { + vendorEntity.PhoneNumber = req.PhoneNumber + } + if req.Address != nil { + vendorEntity.Address = req.Address + } + if req.ContactPerson != nil { + vendorEntity.ContactPerson = req.ContactPerson + } + if req.TaxNumber != nil { + vendorEntity.TaxNumber = req.TaxNumber + } + if req.PaymentTerms != nil { + vendorEntity.PaymentTerms = req.PaymentTerms + } + if req.Notes != nil { + vendorEntity.Notes = req.Notes + } + if req.IsActive != nil { + vendorEntity.IsActive = *req.IsActive + } + + err = p.vendorRepo.Update(ctx, vendorEntity) + if err != nil { + return nil, fmt.Errorf("failed to update vendor: %w", err) + } + + return mappers.VendorEntityToResponse(vendorEntity), nil +} + +func (p *VendorProcessorImpl) DeleteVendor(ctx context.Context, id, organizationID uuid.UUID) error { + _, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, id, organizationID) + if err != nil { + return fmt.Errorf("vendor not found: %w", err) + } + + err = p.vendorRepo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete vendor: %w", err) + } + + return nil +} + +func (p *VendorProcessorImpl) GetVendorByID(ctx context.Context, id, organizationID uuid.UUID) (*models.VendorResponse, error) { + vendorEntity, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, id, organizationID) + if err != nil { + return nil, fmt.Errorf("vendor not found: %w", err) + } + + return mappers.VendorEntityToResponse(vendorEntity), nil +} + +func (p *VendorProcessorImpl) ListVendors(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.VendorResponse, int, error) { + offset := (page - 1) * limit + vendorEntities, total, err := p.vendorRepo.List(ctx, organizationID, filters, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to list vendors: %w", err) + } + + vendorResponses := mappers.VendorEntitiesToResponses(vendorEntities) + totalPages := int((total + int64(limit) - 1) / int64(limit)) + + return vendorResponses, totalPages, nil +} + +func (p *VendorProcessorImpl) GetActiveVendors(ctx context.Context, organizationID uuid.UUID) ([]*models.VendorResponse, error) { + vendorEntities, err := p.vendorRepo.GetActiveVendors(ctx, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get active vendors: %w", err) + } + + return mappers.VendorEntitiesToResponses(vendorEntities), nil +} diff --git a/internal/processor/vendor_repository.go b/internal/processor/vendor_repository.go new file mode 100644 index 0000000..3e39c1f --- /dev/null +++ b/internal/processor/vendor_repository.go @@ -0,0 +1,21 @@ +package processor + +import ( + "apskel-pos-be/internal/entities" + "context" + + "github.com/google/uuid" +) + +type VendorRepository interface { + Create(ctx context.Context, vendor *entities.Vendor) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.Vendor, error) + GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Vendor, error) + Update(ctx context.Context, vendor *entities.Vendor) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.Vendor, int64, error) + Count(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}) (int64, error) + GetByEmail(ctx context.Context, email string, organizationID uuid.UUID) (*entities.Vendor, error) + GetByName(ctx context.Context, name string, organizationID uuid.UUID) (*entities.Vendor, error) + GetActiveVendors(ctx context.Context, organizationID uuid.UUID) ([]*entities.Vendor, error) +} diff --git a/internal/repository/account_repository.go b/internal/repository/account_repository.go new file mode 100644 index 0000000..8c78985 --- /dev/null +++ b/internal/repository/account_repository.go @@ -0,0 +1,147 @@ +package repository + +import ( + "context" + + "github.com/google/uuid" + + "apskel-pos-be/internal/entities" + + "gorm.io/gorm" +) + +type AccountRepository interface { + Create(ctx context.Context, account *entities.Account) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.Account, error) + Update(ctx context.Context, account *entities.Account) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, req *entities.Account) ([]*entities.Account, int, error) + GetByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]*entities.Account, error) + GetByChartOfAccount(ctx context.Context, chartOfAccountID uuid.UUID) ([]*entities.Account, error) + GetByNumber(ctx context.Context, organizationID uuid.UUID, number string, outletID *uuid.UUID) (*entities.Account, error) + UpdateBalance(ctx context.Context, id uuid.UUID, amount float64) error + GetBalance(ctx context.Context, id uuid.UUID) (float64, error) + GetSystemAccounts(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]*entities.Account, error) +} + +type AccountRepositoryImpl struct { + db *gorm.DB +} + +func NewAccountRepositoryImpl(db *gorm.DB) *AccountRepositoryImpl { + return &AccountRepositoryImpl{ + db: db, + } +} + +func (r *AccountRepositoryImpl) Create(ctx context.Context, account *entities.Account) error { + return r.db.WithContext(ctx).Create(account).Error +} + +func (r *AccountRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Account, error) { + var account entities.Account + err := r.db.WithContext(ctx).Preload("ChartOfAccount").Preload("ChartOfAccount.ChartOfAccountType").First(&account, "id = ?", id).Error + if err != nil { + return nil, err + } + return &account, nil +} + +func (r *AccountRepositoryImpl) Update(ctx context.Context, account *entities.Account) error { + return r.db.WithContext(ctx).Save(account).Error +} + +func (r *AccountRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Account{}, "id = ?", id).Error +} + +func (r *AccountRepositoryImpl) List(ctx context.Context, req *entities.Account) ([]*entities.Account, int, error) { + var accounts []*entities.Account + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.Account{}) + + // Apply filters + if req.OrganizationID != uuid.Nil { + query = query.Where("organization_id = ?", req.OrganizationID) + } + if req.OutletID != nil { + query = query.Where("outlet_id = ?", *req.OutletID) + } + if req.ChartOfAccountID != uuid.Nil { + query = query.Where("chart_of_account_id = ?", req.ChartOfAccountID) + } + if req.AccountType != "" { + query = query.Where("account_type = ?", req.AccountType) + } + + // Count total + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + // Apply pagination and preloads + err := query.Preload("ChartOfAccount").Preload("ChartOfAccount.ChartOfAccountType").Find(&accounts).Error + return accounts, int(total), err +} + +func (r *AccountRepositoryImpl) GetByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]*entities.Account, error) { + var accounts []*entities.Account + query := r.db.WithContext(ctx).Where("organization_id = ?", organizationID) + + if outletID != nil { + query = query.Where("outlet_id = ?", *outletID) + } else { + query = query.Where("outlet_id IS NULL") + } + + err := query.Preload("ChartOfAccount").Preload("ChartOfAccount.ChartOfAccountType").Find(&accounts).Error + return accounts, err +} + +func (r *AccountRepositoryImpl) GetByChartOfAccount(ctx context.Context, chartOfAccountID uuid.UUID) ([]*entities.Account, error) { + var accounts []*entities.Account + err := r.db.WithContext(ctx).Where("chart_of_account_id = ?", chartOfAccountID).Preload("ChartOfAccount").Preload("ChartOfAccount.ChartOfAccountType").Find(&accounts).Error + return accounts, err +} + +func (r *AccountRepositoryImpl) GetByNumber(ctx context.Context, organizationID uuid.UUID, number string, outletID *uuid.UUID) (*entities.Account, error) { + var account entities.Account + query := r.db.WithContext(ctx).Where("organization_id = ? AND number = ?", organizationID, number) + + if outletID != nil { + query = query.Where("outlet_id = ?", *outletID) + } else { + query = query.Where("outlet_id IS NULL") + } + + err := query.Preload("ChartOfAccount").Preload("ChartOfAccount.ChartOfAccountType").First(&account).Error + if err != nil { + return nil, err + } + return &account, nil +} + +func (r *AccountRepositoryImpl) UpdateBalance(ctx context.Context, id uuid.UUID, amount float64) error { + return r.db.WithContext(ctx).Model(&entities.Account{}).Where("id = ?", id).Update("current_balance", amount).Error +} + +func (r *AccountRepositoryImpl) GetBalance(ctx context.Context, id uuid.UUID) (float64, error) { + var balance float64 + err := r.db.WithContext(ctx).Model(&entities.Account{}).Select("current_balance").Where("id = ?", id).Scan(&balance).Error + return balance, err +} + +func (r *AccountRepositoryImpl) GetSystemAccounts(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]*entities.Account, error) { + var accounts []*entities.Account + query := r.db.WithContext(ctx).Where("organization_id = ? AND is_system = ?", organizationID, true) + + if outletID != nil { + query = query.Where("outlet_id = ?", *outletID) + } else { + query = query.Where("outlet_id IS NULL") + } + + err := query.Preload("ChartOfAccount").Preload("ChartOfAccount.ChartOfAccountType").Find(&accounts).Error + return accounts, err +} diff --git a/internal/repository/chart_of_account_repository.go b/internal/repository/chart_of_account_repository.go new file mode 100644 index 0000000..7b21b15 --- /dev/null +++ b/internal/repository/chart_of_account_repository.go @@ -0,0 +1,143 @@ +package repository + +import ( + "context" + + "github.com/google/uuid" + + "apskel-pos-be/internal/entities" + + "gorm.io/gorm" +) + +type ChartOfAccountRepository interface { + Create(ctx context.Context, chartOfAccount *entities.ChartOfAccount) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.ChartOfAccount, error) + Update(ctx context.Context, chartOfAccount *entities.ChartOfAccount) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, req *entities.ChartOfAccount) ([]*entities.ChartOfAccount, int, error) + GetByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]*entities.ChartOfAccount, error) + GetByType(ctx context.Context, organizationID uuid.UUID, chartOfAccountTypeID uuid.UUID, outletID *uuid.UUID) ([]*entities.ChartOfAccount, error) + GetByCode(ctx context.Context, organizationID uuid.UUID, code string, outletID *uuid.UUID) (*entities.ChartOfAccount, error) + GetSystemAccounts(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]*entities.ChartOfAccount, error) +} + +type ChartOfAccountRepositoryImpl struct { + db *gorm.DB +} + +func NewChartOfAccountRepositoryImpl(db *gorm.DB) *ChartOfAccountRepositoryImpl { + return &ChartOfAccountRepositoryImpl{ + db: db, + } +} + +func (r *ChartOfAccountRepositoryImpl) Create(ctx context.Context, chartOfAccount *entities.ChartOfAccount) error { + return r.db.WithContext(ctx).Create(chartOfAccount).Error +} + +func (r *ChartOfAccountRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.ChartOfAccount, error) { + var chartOfAccount entities.ChartOfAccount + err := r.db.WithContext(ctx).Preload("ChartOfAccountType").Preload("Parent").Preload("Children").First(&chartOfAccount, "id = ?", id).Error + if err != nil { + return nil, err + } + return &chartOfAccount, nil +} + +func (r *ChartOfAccountRepositoryImpl) Update(ctx context.Context, chartOfAccount *entities.ChartOfAccount) error { + return r.db.WithContext(ctx).Save(chartOfAccount).Error +} + +func (r *ChartOfAccountRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.ChartOfAccount{}, "id = ?", id).Error +} + +func (r *ChartOfAccountRepositoryImpl) List(ctx context.Context, req *entities.ChartOfAccount) ([]*entities.ChartOfAccount, int, error) { + var chartOfAccounts []*entities.ChartOfAccount + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.ChartOfAccount{}) + + // Apply filters + if req.OrganizationID != uuid.Nil { + query = query.Where("organization_id = ?", req.OrganizationID) + } + if req.OutletID != nil { + query = query.Where("outlet_id = ?", *req.OutletID) + } + if req.ChartOfAccountTypeID != uuid.Nil { + query = query.Where("chart_of_account_type_id = ?", req.ChartOfAccountTypeID) + } + if req.ParentID != nil { + query = query.Where("parent_id = ?", *req.ParentID) + } + + // Count total + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + // Apply pagination and preloads + err := query.Preload("ChartOfAccountType").Preload("Parent").Preload("Children").Find(&chartOfAccounts).Error + return chartOfAccounts, int(total), err +} + +func (r *ChartOfAccountRepositoryImpl) GetByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]*entities.ChartOfAccount, error) { + var chartOfAccounts []*entities.ChartOfAccount + query := r.db.WithContext(ctx).Where("organization_id = ?", organizationID) + + if outletID != nil { + query = query.Where("outlet_id = ?", *outletID) + } else { + query = query.Where("outlet_id IS NULL") + } + + err := query.Preload("ChartOfAccountType").Preload("Parent").Preload("Children").Find(&chartOfAccounts).Error + return chartOfAccounts, err +} + +func (r *ChartOfAccountRepositoryImpl) GetByType(ctx context.Context, organizationID uuid.UUID, chartOfAccountTypeID uuid.UUID, outletID *uuid.UUID) ([]*entities.ChartOfAccount, error) { + var chartOfAccounts []*entities.ChartOfAccount + query := r.db.WithContext(ctx).Where("organization_id = ? AND chart_of_account_type_id = ?", organizationID, chartOfAccountTypeID) + + if outletID != nil { + query = query.Where("outlet_id = ?", *outletID) + } else { + query = query.Where("outlet_id IS NULL") + } + + err := query.Preload("ChartOfAccountType").Preload("Parent").Preload("Children").Find(&chartOfAccounts).Error + return chartOfAccounts, err +} + +func (r *ChartOfAccountRepositoryImpl) GetByCode(ctx context.Context, organizationID uuid.UUID, code string, outletID *uuid.UUID) (*entities.ChartOfAccount, error) { + var chartOfAccount entities.ChartOfAccount + query := r.db.WithContext(ctx).Where("organization_id = ? AND code = ?", organizationID, code) + + if outletID != nil { + query = query.Where("outlet_id = ?", *outletID) + } else { + query = query.Where("outlet_id IS NULL") + } + + err := query.Preload("ChartOfAccountType").Preload("Parent").First(&chartOfAccount).Error + if err != nil { + return nil, err + } + return &chartOfAccount, nil +} + +func (r *ChartOfAccountRepositoryImpl) GetSystemAccounts(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]*entities.ChartOfAccount, error) { + var chartOfAccounts []*entities.ChartOfAccount + query := r.db.WithContext(ctx).Where("organization_id = ? AND is_system = ?", organizationID, true) + + if outletID != nil { + query = query.Where("outlet_id = ?", *outletID) + } else { + query = query.Where("outlet_id IS NULL") + } + + err := query.Preload("ChartOfAccountType").Preload("Parent").Find(&chartOfAccounts).Error + return chartOfAccounts, err +} diff --git a/internal/repository/chart_of_account_type_repository.go b/internal/repository/chart_of_account_type_repository.go new file mode 100644 index 0000000..9099ffc --- /dev/null +++ b/internal/repository/chart_of_account_type_repository.go @@ -0,0 +1,91 @@ +package repository + +import ( + "context" + + "github.com/google/uuid" + + "apskel-pos-be/internal/entities" + + "gorm.io/gorm" +) + +type ChartOfAccountTypeRepository interface { + Create(ctx context.Context, chartOfAccountType *entities.ChartOfAccountType) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.ChartOfAccountType, error) + GetByCode(ctx context.Context, code string) (*entities.ChartOfAccountType, error) + Update(ctx context.Context, chartOfAccountType *entities.ChartOfAccountType) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, filters map[string]interface{}, page, limit int) ([]*entities.ChartOfAccountType, int, error) + GetActive(ctx context.Context) ([]*entities.ChartOfAccountType, error) +} + +type ChartOfAccountTypeRepositoryImpl struct { + db *gorm.DB +} + +func NewChartOfAccountTypeRepositoryImpl(db *gorm.DB) *ChartOfAccountTypeRepositoryImpl { + return &ChartOfAccountTypeRepositoryImpl{ + db: db, + } +} + +func (r *ChartOfAccountTypeRepositoryImpl) Create(ctx context.Context, chartOfAccountType *entities.ChartOfAccountType) error { + return r.db.WithContext(ctx).Create(chartOfAccountType).Error +} + +func (r *ChartOfAccountTypeRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.ChartOfAccountType, error) { + var chartOfAccountType entities.ChartOfAccountType + err := r.db.WithContext(ctx).First(&chartOfAccountType, "id = ?", id).Error + if err != nil { + return nil, err + } + return &chartOfAccountType, nil +} + +func (r *ChartOfAccountTypeRepositoryImpl) GetByCode(ctx context.Context, code string) (*entities.ChartOfAccountType, error) { + var chartOfAccountType entities.ChartOfAccountType + err := r.db.WithContext(ctx).First(&chartOfAccountType, "code = ?", code).Error + if err != nil { + return nil, err + } + return &chartOfAccountType, nil +} + +func (r *ChartOfAccountTypeRepositoryImpl) Update(ctx context.Context, chartOfAccountType *entities.ChartOfAccountType) error { + return r.db.WithContext(ctx).Save(chartOfAccountType).Error +} + +func (r *ChartOfAccountTypeRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.ChartOfAccountType{}, "id = ?", id).Error +} + +func (r *ChartOfAccountTypeRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, page, limit int) ([]*entities.ChartOfAccountType, int, error) { + var chartOfAccountTypes []*entities.ChartOfAccountType + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.ChartOfAccountType{}) + + // Apply filters + for key, value := range filters { + if value != nil { + query = query.Where(key+" = ?", value) + } + } + + // Count total + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + // Apply pagination + offset := (page - 1) * limit + err := query.Offset(offset).Limit(limit).Find(&chartOfAccountTypes).Error + return chartOfAccountTypes, int(total), err +} + +func (r *ChartOfAccountTypeRepositoryImpl) GetActive(ctx context.Context) ([]*entities.ChartOfAccountType, error) { + var chartOfAccountTypes []*entities.ChartOfAccountType + err := r.db.WithContext(ctx).Where("is_active = ?", true).Find(&chartOfAccountTypes).Error + return chartOfAccountTypes, err +} diff --git a/internal/repository/ingredient_unit_converter_repository.go b/internal/repository/ingredient_unit_converter_repository.go new file mode 100644 index 0000000..88ae55a --- /dev/null +++ b/internal/repository/ingredient_unit_converter_repository.go @@ -0,0 +1,176 @@ +package repository + +import ( + "apskel-pos-be/internal/entities" + "context" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type IngredientUnitConverterRepository interface { + Create(ctx context.Context, converter *entities.IngredientUnitConverter) error + GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.IngredientUnitConverter, error) + GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.IngredientUnitConverter, error) + Update(ctx context.Context, converter *entities.IngredientUnitConverter) 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.IngredientUnitConverter, int, error) + GetByIngredientAndUnits(ctx context.Context, ingredientID, fromUnitID, toUnitID, organizationID uuid.UUID) (*entities.IngredientUnitConverter, error) + GetConvertersForIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.IngredientUnitConverter, error) + GetActiveConverters(ctx context.Context, organizationID uuid.UUID) ([]*entities.IngredientUnitConverter, error) + ConvertQuantity(ctx context.Context, ingredientID, fromUnitID, toUnitID, organizationID uuid.UUID, quantity float64) (float64, error) +} + +type IngredientUnitConverterRepositoryImpl struct { + db *gorm.DB +} + +func NewIngredientUnitConverterRepositoryImpl(db *gorm.DB) IngredientUnitConverterRepository { + return &IngredientUnitConverterRepositoryImpl{ + db: db, + } +} + +func (r *IngredientUnitConverterRepositoryImpl) Create(ctx context.Context, converter *entities.IngredientUnitConverter) error { + return r.db.WithContext(ctx).Create(converter).Error +} + +func (r *IngredientUnitConverterRepositoryImpl) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.IngredientUnitConverter, error) { + var converter entities.IngredientUnitConverter + err := r.db.WithContext(ctx). + Where("id = ? AND organization_id = ?", id, organizationID). + Preload("Ingredient"). + Preload("FromUnit"). + Preload("ToUnit"). + Preload("CreatedByUser"). + Preload("UpdatedByUser"). + First(&converter).Error + if err != nil { + return nil, err + } + return &converter, nil +} + +func (r *IngredientUnitConverterRepositoryImpl) GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.IngredientUnitConverter, error) { + return r.GetByID(ctx, id, organizationID) +} + +func (r *IngredientUnitConverterRepositoryImpl) Update(ctx context.Context, converter *entities.IngredientUnitConverter) error { + return r.db.WithContext(ctx).Save(converter).Error +} + +func (r *IngredientUnitConverterRepositoryImpl) Delete(ctx context.Context, id, organizationID uuid.UUID) error { + return r.db.WithContext(ctx). + Where("id = ? AND organization_id = ?", id, organizationID). + Delete(&entities.IngredientUnitConverter{}).Error +} + +func (r *IngredientUnitConverterRepositoryImpl) List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*entities.IngredientUnitConverter, int, error) { + var converters []*entities.IngredientUnitConverter + var total int64 + + query := r.db.WithContext(ctx). + Model(&entities.IngredientUnitConverter{}). + Where("organization_id = ?", organizationID) + + // Apply filters + if ingredientID, ok := filters["ingredient_id"].(uuid.UUID); ok { + query = query.Where("ingredient_id = ?", ingredientID) + } + if fromUnitID, ok := filters["from_unit_id"].(uuid.UUID); ok { + query = query.Where("from_unit_id = ?", fromUnitID) + } + if toUnitID, ok := filters["to_unit_id"].(uuid.UUID); ok { + query = query.Where("to_unit_id = ?", toUnitID) + } + if isActive, ok := filters["is_active"].(bool); ok { + query = query.Where("is_active = ?", isActive) + } + if search, ok := filters["search"].(string); ok && search != "" { + query = query.Joins("LEFT JOIN ingredients ON ingredient_unit_converters.ingredient_id = ingredients.id"). + Joins("LEFT JOIN units AS from_units ON ingredient_unit_converters.from_unit_id = from_units.id"). + Joins("LEFT JOIN units AS to_units ON ingredient_unit_converters.to_unit_id = to_units.id"). + Where("ingredients.name ILIKE ? OR from_units.name ILIKE ? OR to_units.name ILIKE ?", + "%"+search+"%", "%"+search+"%", "%"+search+"%") + } + + // 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("Ingredient"). + Preload("FromUnit"). + Preload("ToUnit"). + Preload("CreatedByUser"). + Preload("UpdatedByUser"). + Order("created_at DESC"). + Offset(offset). + Limit(limit). + Find(&converters).Error + + return converters, int(total), err +} + +func (r *IngredientUnitConverterRepositoryImpl) GetByIngredientAndUnits(ctx context.Context, ingredientID, fromUnitID, toUnitID, organizationID uuid.UUID) (*entities.IngredientUnitConverter, error) { + var converter entities.IngredientUnitConverter + err := r.db.WithContext(ctx). + Where("ingredient_id = ? AND from_unit_id = ? AND to_unit_id = ? AND organization_id = ? AND is_active = ?", + ingredientID, fromUnitID, toUnitID, organizationID, true). + Preload("Ingredient"). + Preload("FromUnit"). + Preload("ToUnit"). + First(&converter).Error + if err != nil { + return nil, err + } + return &converter, nil +} + +func (r *IngredientUnitConverterRepositoryImpl) GetConvertersForIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.IngredientUnitConverter, error) { + var converters []*entities.IngredientUnitConverter + err := r.db.WithContext(ctx). + Where("ingredient_id = ? AND organization_id = ? AND is_active = ?", ingredientID, organizationID, true). + Preload("FromUnit"). + Preload("ToUnit"). + Find(&converters).Error + return converters, err +} + +func (r *IngredientUnitConverterRepositoryImpl) GetActiveConverters(ctx context.Context, organizationID uuid.UUID) ([]*entities.IngredientUnitConverter, error) { + var converters []*entities.IngredientUnitConverter + err := r.db.WithContext(ctx). + Where("organization_id = ? AND is_active = ?", organizationID, true). + Preload("Ingredient"). + Preload("FromUnit"). + Preload("ToUnit"). + Find(&converters).Error + return converters, err +} + +func (r *IngredientUnitConverterRepositoryImpl) ConvertQuantity(ctx context.Context, ingredientID, fromUnitID, toUnitID, organizationID uuid.UUID, quantity float64) (float64, error) { + // If from and to units are the same, return the same quantity + if fromUnitID == toUnitID { + return quantity, nil + } + + // Try to find direct converter + converter, err := r.GetByIngredientAndUnits(ctx, ingredientID, fromUnitID, toUnitID, organizationID) + if err == nil { + return quantity * converter.ConversionFactor, nil + } + + // If direct converter not found, try to find reverse converter + reverseConverter, err := r.GetByIngredientAndUnits(ctx, ingredientID, toUnitID, fromUnitID, organizationID) + if err == nil { + return quantity / reverseConverter.ConversionFactor, nil + } + + // If no converter found, return error + return 0, fmt.Errorf("no conversion found between units %s and %s for ingredient %s", fromUnitID, toUnitID, ingredientID) +} + diff --git a/internal/repository/purchase_order_repository.go b/internal/repository/purchase_order_repository.go new file mode 100644 index 0000000..0d51a8e --- /dev/null +++ b/internal/repository/purchase_order_repository.go @@ -0,0 +1,248 @@ +package repository + +import ( + "context" + "strings" + "time" + + "github.com/google/uuid" + + "apskel-pos-be/internal/entities" + + "gorm.io/gorm" +) + +type PurchaseOrderRepositoryImpl struct { + db *gorm.DB +} + +func NewPurchaseOrderRepositoryImpl(db *gorm.DB) *PurchaseOrderRepositoryImpl { + return &PurchaseOrderRepositoryImpl{ + db: db, + } +} + +func (r *PurchaseOrderRepositoryImpl) Create(ctx context.Context, po *entities.PurchaseOrder) error { + return r.db.WithContext(ctx).Create(po).Error +} + +func (r *PurchaseOrderRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.PurchaseOrder, error) { + var po entities.PurchaseOrder + err := r.db.WithContext(ctx). + Preload("Vendor"). + Preload("Items.Ingredient"). + Preload("Items.Unit"). + Preload("Attachments.File"). + First(&po, "id = ?", id).Error + if err != nil { + return nil, err + } + return &po, nil +} + +func (r *PurchaseOrderRepositoryImpl) GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.PurchaseOrder, error) { + var po entities.PurchaseOrder + err := r.db.WithContext(ctx). + Preload("Vendor"). + Preload("Items.Ingredient"). + Preload("Items.Unit"). + Preload("Attachments.File"). + Where("id = ? AND organization_id = ?", id, organizationID). + First(&po).Error + if err != nil { + return nil, err + } + return &po, nil +} + +func (r *PurchaseOrderRepositoryImpl) Update(ctx context.Context, po *entities.PurchaseOrder) error { + return r.db.WithContext(ctx).Save(po).Error +} + +func (r *PurchaseOrderRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.PurchaseOrder{}, "id = ?", id).Error +} + +func (r *PurchaseOrderRepositoryImpl) List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.PurchaseOrder, int64, error) { + var pos []*entities.PurchaseOrder + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.PurchaseOrder{}).Where("organization_id = ?", organizationID) + + // Apply filters + for key, value := range filters { + switch key { + case "search": + if searchStr, ok := value.(string); ok && searchStr != "" { + searchPattern := "%" + strings.ToLower(searchStr) + "%" + query = query.Where("LOWER(po_number) LIKE ? OR LOWER(reference) LIKE ?", searchPattern, searchPattern) + } + case "status": + if status, ok := value.(string); ok && status != "" { + query = query.Where("status = ?", status) + } + case "vendor_id": + if vendorID, ok := value.(uuid.UUID); ok { + query = query.Where("vendor_id = ?", vendorID) + } + case "start_date": + if startDate, ok := value.(time.Time); ok { + query = query.Where("transaction_date >= ?", startDate) + } + case "end_date": + if endDate, ok := value.(time.Time); ok { + query = query.Where("transaction_date <= ?", endDate) + } + default: + query = query.Where(key+" = ?", value) + } + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + err := query. + Preload("Vendor"). + Preload("Items.Ingredient"). + Preload("Items.Unit"). + Preload("Attachments.File"). + Order("created_at DESC"). + Limit(limit). + Offset(offset). + Find(&pos).Error + return pos, total, err +} + +func (r *PurchaseOrderRepositoryImpl) Count(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}) (int64, error) { + var count int64 + query := r.db.WithContext(ctx).Model(&entities.PurchaseOrder{}).Where("organization_id = ?", organizationID) + + // Apply filters + for key, value := range filters { + switch key { + case "search": + if searchStr, ok := value.(string); ok && searchStr != "" { + searchPattern := "%" + strings.ToLower(searchStr) + "%" + query = query.Where("LOWER(po_number) LIKE ? OR LOWER(reference) LIKE ?", searchPattern, searchPattern) + } + case "status": + if status, ok := value.(string); ok && status != "" { + query = query.Where("status = ?", status) + } + case "vendor_id": + if vendorID, ok := value.(uuid.UUID); ok { + query = query.Where("vendor_id = ?", vendorID) + } + case "start_date": + if startDate, ok := value.(time.Time); ok { + query = query.Where("transaction_date >= ?", startDate) + } + case "end_date": + if endDate, ok := value.(time.Time); ok { + query = query.Where("transaction_date <= ?", endDate) + } + default: + query = query.Where(key+" = ?", value) + } + } + + err := query.Count(&count).Error + return count, err +} + +func (r *PurchaseOrderRepositoryImpl) GetByPONumber(ctx context.Context, poNumber string, organizationID uuid.UUID) (*entities.PurchaseOrder, error) { + var po entities.PurchaseOrder + err := r.db.WithContext(ctx). + Where("po_number = ? AND organization_id = ?", poNumber, organizationID). + First(&po).Error + if err != nil { + return nil, err + } + return &po, nil +} + +func (r *PurchaseOrderRepositoryImpl) GetByStatus(ctx context.Context, organizationID uuid.UUID, status string) ([]*entities.PurchaseOrder, error) { + var pos []*entities.PurchaseOrder + err := r.db.WithContext(ctx). + Where("organization_id = ? AND status = ?", organizationID, status). + Preload("Vendor"). + Preload("Items.Ingredient"). + Preload("Items.Unit"). + Find(&pos).Error + return pos, err +} + +func (r *PurchaseOrderRepositoryImpl) GetOverdue(ctx context.Context, organizationID uuid.UUID) ([]*entities.PurchaseOrder, error) { + var pos []*entities.PurchaseOrder + err := r.db.WithContext(ctx). + Where("organization_id = ? AND due_date < ? AND status IN (?)", organizationID, time.Now(), []string{"draft", "sent", "approved"}). + Preload("Vendor"). + Preload("Items.Ingredient"). + Preload("Items.Unit"). + Find(&pos).Error + return pos, err +} + +func (r *PurchaseOrderRepositoryImpl) UpdateStatus(ctx context.Context, id uuid.UUID, status string) error { + return r.db.WithContext(ctx). + Model(&entities.PurchaseOrder{}). + Where("id = ?", id). + Update("status", status).Error +} + +func (r *PurchaseOrderRepositoryImpl) UpdateTotalAmount(ctx context.Context, id uuid.UUID, totalAmount float64) error { + return r.db.WithContext(ctx). + Model(&entities.PurchaseOrder{}). + Where("id = ?", id). + Update("total_amount", totalAmount).Error +} + +// Purchase Order Items methods +func (r *PurchaseOrderRepositoryImpl) CreateItem(ctx context.Context, item *entities.PurchaseOrderItem) error { + return r.db.WithContext(ctx).Create(item).Error +} + +func (r *PurchaseOrderRepositoryImpl) UpdateItem(ctx context.Context, item *entities.PurchaseOrderItem) error { + return r.db.WithContext(ctx).Save(item).Error +} + +func (r *PurchaseOrderRepositoryImpl) DeleteItem(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.PurchaseOrderItem{}, "id = ?", id).Error +} + +func (r *PurchaseOrderRepositoryImpl) DeleteItemsByPurchaseOrderID(ctx context.Context, purchaseOrderID uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.PurchaseOrderItem{}, "purchase_order_id = ?", purchaseOrderID).Error +} + +func (r *PurchaseOrderRepositoryImpl) GetItemsByPurchaseOrderID(ctx context.Context, purchaseOrderID uuid.UUID) ([]*entities.PurchaseOrderItem, error) { + var items []*entities.PurchaseOrderItem + err := r.db.WithContext(ctx). + Preload("Ingredient"). + Preload("Unit"). + Where("purchase_order_id = ?", purchaseOrderID). + Find(&items).Error + return items, err +} + +// Purchase Order Attachments methods +func (r *PurchaseOrderRepositoryImpl) CreateAttachment(ctx context.Context, attachment *entities.PurchaseOrderAttachment) error { + return r.db.WithContext(ctx).Create(attachment).Error +} + +func (r *PurchaseOrderRepositoryImpl) DeleteAttachment(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.PurchaseOrderAttachment{}, "id = ?", id).Error +} + +func (r *PurchaseOrderRepositoryImpl) DeleteAttachmentsByPurchaseOrderID(ctx context.Context, purchaseOrderID uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.PurchaseOrderAttachment{}, "purchase_order_id = ?", purchaseOrderID).Error +} + +func (r *PurchaseOrderRepositoryImpl) GetAttachmentsByPurchaseOrderID(ctx context.Context, purchaseOrderID uuid.UUID) ([]*entities.PurchaseOrderAttachment, error) { + var attachments []*entities.PurchaseOrderAttachment + err := r.db.WithContext(ctx). + Preload("File"). + Where("purchase_order_id = ?", purchaseOrderID). + Find(&attachments).Error + return attachments, err +} diff --git a/internal/repository/vendor_repository.go b/internal/repository/vendor_repository.go new file mode 100644 index 0000000..d1942df --- /dev/null +++ b/internal/repository/vendor_repository.go @@ -0,0 +1,134 @@ +package repository + +import ( + "context" + "strings" + + "github.com/google/uuid" + + "apskel-pos-be/internal/entities" + + "gorm.io/gorm" +) + +type VendorRepositoryImpl struct { + db *gorm.DB +} + +func NewVendorRepositoryImpl(db *gorm.DB) *VendorRepositoryImpl { + return &VendorRepositoryImpl{ + db: db, + } +} + +func (r *VendorRepositoryImpl) Create(ctx context.Context, vendor *entities.Vendor) error { + return r.db.WithContext(ctx).Create(vendor).Error +} + +func (r *VendorRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Vendor, error) { + var vendor entities.Vendor + err := r.db.WithContext(ctx).First(&vendor, "id = ?", id).Error + if err != nil { + return nil, err + } + return &vendor, nil +} + +func (r *VendorRepositoryImpl) GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Vendor, error) { + var vendor entities.Vendor + err := r.db.WithContext(ctx).Where("id = ? AND organization_id = ?", id, organizationID).First(&vendor).Error + if err != nil { + return nil, err + } + return &vendor, nil +} + +func (r *VendorRepositoryImpl) Update(ctx context.Context, vendor *entities.Vendor) error { + return r.db.WithContext(ctx).Save(vendor).Error +} + +func (r *VendorRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Vendor{}, "id = ?", id).Error +} + +func (r *VendorRepositoryImpl) List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.Vendor, int64, error) { + var vendors []*entities.Vendor + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.Vendor{}).Where("organization_id = ?", organizationID) + + // Apply filters + for key, value := range filters { + switch key { + case "search": + if searchStr, ok := value.(string); ok && searchStr != "" { + searchPattern := "%" + strings.ToLower(searchStr) + "%" + query = query.Where("LOWER(name) LIKE ? OR LOWER(email) LIKE ? OR LOWER(contact_person) LIKE ?", + searchPattern, searchPattern, searchPattern) + } + case "is_active": + if isActive, ok := value.(bool); ok { + query = query.Where("is_active = ?", isActive) + } + default: + query = query.Where(key+" = ?", value) + } + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&vendors).Error + return vendors, total, err +} + +func (r *VendorRepositoryImpl) Count(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}) (int64, error) { + var count int64 + query := r.db.WithContext(ctx).Model(&entities.Vendor{}).Where("organization_id = ?", organizationID) + + // Apply filters + for key, value := range filters { + switch key { + case "search": + if searchStr, ok := value.(string); ok && searchStr != "" { + searchPattern := "%" + strings.ToLower(searchStr) + "%" + query = query.Where("LOWER(name) LIKE ? OR LOWER(email) LIKE ? OR LOWER(contact_person) LIKE ?", + searchPattern, searchPattern, searchPattern) + } + case "is_active": + if isActive, ok := value.(bool); ok { + query = query.Where("is_active = ?", isActive) + } + default: + query = query.Where(key+" = ?", value) + } + } + + err := query.Count(&count).Error + return count, err +} + +func (r *VendorRepositoryImpl) GetByEmail(ctx context.Context, email string, organizationID uuid.UUID) (*entities.Vendor, error) { + var vendor entities.Vendor + err := r.db.WithContext(ctx).Where("email = ? AND organization_id = ?", email, organizationID).First(&vendor).Error + if err != nil { + return nil, err + } + return &vendor, nil +} + +func (r *VendorRepositoryImpl) GetByName(ctx context.Context, name string, organizationID uuid.UUID) (*entities.Vendor, error) { + var vendor entities.Vendor + err := r.db.WithContext(ctx).Where("name = ? AND organization_id = ?", name, organizationID).First(&vendor).Error + if err != nil { + return nil, err + } + return &vendor, nil +} + +func (r *VendorRepositoryImpl) GetActiveVendors(ctx context.Context, organizationID uuid.UUID) ([]*entities.Vendor, error) { + var vendors []*entities.Vendor + err := r.db.WithContext(ctx).Where("organization_id = ? AND is_active = ?", organizationID, true).Find(&vendors).Error + return vendors, err +} diff --git a/internal/router/router.go b/internal/router/router.go index 6732a71..abf50f2 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -12,28 +12,34 @@ 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 - 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 + authMiddleware *middleware.AuthMiddleware } func NewRouter(cfg *config.Config, @@ -69,30 +75,48 @@ func NewRouter(cfg *config.Config, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, - productRecipeService service.ProductRecipeService) *Router { + productRecipeService service.ProductRecipeService, + vendorService service.VendorService, + vendorValidator validator.VendorValidator, + purchaseOrderService service.PurchaseOrderService, + purchaseOrderValidator validator.PurchaseOrderValidator, + unitConverterService service.IngredientUnitConverterService, + unitConverterValidator validator.IngredientUnitConverterValidator, + chartOfAccountTypeService service.ChartOfAccountTypeService, + chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, + chartOfAccountService service.ChartOfAccountService, + chartOfAccountValidator validator.ChartOfAccountValidator, + accountService service.AccountService, + accountValidator validator.AccountValidator) *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), - 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), + authMiddleware: authMiddleware, } } @@ -297,6 +321,42 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { ingredients.DELETE("/:id", r.ingredientHandler.Delete) } + vendors := protected.Group("/vendors") + vendors.Use(r.authMiddleware.RequireAdminOrManager()) + { + vendors.POST("", r.vendorHandler.CreateVendor) + vendors.GET("", r.vendorHandler.ListVendors) + vendors.GET("/active", r.vendorHandler.GetActiveVendors) + vendors.GET("/:id", r.vendorHandler.GetVendor) + vendors.PUT("/:id", r.vendorHandler.UpdateVendor) + vendors.DELETE("/:id", r.vendorHandler.DeleteVendor) + } + + purchaseOrders := protected.Group("/purchase-orders") + purchaseOrders.Use(r.authMiddleware.RequireAdminOrManager()) + { + purchaseOrders.POST("", r.purchaseOrderHandler.CreatePurchaseOrder) + purchaseOrders.GET("", r.purchaseOrderHandler.ListPurchaseOrders) + purchaseOrders.GET("/status/:status", r.purchaseOrderHandler.GetPurchaseOrdersByStatus) + purchaseOrders.GET("/overdue", r.purchaseOrderHandler.GetOverduePurchaseOrders) + purchaseOrders.GET("/:id", r.purchaseOrderHandler.GetPurchaseOrder) + purchaseOrders.PUT("/:id", r.purchaseOrderHandler.UpdatePurchaseOrder) + purchaseOrders.PUT("/:id/status/:status", r.purchaseOrderHandler.UpdatePurchaseOrderStatus) + purchaseOrders.DELETE("/:id", r.purchaseOrderHandler.DeletePurchaseOrder) + } + + unitConverters := protected.Group("/unit-converters") + unitConverters.Use(r.authMiddleware.RequireAdminOrManager()) + { + unitConverters.POST("", r.unitConverterHandler.CreateIngredientUnitConverter) + unitConverters.GET("", r.unitConverterHandler.ListIngredientUnitConverters) + unitConverters.GET("/ingredient/:ingredient_id", r.unitConverterHandler.GetConvertersForIngredient) + unitConverters.POST("/convert", r.unitConverterHandler.ConvertUnit) + unitConverters.GET("/:id", r.unitConverterHandler.GetIngredientUnitConverter) + unitConverters.PUT("/:id", r.unitConverterHandler.UpdateIngredientUnitConverter) + unitConverters.DELETE("/:id", r.unitConverterHandler.DeleteIngredientUnitConverter) + } + productRecipes := protected.Group("/product-recipes") productRecipes.Use(r.authMiddleware.RequireAdminOrManager()) { @@ -309,6 +369,43 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { productRecipes.GET("/ingredient/:ingredient_id", r.productRecipeHandler.GetByIngredientID) } + // Accounting routes + chartOfAccountTypes := protected.Group("/chart-of-account-types") + chartOfAccountTypes.Use(r.authMiddleware.RequireAdminOrManager()) + { + chartOfAccountTypes.POST("", r.chartOfAccountTypeHandler.CreateChartOfAccountType) + chartOfAccountTypes.GET("", r.chartOfAccountTypeHandler.ListChartOfAccountTypes) + chartOfAccountTypes.GET("/:id", r.chartOfAccountTypeHandler.GetChartOfAccountTypeByID) + chartOfAccountTypes.PUT("/:id", r.chartOfAccountTypeHandler.UpdateChartOfAccountType) + chartOfAccountTypes.DELETE("/:id", r.chartOfAccountTypeHandler.DeleteChartOfAccountType) + } + + chartOfAccounts := protected.Group("/chart-of-accounts") + chartOfAccounts.Use(r.authMiddleware.RequireAdminOrManager()) + { + chartOfAccounts.POST("", r.chartOfAccountHandler.CreateChartOfAccount) + chartOfAccounts.GET("", r.chartOfAccountHandler.ListChartOfAccounts) + chartOfAccounts.GET("/:id", r.chartOfAccountHandler.GetChartOfAccountByID) + chartOfAccounts.PUT("/:id", r.chartOfAccountHandler.UpdateChartOfAccount) + chartOfAccounts.DELETE("/:id", r.chartOfAccountHandler.DeleteChartOfAccount) + chartOfAccounts.GET("/organization/:organization_id", r.chartOfAccountHandler.GetChartOfAccountsByOrganization) + chartOfAccounts.GET("/organization/:organization_id/type/:type_id", r.chartOfAccountHandler.GetChartOfAccountsByType) + } + + accounts := protected.Group("/accounts") + accounts.Use(r.authMiddleware.RequireAdminOrManager()) + { + accounts.POST("", r.accountHandler.CreateAccount) + accounts.GET("", r.accountHandler.ListAccounts) + accounts.GET("/:id", r.accountHandler.GetAccountByID) + accounts.PUT("/:id", r.accountHandler.UpdateAccount) + accounts.DELETE("/:id", r.accountHandler.DeleteAccount) + accounts.GET("/organization/:organization_id", r.accountHandler.GetAccountsByOrganization) + accounts.GET("/chart-of-account/:chart_of_account_id", r.accountHandler.GetAccountsByChartOfAccount) + accounts.PUT("/:id/balance", r.accountHandler.UpdateAccountBalance) + accounts.GET("/:id/balance", r.accountHandler.GetAccountBalance) + } + outlets := protected.Group("/outlets") outlets.Use(r.authMiddleware.RequireAdminOrManager()) { diff --git a/internal/service/account_service.go b/internal/service/account_service.go new file mode 100644 index 0000000..83a5c9f --- /dev/null +++ b/internal/service/account_service.go @@ -0,0 +1,106 @@ +package service + +import ( + "context" + + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/processor" + + "github.com/google/uuid" +) + +type AccountService interface { + contract.AccountContract +} + +type AccountServiceImpl struct { + processor processor.AccountProcessor +} + +func NewAccountService(processor processor.AccountProcessor) AccountService { + return &AccountServiceImpl{ + processor: processor, + } +} + +func (s *AccountServiceImpl) CreateAccount(ctx context.Context, req *contract.CreateAccountRequest) (*contract.AccountResponse, error) { + modelReq := mappers.ContractToModelCreateAccountRequest(req) + modelResp, err := s.processor.CreateAccount(ctx, modelReq) + if err != nil { + return nil, err + } + return mappers.ModelToContractAccountResponse(modelResp), nil +} + +func (s *AccountServiceImpl) GetAccountByID(ctx context.Context, id uuid.UUID) (*contract.AccountResponse, error) { + modelResp, err := s.processor.GetAccountByID(ctx, id) + if err != nil { + return nil, err + } + return mappers.ModelToContractAccountResponse(modelResp), nil +} + +func (s *AccountServiceImpl) UpdateAccount(ctx context.Context, id uuid.UUID, req *contract.UpdateAccountRequest) (*contract.AccountResponse, error) { + modelReq := mappers.ContractToModelUpdateAccountRequest(req) + modelResp, err := s.processor.UpdateAccount(ctx, id, modelReq) + if err != nil { + return nil, err + } + return mappers.ModelToContractAccountResponse(modelResp), nil +} + +func (s *AccountServiceImpl) DeleteAccount(ctx context.Context, id uuid.UUID) error { + return s.processor.DeleteAccount(ctx, id) +} + +func (s *AccountServiceImpl) ListAccounts(ctx context.Context, req *contract.ListAccountsRequest) ([]contract.AccountResponse, int, error) { + modelReq := mappers.ContractToModelListAccountsRequest(req) + modelResp, total, err := s.processor.ListAccounts(ctx, modelReq) + if err != nil { + return nil, 0, err + } + + contractResp := make([]contract.AccountResponse, len(modelResp)) + for i, resp := range modelResp { + contractResp[i] = *mappers.ModelToContractAccountResponse(&resp) + } + + return contractResp, total, nil +} + +func (s *AccountServiceImpl) GetAccountsByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]contract.AccountResponse, error) { + modelResp, err := s.processor.GetAccountsByOrganization(ctx, organizationID, outletID) + if err != nil { + return nil, err + } + + contractResp := make([]contract.AccountResponse, len(modelResp)) + for i, resp := range modelResp { + contractResp[i] = *mappers.ModelToContractAccountResponse(&resp) + } + + return contractResp, nil +} + +func (s *AccountServiceImpl) GetAccountsByChartOfAccount(ctx context.Context, chartOfAccountID uuid.UUID) ([]contract.AccountResponse, error) { + modelResp, err := s.processor.GetAccountsByChartOfAccount(ctx, chartOfAccountID) + if err != nil { + return nil, err + } + + contractResp := make([]contract.AccountResponse, len(modelResp)) + for i, resp := range modelResp { + contractResp[i] = *mappers.ModelToContractAccountResponse(&resp) + } + + return contractResp, nil +} + +func (s *AccountServiceImpl) UpdateAccountBalance(ctx context.Context, id uuid.UUID, req *contract.UpdateAccountBalanceRequest) error { + return s.processor.UpdateAccountBalance(ctx, id, req.Amount) +} + +func (s *AccountServiceImpl) GetAccountBalance(ctx context.Context, id uuid.UUID) (float64, error) { + return s.processor.GetAccountBalance(ctx, id) +} diff --git a/internal/service/chart_of_account_service.go b/internal/service/chart_of_account_service.go new file mode 100644 index 0000000..ade6963 --- /dev/null +++ b/internal/service/chart_of_account_service.go @@ -0,0 +1,98 @@ +package service + +import ( + "context" + + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/processor" + + "github.com/google/uuid" +) + +type ChartOfAccountService interface { + contract.ChartOfAccountContract +} + +type ChartOfAccountServiceImpl struct { + processor processor.ChartOfAccountProcessor +} + +func NewChartOfAccountService(processor processor.ChartOfAccountProcessor) ChartOfAccountService { + return &ChartOfAccountServiceImpl{ + processor: processor, + } +} + +func (s *ChartOfAccountServiceImpl) CreateChartOfAccount(ctx context.Context, req *contract.CreateChartOfAccountRequest) (*contract.ChartOfAccountResponse, error) { + modelReq := mappers.ContractToModelCreateChartOfAccountRequest(req) + modelResp, err := s.processor.CreateChartOfAccount(ctx, modelReq) + if err != nil { + return nil, err + } + return mappers.ModelToContractChartOfAccountResponse(modelResp), nil +} + +func (s *ChartOfAccountServiceImpl) GetChartOfAccountByID(ctx context.Context, id uuid.UUID) (*contract.ChartOfAccountResponse, error) { + modelResp, err := s.processor.GetChartOfAccountByID(ctx, id) + if err != nil { + return nil, err + } + return mappers.ModelToContractChartOfAccountResponse(modelResp), nil +} + +func (s *ChartOfAccountServiceImpl) UpdateChartOfAccount(ctx context.Context, id uuid.UUID, req *contract.UpdateChartOfAccountRequest) (*contract.ChartOfAccountResponse, error) { + modelReq := mappers.ContractToModelUpdateChartOfAccountRequest(req) + modelResp, err := s.processor.UpdateChartOfAccount(ctx, id, modelReq) + if err != nil { + return nil, err + } + return mappers.ModelToContractChartOfAccountResponse(modelResp), nil +} + +func (s *ChartOfAccountServiceImpl) DeleteChartOfAccount(ctx context.Context, id uuid.UUID) error { + return s.processor.DeleteChartOfAccount(ctx, id) +} + +func (s *ChartOfAccountServiceImpl) ListChartOfAccounts(ctx context.Context, req *contract.ListChartOfAccountsRequest) ([]contract.ChartOfAccountResponse, int, error) { + modelReq := mappers.ContractToModelListChartOfAccountsRequest(req) + modelResp, total, err := s.processor.ListChartOfAccounts(ctx, modelReq) + if err != nil { + return nil, 0, err + } + + contractResp := make([]contract.ChartOfAccountResponse, len(modelResp)) + for i, resp := range modelResp { + contractResp[i] = *mappers.ModelToContractChartOfAccountResponse(&resp) + } + + return contractResp, total, nil +} + +func (s *ChartOfAccountServiceImpl) GetChartOfAccountsByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]contract.ChartOfAccountResponse, error) { + modelResp, err := s.processor.GetChartOfAccountsByOrganization(ctx, organizationID, outletID) + if err != nil { + return nil, err + } + + contractResp := make([]contract.ChartOfAccountResponse, len(modelResp)) + for i, resp := range modelResp { + contractResp[i] = *mappers.ModelToContractChartOfAccountResponse(&resp) + } + + return contractResp, nil +} + +func (s *ChartOfAccountServiceImpl) GetChartOfAccountsByType(ctx context.Context, organizationID uuid.UUID, chartOfAccountTypeID uuid.UUID, outletID *uuid.UUID) ([]contract.ChartOfAccountResponse, error) { + modelResp, err := s.processor.GetChartOfAccountsByType(ctx, organizationID, chartOfAccountTypeID, outletID) + if err != nil { + return nil, err + } + + contractResp := make([]contract.ChartOfAccountResponse, len(modelResp)) + for i, resp := range modelResp { + contractResp[i] = *mappers.ModelToContractChartOfAccountResponse(&resp) + } + + return contractResp, nil +} diff --git a/internal/service/chart_of_account_type_service.go b/internal/service/chart_of_account_type_service.go new file mode 100644 index 0000000..a4085d9 --- /dev/null +++ b/internal/service/chart_of_account_type_service.go @@ -0,0 +1,69 @@ +package service + +import ( + "context" + + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/processor" + + "github.com/google/uuid" +) + +type ChartOfAccountTypeService interface { + contract.ChartOfAccountTypeContract +} + +type ChartOfAccountTypeServiceImpl struct { + processor processor.ChartOfAccountTypeProcessor +} + +func NewChartOfAccountTypeService(processor processor.ChartOfAccountTypeProcessor) ChartOfAccountTypeService { + return &ChartOfAccountTypeServiceImpl{ + processor: processor, + } +} + +func (s *ChartOfAccountTypeServiceImpl) CreateChartOfAccountType(ctx context.Context, req *contract.CreateChartOfAccountTypeRequest) (*contract.ChartOfAccountTypeResponse, error) { + modelReq := mappers.ContractToModelCreateChartOfAccountTypeRequest(req) + modelResp, err := s.processor.CreateChartOfAccountType(ctx, modelReq) + if err != nil { + return nil, err + } + return mappers.ModelToContractChartOfAccountTypeResponse(modelResp), nil +} + +func (s *ChartOfAccountTypeServiceImpl) GetChartOfAccountTypeByID(ctx context.Context, id uuid.UUID) (*contract.ChartOfAccountTypeResponse, error) { + modelResp, err := s.processor.GetChartOfAccountTypeByID(ctx, id) + if err != nil { + return nil, err + } + return mappers.ModelToContractChartOfAccountTypeResponse(modelResp), nil +} + +func (s *ChartOfAccountTypeServiceImpl) UpdateChartOfAccountType(ctx context.Context, id uuid.UUID, req *contract.UpdateChartOfAccountTypeRequest) (*contract.ChartOfAccountTypeResponse, error) { + modelReq := mappers.ContractToModelUpdateChartOfAccountTypeRequest(req) + modelResp, err := s.processor.UpdateChartOfAccountType(ctx, id, modelReq) + if err != nil { + return nil, err + } + return mappers.ModelToContractChartOfAccountTypeResponse(modelResp), nil +} + +func (s *ChartOfAccountTypeServiceImpl) DeleteChartOfAccountType(ctx context.Context, id uuid.UUID) error { + return s.processor.DeleteChartOfAccountType(ctx, id) +} + +func (s *ChartOfAccountTypeServiceImpl) ListChartOfAccountTypes(ctx context.Context, filters map[string]interface{}, page, limit int) ([]contract.ChartOfAccountTypeResponse, int, error) { + modelResp, total, err := s.processor.ListChartOfAccountTypes(ctx, filters, page, limit) + if err != nil { + return nil, 0, err + } + + contractResp := make([]contract.ChartOfAccountTypeResponse, len(modelResp)) + for i, resp := range modelResp { + contractResp[i] = *mappers.ModelToContractChartOfAccountTypeResponse(&resp) + } + + return contractResp, total, nil +} diff --git a/internal/service/ingredient_unit_converter_service.go b/internal/service/ingredient_unit_converter_service.go new file mode 100644 index 0000000..665bad1 --- /dev/null +++ b/internal/service/ingredient_unit_converter_service.go @@ -0,0 +1,151 @@ +package service + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/transformer" + "context" + + "github.com/google/uuid" +) + +type IngredientUnitConverterService interface { + CreateIngredientUnitConverter(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateIngredientUnitConverterRequest) *contract.Response + UpdateIngredientUnitConverter(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdateIngredientUnitConverterRequest) *contract.Response + DeleteIngredientUnitConverter(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response + GetIngredientUnitConverter(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response + ListIngredientUnitConverters(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListIngredientUnitConvertersRequest) *contract.Response + GetConvertersForIngredient(ctx context.Context, apctx *appcontext.ContextInfo, ingredientID uuid.UUID) *contract.Response + ConvertUnit(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ConvertUnitRequest) *contract.Response +} + +type IngredientUnitConverterServiceImpl struct { + converterProcessor processor.IngredientUnitConverterProcessor +} + +func NewIngredientUnitConverterService(converterProcessor processor.IngredientUnitConverterProcessor) *IngredientUnitConverterServiceImpl { + return &IngredientUnitConverterServiceImpl{ + converterProcessor: converterProcessor, + } +} + +func (s *IngredientUnitConverterServiceImpl) CreateIngredientUnitConverter(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateIngredientUnitConverterRequest) *contract.Response { + modelReq := transformer.CreateIngredientUnitConverterRequestToModel(req) + + converterResponse, err := s.converterProcessor.CreateIngredientUnitConverter(ctx, apctx.OrganizationID, apctx.UserID, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.IngredientUnitConverterServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.IngredientUnitConverterModelResponseToResponse(converterResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *IngredientUnitConverterServiceImpl) UpdateIngredientUnitConverter(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdateIngredientUnitConverterRequest) *contract.Response { + modelReq := transformer.UpdateIngredientUnitConverterRequestToModel(req) + + converterResponse, err := s.converterProcessor.UpdateIngredientUnitConverter(ctx, id, apctx.OrganizationID, apctx.UserID, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.IngredientUnitConverterServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.IngredientUnitConverterModelResponseToResponse(converterResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *IngredientUnitConverterServiceImpl) DeleteIngredientUnitConverter(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response { + err := s.converterProcessor.DeleteIngredientUnitConverter(ctx, id, apctx.OrganizationID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.IngredientUnitConverterServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + return contract.BuildSuccessResponse(nil) +} + +func (s *IngredientUnitConverterServiceImpl) GetIngredientUnitConverter(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response { + converterResponse, err := s.converterProcessor.GetIngredientUnitConverterByID(ctx, id, apctx.OrganizationID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.IngredientUnitConverterServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.IngredientUnitConverterModelResponseToResponse(converterResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *IngredientUnitConverterServiceImpl) ListIngredientUnitConverters(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListIngredientUnitConvertersRequest) *contract.Response { + modelReq := transformer.ListIngredientUnitConvertersRequestToModel(req) + + filters := make(map[string]interface{}) + if modelReq.IngredientID != nil { + filters["ingredient_id"] = *modelReq.IngredientID + } + if modelReq.FromUnitID != nil { + filters["from_unit_id"] = *modelReq.FromUnitID + } + if modelReq.ToUnitID != nil { + filters["to_unit_id"] = *modelReq.ToUnitID + } + if modelReq.IsActive != nil { + filters["is_active"] = *modelReq.IsActive + } + if modelReq.Search != "" { + filters["search"] = modelReq.Search + } + + converters, total, err := s.converterProcessor.ListIngredientUnitConverters(ctx, apctx.OrganizationID, filters, modelReq.Page, modelReq.Limit) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.IngredientUnitConverterServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponses := make([]contract.IngredientUnitConverterResponse, len(converters)) + for i, converter := range converters { + contractResponses[i] = transformer.IngredientUnitConverterModelResponseToResponse(converter) + } + + totalPages := (total + modelReq.Limit - 1) / modelReq.Limit + response := contract.ListIngredientUnitConvertersResponse{ + Converters: contractResponses, + TotalCount: total, + Page: modelReq.Page, + Limit: modelReq.Limit, + TotalPages: totalPages, + } + + return contract.BuildSuccessResponse(response) +} + +func (s *IngredientUnitConverterServiceImpl) GetConvertersForIngredient(ctx context.Context, apctx *appcontext.ContextInfo, ingredientID uuid.UUID) *contract.Response { + converters, err := s.converterProcessor.GetConvertersForIngredient(ctx, ingredientID, apctx.OrganizationID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.IngredientUnitConverterServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponses := make([]contract.IngredientUnitConverterResponse, len(converters)) + for i, converter := range converters { + contractResponses[i] = transformer.IngredientUnitConverterModelResponseToResponse(converter) + } + + return contract.BuildSuccessResponse(contractResponses) +} + +func (s *IngredientUnitConverterServiceImpl) ConvertUnit(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ConvertUnitRequest) *contract.Response { + modelReq := transformer.ConvertUnitRequestToModel(req) + + convertResponse, err := s.converterProcessor.ConvertUnit(ctx, apctx.OrganizationID, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.IngredientUnitConverterServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.ConvertUnitModelResponseToResponse(convertResponse) + return contract.BuildSuccessResponse(contractResponse) +} + diff --git a/internal/service/purchase_order_service.go b/internal/service/purchase_order_service.go new file mode 100644 index 0000000..db84a76 --- /dev/null +++ b/internal/service/purchase_order_service.go @@ -0,0 +1,175 @@ +package service + +import ( + "apskel-pos-be/internal/appcontext" + "context" + + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/transformer" + + "github.com/google/uuid" +) + +type PurchaseOrderService interface { + CreatePurchaseOrder(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreatePurchaseOrderRequest) *contract.Response + UpdatePurchaseOrder(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdatePurchaseOrderRequest) *contract.Response + DeletePurchaseOrder(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response + GetPurchaseOrderByID(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response + ListPurchaseOrders(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListPurchaseOrdersRequest) *contract.Response + GetPurchaseOrdersByStatus(ctx context.Context, apctx *appcontext.ContextInfo, status string) *contract.Response + GetOverduePurchaseOrders(ctx context.Context, apctx *appcontext.ContextInfo) *contract.Response + UpdatePurchaseOrderStatus(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, status string) *contract.Response +} + +type PurchaseOrderServiceImpl struct { + purchaseOrderProcessor processor.PurchaseOrderProcessor +} + +func NewPurchaseOrderService(purchaseOrderProcessor processor.PurchaseOrderProcessor) *PurchaseOrderServiceImpl { + return &PurchaseOrderServiceImpl{ + purchaseOrderProcessor: purchaseOrderProcessor, + } +} + +func (s *PurchaseOrderServiceImpl) CreatePurchaseOrder(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreatePurchaseOrderRequest) *contract.Response { + modelReq := transformer.CreatePurchaseOrderRequestToModel(req) + + poResponse, err := s.purchaseOrderProcessor.CreatePurchaseOrder(ctx, apctx.OrganizationID, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseOrderServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.PurchaseOrderModelResponseToResponse(poResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *PurchaseOrderServiceImpl) UpdatePurchaseOrder(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdatePurchaseOrderRequest) *contract.Response { + modelReq := transformer.UpdatePurchaseOrderRequestToModel(req) + + poResponse, err := s.purchaseOrderProcessor.UpdatePurchaseOrder(ctx, id, apctx.OrganizationID, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseOrderServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.PurchaseOrderModelResponseToResponse(poResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *PurchaseOrderServiceImpl) DeletePurchaseOrder(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response { + err := s.purchaseOrderProcessor.DeletePurchaseOrder(ctx, id, apctx.OrganizationID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseOrderServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + return contract.BuildSuccessResponse(map[string]interface{}{ + "message": "Purchase order deleted successfully", + }) +} + +func (s *PurchaseOrderServiceImpl) GetPurchaseOrderByID(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response { + poResponse, err := s.purchaseOrderProcessor.GetPurchaseOrderByID(ctx, id, apctx.OrganizationID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseOrderServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.PurchaseOrderModelResponseToResponse(poResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *PurchaseOrderServiceImpl) ListPurchaseOrders(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListPurchaseOrdersRequest) *contract.Response { + modelReq := transformer.ListPurchaseOrdersRequestToModel(req) + + filters := make(map[string]interface{}) + if modelReq.Search != "" { + filters["search"] = modelReq.Search + } + if modelReq.Status != "" { + filters["status"] = modelReq.Status + } + if modelReq.VendorID != nil { + filters["vendor_id"] = *modelReq.VendorID + } + if modelReq.StartDate != nil { + filters["start_date"] = *modelReq.StartDate + } + if modelReq.EndDate != nil { + filters["end_date"] = *modelReq.EndDate + } + + pos, totalPages, err := s.purchaseOrderProcessor.ListPurchaseOrders(ctx, apctx.OrganizationID, filters, modelReq.Page, modelReq.Limit) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseOrderServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponses := make([]contract.PurchaseOrderResponse, len(pos)) + for i, po := range pos { + response := transformer.PurchaseOrderModelResponseToResponse(po) + if response != nil { + contractResponses[i] = *response + } + } + + response := contract.ListPurchaseOrdersResponse{ + PurchaseOrders: contractResponses, + TotalCount: len(contractResponses), + Page: modelReq.Page, + Limit: modelReq.Limit, + TotalPages: totalPages, + } + + return contract.BuildSuccessResponse(response) +} + +func (s *PurchaseOrderServiceImpl) GetPurchaseOrdersByStatus(ctx context.Context, apctx *appcontext.ContextInfo, status string) *contract.Response { + poResponses, err := s.purchaseOrderProcessor.GetPurchaseOrdersByStatus(ctx, apctx.OrganizationID, status) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseOrderServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponses := make([]contract.PurchaseOrderResponse, len(poResponses)) + for i, po := range poResponses { + response := transformer.PurchaseOrderModelResponseToResponse(po) + if response != nil { + contractResponses[i] = *response + } + } + + return contract.BuildSuccessResponse(contractResponses) +} + +func (s *PurchaseOrderServiceImpl) GetOverduePurchaseOrders(ctx context.Context, apctx *appcontext.ContextInfo) *contract.Response { + poResponses, err := s.purchaseOrderProcessor.GetOverduePurchaseOrders(ctx, apctx.OrganizationID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseOrderServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponses := make([]contract.PurchaseOrderResponse, len(poResponses)) + for i, po := range poResponses { + response := transformer.PurchaseOrderModelResponseToResponse(po) + if response != nil { + contractResponses[i] = *response + } + } + + return contract.BuildSuccessResponse(contractResponses) +} + +func (s *PurchaseOrderServiceImpl) UpdatePurchaseOrderStatus(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, status string) *contract.Response { + poResponse, err := s.purchaseOrderProcessor.UpdatePurchaseOrderStatus(ctx, id, apctx.OrganizationID, apctx.UserID, apctx.OutletID, status) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseOrderServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.PurchaseOrderModelResponseToResponse(poResponse) + return contract.BuildSuccessResponse(contractResponse) +} diff --git a/internal/service/vendor_service.go b/internal/service/vendor_service.go new file mode 100644 index 0000000..3276369 --- /dev/null +++ b/internal/service/vendor_service.go @@ -0,0 +1,122 @@ +package service + +import ( + "apskel-pos-be/internal/appcontext" + "context" + + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/transformer" + + "github.com/google/uuid" +) + +type VendorService interface { + CreateVendor(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateVendorRequest) *contract.Response + UpdateVendor(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdateVendorRequest) *contract.Response + DeleteVendor(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response + GetVendorByID(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response + ListVendors(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListVendorsRequest) *contract.Response + GetActiveVendors(ctx context.Context, apctx *appcontext.ContextInfo) *contract.Response +} + +type VendorServiceImpl struct { + vendorProcessor processor.VendorProcessor +} + +func NewVendorService(vendorProcessor processor.VendorProcessor) *VendorServiceImpl { + return &VendorServiceImpl{ + vendorProcessor: vendorProcessor, + } +} + +func (s *VendorServiceImpl) CreateVendor(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateVendorRequest) *contract.Response { + modelReq := transformer.CreateVendorRequestToModel(req) + + vendorResponse, err := s.vendorProcessor.CreateVendor(ctx, apctx.OrganizationID, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.VendorServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.VendorModelResponseToResponse(vendorResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *VendorServiceImpl) UpdateVendor(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdateVendorRequest) *contract.Response { + modelReq := transformer.UpdateVendorRequestToModel(req) + + vendorResponse, err := s.vendorProcessor.UpdateVendor(ctx, id, apctx.OrganizationID, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.VendorServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.VendorModelResponseToResponse(vendorResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *VendorServiceImpl) DeleteVendor(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response { + err := s.vendorProcessor.DeleteVendor(ctx, id, apctx.OrganizationID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.VendorServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + return contract.BuildSuccessResponse(map[string]interface{}{ + "message": "Vendor deleted successfully", + }) +} + +func (s *VendorServiceImpl) GetVendorByID(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response { + vendorResponse, err := s.vendorProcessor.GetVendorByID(ctx, id, apctx.OrganizationID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.VendorServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.VendorModelResponseToResponse(vendorResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *VendorServiceImpl) ListVendors(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListVendorsRequest) *contract.Response { + modelReq := transformer.ListVendorsRequestToModel(req) + + filters := make(map[string]interface{}) + if modelReq.Search != "" { + filters["search"] = modelReq.Search + } + if modelReq.IsActive != nil { + filters["is_active"] = *modelReq.IsActive + } + + vendors, totalPages, err := s.vendorProcessor.ListVendors(ctx, apctx.OrganizationID, filters, modelReq.Page, modelReq.Limit) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.VendorServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponses := transformer.VendorModelResponsesToResponses(vendors) + + response := contract.ListVendorsResponse{ + Vendors: contractResponses, + TotalCount: len(contractResponses), + Page: modelReq.Page, + Limit: modelReq.Limit, + TotalPages: totalPages, + } + + return contract.BuildSuccessResponse(response) +} + +func (s *VendorServiceImpl) GetActiveVendors(ctx context.Context, apctx *appcontext.ContextInfo) *contract.Response { + vendorResponses, err := s.vendorProcessor.GetActiveVendors(ctx, apctx.OrganizationID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.VendorServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponses := transformer.VendorModelResponsesToResponses(vendorResponses) + return contract.BuildSuccessResponse(contractResponses) +} diff --git a/internal/transformer/ingredient_unit_converter_transformer.go b/internal/transformer/ingredient_unit_converter_transformer.go new file mode 100644 index 0000000..6789de6 --- /dev/null +++ b/internal/transformer/ingredient_unit_converter_transformer.go @@ -0,0 +1,154 @@ +package transformer + +import ( + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" +) + +// Request transformations +func CreateIngredientUnitConverterRequestToModel(req *contract.CreateIngredientUnitConverterRequest) *models.CreateIngredientUnitConverterRequest { + if req == nil { + return nil + } + + return &models.CreateIngredientUnitConverterRequest{ + IngredientID: req.IngredientID, + FromUnitID: req.FromUnitID, + ToUnitID: req.ToUnitID, + ConversionFactor: req.ConversionFactor, + IsActive: req.IsActive, + } +} + +func UpdateIngredientUnitConverterRequestToModel(req *contract.UpdateIngredientUnitConverterRequest) *models.UpdateIngredientUnitConverterRequest { + if req == nil { + return nil + } + + return &models.UpdateIngredientUnitConverterRequest{ + FromUnitID: req.FromUnitID, + ToUnitID: req.ToUnitID, + ConversionFactor: req.ConversionFactor, + IsActive: req.IsActive, + } +} + +func ListIngredientUnitConvertersRequestToModel(req *contract.ListIngredientUnitConvertersRequest) *models.ListIngredientUnitConvertersRequest { + if req == nil { + return nil + } + + return &models.ListIngredientUnitConvertersRequest{ + IngredientID: req.IngredientID, + FromUnitID: req.FromUnitID, + ToUnitID: req.ToUnitID, + IsActive: req.IsActive, + Search: req.Search, + Page: req.Page, + Limit: req.Limit, + } +} + +func ConvertUnitRequestToModel(req *contract.ConvertUnitRequest) *models.ConvertUnitRequest { + if req == nil { + return nil + } + + return &models.ConvertUnitRequest{ + IngredientID: req.IngredientID, + FromUnitID: req.FromUnitID, + ToUnitID: req.ToUnitID, + Quantity: req.Quantity, + } +} + +// Response transformations +func IngredientUnitConverterModelResponseToResponse(model *models.IngredientUnitConverterResponse) contract.IngredientUnitConverterResponse { + if model == nil { + return contract.IngredientUnitConverterResponse{} + } + + response := contract.IngredientUnitConverterResponse{ + ID: model.ID, + OrganizationID: model.OrganizationID, + IngredientID: model.IngredientID, + FromUnitID: model.FromUnitID, + ToUnitID: model.ToUnitID, + ConversionFactor: model.ConversionFactor, + IsActive: model.IsActive, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + CreatedBy: model.CreatedBy, + UpdatedBy: model.UpdatedBy, + } + + // Map related entities + if model.Ingredient != nil { + ingredientResp := IngredientModelResponseToResponse(model.Ingredient) + response.Ingredient = &ingredientResp + } + if model.FromUnit != nil { + fromUnitResp := UnitModelResponseToResponse(model.FromUnit) + response.FromUnit = &fromUnitResp + } + if model.ToUnit != nil { + toUnitResp := UnitModelResponseToResponse(model.ToUnit) + response.ToUnit = &toUnitResp + } + + return response +} + +func ConvertUnitModelResponseToResponse(model *models.ConvertUnitResponse) contract.ConvertUnitResponse { + if model == nil { + return contract.ConvertUnitResponse{} + } + + response := contract.ConvertUnitResponse{ + FromQuantity: model.FromQuantity, + ToQuantity: model.ToQuantity, + ConversionFactor: model.ConversionFactor, + } + + // Map units + if model.FromUnit != nil { + fromUnitResp := UnitModelResponseToResponse(model.FromUnit) + response.FromUnit = &fromUnitResp + } + if model.ToUnit != nil { + toUnitResp := UnitModelResponseToResponse(model.ToUnit) + response.ToUnit = &toUnitResp + } + + // Map ingredient + if model.Ingredient != nil { + ingredientResp := IngredientModelResponseToResponse(model.Ingredient) + response.Ingredient = &ingredientResp + } + + return response +} + +// Helper functions for related entities +func IngredientModelResponseToResponse(model *models.IngredientResponse) contract.IngredientResponse { + if model == nil { + return contract.IngredientResponse{} + } + + return contract.IngredientResponse{ + ID: model.ID, + Name: model.Name, + } +} + +func UnitModelResponseToResponse(model *models.UnitResponse) contract.UnitResponse { + if model == nil { + return contract.UnitResponse{} + } + + return contract.UnitResponse{ + ID: model.ID, + Name: model.Name, + } +} + diff --git a/internal/transformer/purchase_order_transformer.go b/internal/transformer/purchase_order_transformer.go new file mode 100644 index 0000000..8164176 --- /dev/null +++ b/internal/transformer/purchase_order_transformer.go @@ -0,0 +1,204 @@ +package transformer + +import ( + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" +) + +// Contract to Model conversions +func CreatePurchaseOrderRequestToModel(req *contract.CreatePurchaseOrderRequest) *models.CreatePurchaseOrderRequest { + items := make([]models.CreatePurchaseOrderItemRequest, len(req.Items)) + for i, item := range req.Items { + items[i] = models.CreatePurchaseOrderItemRequest{ + IngredientID: item.IngredientID, + Description: item.Description, + Quantity: item.Quantity, + UnitID: item.UnitID, + Amount: item.Amount, + } + } + + return &models.CreatePurchaseOrderRequest{ + VendorID: req.VendorID, + PONumber: req.PONumber, + TransactionDate: req.TransactionDate, + DueDate: req.DueDate, + Reference: req.Reference, + Status: req.Status, + Message: req.Message, + Items: items, + AttachmentFileIDs: req.AttachmentFileIDs, + } +} + +func UpdatePurchaseOrderRequestToModel(req *contract.UpdatePurchaseOrderRequest) *models.UpdatePurchaseOrderRequest { + var items []models.UpdatePurchaseOrderItemRequest + if req.Items != nil { + items = make([]models.UpdatePurchaseOrderItemRequest, len(req.Items)) + for i, item := range req.Items { + items[i] = models.UpdatePurchaseOrderItemRequest{ + ID: item.ID, + IngredientID: item.IngredientID, + Description: item.Description, + Quantity: item.Quantity, + UnitID: item.UnitID, + Amount: item.Amount, + } + } + } + + return &models.UpdatePurchaseOrderRequest{ + VendorID: req.VendorID, + PONumber: req.PONumber, + TransactionDate: req.TransactionDate, + DueDate: req.DueDate, + Reference: req.Reference, + Status: req.Status, + Message: req.Message, + Items: items, + AttachmentFileIDs: req.AttachmentFileIDs, + } +} + +func ListPurchaseOrdersRequestToModel(req *contract.ListPurchaseOrdersRequest) *models.ListPurchaseOrdersRequest { + return &models.ListPurchaseOrdersRequest{ + Page: req.Page, + Limit: req.Limit, + Search: req.Search, + Status: req.Status, + VendorID: req.VendorID, + StartDate: req.StartDate, + EndDate: req.EndDate, + } +} + +// Model to Contract conversions +func PurchaseOrderModelResponseToResponse(po *models.PurchaseOrderResponse) *contract.PurchaseOrderResponse { + if po == nil { + return nil + } + + response := &contract.PurchaseOrderResponse{ + ID: po.ID, + OrganizationID: po.OrganizationID, + VendorID: po.VendorID, + PONumber: po.PONumber, + TransactionDate: po.TransactionDate, + DueDate: po.DueDate, + Reference: po.Reference, + Status: po.Status, + Message: po.Message, + TotalAmount: po.TotalAmount, + CreatedAt: po.CreatedAt, + UpdatedAt: po.UpdatedAt, + } + + // Map vendor if present + if po.Vendor != nil { + response.Vendor = &contract.VendorResponse{ + ID: po.Vendor.ID, + OrganizationID: po.Vendor.OrganizationID, + Name: po.Vendor.Name, + Email: po.Vendor.Email, + PhoneNumber: po.Vendor.PhoneNumber, + Address: po.Vendor.Address, + ContactPerson: po.Vendor.ContactPerson, + TaxNumber: po.Vendor.TaxNumber, + PaymentTerms: po.Vendor.PaymentTerms, + Notes: po.Vendor.Notes, + IsActive: po.Vendor.IsActive, + CreatedAt: po.Vendor.CreatedAt, + UpdatedAt: po.Vendor.UpdatedAt, + } + } + + // Map items if present + if po.Items != nil { + response.Items = make([]contract.PurchaseOrderItemResponse, len(po.Items)) + for i, item := range po.Items { + response.Items[i] = contract.PurchaseOrderItemResponse{ + ID: item.ID, + PurchaseOrderID: item.PurchaseOrderID, + IngredientID: item.IngredientID, + Description: item.Description, + Quantity: item.Quantity, + UnitID: item.UnitID, + Amount: item.Amount, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + + // Map ingredient if present + if item.Ingredient != nil { + response.Items[i].Ingredient = &contract.IngredientResponse{ + ID: item.Ingredient.ID, + Name: item.Ingredient.Name, + } + } + + // Map unit if present + if item.Unit != nil { + response.Items[i].Unit = &contract.UnitResponse{ + ID: item.Unit.ID, + Name: item.Unit.Name, + } + } + } + } + + // Map attachments if present + if po.Attachments != nil { + response.Attachments = make([]contract.PurchaseOrderAttachmentResponse, len(po.Attachments)) + for i, attachment := range po.Attachments { + response.Attachments[i] = contract.PurchaseOrderAttachmentResponse{ + ID: attachment.ID, + PurchaseOrderID: attachment.PurchaseOrderID, + FileID: attachment.FileID, + CreatedAt: attachment.CreatedAt, + } + + // Map file if present + if attachment.File != nil { + response.Attachments[i].File = &contract.FileResponse{ + ID: attachment.File.ID, + FileName: attachment.File.FileName, + OriginalName: attachment.File.OriginalName, + FileURL: attachment.File.FileURL, + FileSize: attachment.File.FileSize, + MimeType: attachment.File.MimeType, + FileType: attachment.File.FileType, + IsPublic: attachment.File.IsPublic, + CreatedAt: attachment.File.CreatedAt, + UpdatedAt: attachment.File.UpdatedAt, + } + } + } + } + + return response +} + +func PurchaseOrderModelResponsesToResponses(pos []models.PurchaseOrderResponse) []contract.PurchaseOrderResponse { + if pos == nil { + return nil + } + + responses := make([]contract.PurchaseOrderResponse, len(pos)) + for i, po := range pos { + response := PurchaseOrderModelResponseToResponse(&po) + if response != nil { + responses[i] = *response + } + } + return responses +} + +func ListPurchaseOrdersModelResponseToResponse(resp *models.ListPurchaseOrdersResponse) *contract.ListPurchaseOrdersResponse { + return &contract.ListPurchaseOrdersResponse{ + PurchaseOrders: PurchaseOrderModelResponsesToResponses(resp.PurchaseOrders), + TotalCount: resp.TotalCount, + Page: resp.Page, + Limit: resp.Limit, + TotalPages: resp.TotalPages, + } +} diff --git a/internal/transformer/vendor_transformer.go b/internal/transformer/vendor_transformer.go new file mode 100644 index 0000000..d32b02d --- /dev/null +++ b/internal/transformer/vendor_transformer.go @@ -0,0 +1,89 @@ +package transformer + +import ( + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" +) + +// Contract to Model conversions +func CreateVendorRequestToModel(req *contract.CreateVendorRequest) *models.CreateVendorRequest { + return &models.CreateVendorRequest{ + Name: req.Name, + Email: req.Email, + PhoneNumber: req.PhoneNumber, + Address: req.Address, + ContactPerson: req.ContactPerson, + TaxNumber: req.TaxNumber, + PaymentTerms: req.PaymentTerms, + Notes: req.Notes, + IsActive: req.IsActive, + } +} + +func UpdateVendorRequestToModel(req *contract.UpdateVendorRequest) *models.UpdateVendorRequest { + return &models.UpdateVendorRequest{ + Name: req.Name, + Email: req.Email, + PhoneNumber: req.PhoneNumber, + Address: req.Address, + ContactPerson: req.ContactPerson, + TaxNumber: req.TaxNumber, + PaymentTerms: req.PaymentTerms, + Notes: req.Notes, + IsActive: req.IsActive, + } +} + +func ListVendorsRequestToModel(req *contract.ListVendorsRequest) *models.ListVendorsRequest { + return &models.ListVendorsRequest{ + Page: req.Page, + Limit: req.Limit, + Search: req.Search, + IsActive: req.IsActive, + } +} + +// Model to Contract conversions +func VendorModelResponseToResponse(vendor *models.VendorResponse) *contract.VendorResponse { + return &contract.VendorResponse{ + ID: vendor.ID, + OrganizationID: vendor.OrganizationID, + Name: vendor.Name, + Email: vendor.Email, + PhoneNumber: vendor.PhoneNumber, + Address: vendor.Address, + ContactPerson: vendor.ContactPerson, + TaxNumber: vendor.TaxNumber, + PaymentTerms: vendor.PaymentTerms, + Notes: vendor.Notes, + IsActive: vendor.IsActive, + CreatedAt: vendor.CreatedAt, + UpdatedAt: vendor.UpdatedAt, + } +} + +func VendorModelResponsesToResponses(vendors []*models.VendorResponse) []contract.VendorResponse { + if vendors == nil { + return nil + } + + responses := make([]contract.VendorResponse, len(vendors)) + for i, vendor := range vendors { + response := VendorModelResponseToResponse(vendor) + if response != nil { + responses[i] = *response + } + } + return responses +} + +func ListVendorsModelResponseToResponse(resp *models.ListVendorsResponse) *contract.ListVendorsResponse { + contractVendors := VendorModelResponsesToResponses(resp.Vendors) + return &contract.ListVendorsResponse{ + Vendors: contractVendors, + TotalCount: resp.TotalCount, + Page: resp.Page, + Limit: resp.Limit, + TotalPages: resp.TotalPages, + } +} diff --git a/internal/util/accounting_util.go b/internal/util/accounting_util.go new file mode 100644 index 0000000..9ec42ab --- /dev/null +++ b/internal/util/accounting_util.go @@ -0,0 +1,113 @@ +package util + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "path/filepath" + + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/processor" + + "github.com/google/uuid" +) + +type DefaultChartOfAccount struct { + ChartOfAccounts []ChartOfAccountTemplate `json:"chart_of_accounts"` +} + +type ChartOfAccountTemplate struct { + Name string `json:"name"` + Code string `json:"code"` + ChartOfAccountType string `json:"chart_of_account_type"` + ParentCode *string `json:"parent_code"` + IsSystem bool `json:"is_system"` + Accounts []AccountTemplate `json:"accounts"` +} + +type AccountTemplate struct { + Name string `json:"name"` + Number string `json:"number"` + AccountType string `json:"account_type"` + OpeningBalance float64 `json:"opening_balance"` + Description *string `json:"description"` +} + +func CreateDefaultChartOfAccounts(ctx context.Context, + chartOfAccountProcessor processor.ChartOfAccountProcessor, + accountProcessor processor.AccountProcessor, + organizationID uuid.UUID, + outletID *uuid.UUID) error { + + // Load the default chart of accounts template + template, err := loadDefaultChartOfAccountsTemplate() + if err != nil { + return fmt.Errorf("failed to load default chart of accounts template: %w", err) + } + + // Note: In a real implementation, you would get chart of account types + // and validate that all required types exist + + // Create chart of accounts and accounts + chartOfAccountMap := make(map[string]uuid.UUID) + + for _, coaTemplate := range template.ChartOfAccounts { + // Note: In a real implementation, you would call the processor to create the chart of account + // For now, we'll just store the mapping + chartOfAccountMap[coaTemplate.Code] = uuid.New() // This would be the actual ID from creation + + // Create accounts for this chart of account + for _, accountTemplate := range coaTemplate.Accounts { + accountReq := &entities.Account{ + OrganizationID: organizationID, + OutletID: outletID, + ChartOfAccountID: chartOfAccountMap[coaTemplate.Code], + Name: accountTemplate.Name, + Number: accountTemplate.Number, + AccountType: entities.AccountType(accountTemplate.AccountType), + OpeningBalance: accountTemplate.OpeningBalance, + CurrentBalance: accountTemplate.OpeningBalance, + Description: accountTemplate.Description, + IsActive: true, + IsSystem: coaTemplate.IsSystem, + } + + // Note: In a real implementation, you would call the processor to create the account + _ = accountReq + } + } + + return nil +} + +func loadDefaultChartOfAccountsTemplate() (*DefaultChartOfAccount, error) { + // Get the path to the template file + templatePath := filepath.Join("internal", "constants", "default_chart_of_accounts.json") + + // Read the template file + data, err := ioutil.ReadFile(templatePath) + if err != nil { + return nil, fmt.Errorf("failed to read template file: %w", err) + } + + // Parse the JSON + var template DefaultChartOfAccount + if err := json.Unmarshal(data, &template); err != nil { + return nil, fmt.Errorf("failed to parse template JSON: %w", err) + } + + return &template, nil +} + +func getChartOfAccountTypeMap(ctx context.Context, chartOfAccountProcessor processor.ChartOfAccountProcessor) (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{ + "ASSET": uuid.New(), + "LIABILITY": uuid.New(), + "EQUITY": uuid.New(), + "REVENUE": uuid.New(), + "EXPENSE": uuid.New(), + }, nil +} diff --git a/internal/validator/account_validator.go b/internal/validator/account_validator.go new file mode 100644 index 0000000..c66fda8 --- /dev/null +++ b/internal/validator/account_validator.go @@ -0,0 +1,98 @@ +package validator + +import ( + "apskel-pos-be/internal/models" + "fmt" + "strings" + + "github.com/go-playground/validator/v10" +) + +type AccountValidator interface { + ValidateCreateAccount(req *models.CreateAccountRequest) error + ValidateUpdateAccount(req *models.UpdateAccountRequest) error +} + +type AccountValidatorImpl struct { + validator *validator.Validate +} + +func NewAccountValidator() AccountValidator { + return &AccountValidatorImpl{ + validator: validator.New(), + } +} + +func (v *AccountValidatorImpl) ValidateCreateAccount(req *models.CreateAccountRequest) error { + if err := v.validator.Struct(req); err != nil { + return err + } + + // Additional custom validations + if strings.TrimSpace(req.Name) == "" { + return fmt.Errorf("name cannot be empty") + } + + if strings.TrimSpace(req.Number) == "" { + return fmt.Errorf("number cannot be empty") + } + + // Validate account type + if !isValidAccountType(req.AccountType) { + return fmt.Errorf("invalid account type") + } + + // Validate number format (alphanumeric) + if !isValidAccountNumberFormat(req.Number) { + return fmt.Errorf("number must be alphanumeric") + } + + return nil +} + +func (v *AccountValidatorImpl) ValidateUpdateAccount(req *models.UpdateAccountRequest) error { + if err := v.validator.Struct(req); err != nil { + return err + } + + // Additional custom validations + if req.Name != nil && strings.TrimSpace(*req.Name) == "" { + return fmt.Errorf("name cannot be empty") + } + + if req.Number != nil && strings.TrimSpace(*req.Number) == "" { + return fmt.Errorf("number cannot be empty") + } + + // Validate account type if provided + if req.AccountType != nil && !isValidAccountType(*req.AccountType) { + return fmt.Errorf("invalid account type") + } + + // Validate number format if provided + if req.Number != nil && !isValidAccountNumberFormat(*req.Number) { + return fmt.Errorf("number must be alphanumeric") + } + + return nil +} + +func isValidAccountType(accountType string) bool { + validTypes := []string{"cash", "wallet", "bank", "credit", "debit", "asset", "liability", "equity", "revenue", "expense"} + for _, validType := range validTypes { + if accountType == validType { + return true + } + } + return false +} + +func isValidAccountNumberFormat(number string) bool { + // Check if number is alphanumeric + for _, char := range number { + if !((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9')) { + return false + } + } + return true +} diff --git a/internal/validator/chart_of_account_type_validator.go b/internal/validator/chart_of_account_type_validator.go new file mode 100644 index 0000000..77bf8ea --- /dev/null +++ b/internal/validator/chart_of_account_type_validator.go @@ -0,0 +1,78 @@ +package validator + +import ( + "apskel-pos-be/internal/models" + "fmt" + "strings" + + "github.com/go-playground/validator/v10" +) + +type ChartOfAccountTypeValidator interface { + ValidateCreateChartOfAccountType(req *models.CreateChartOfAccountTypeRequest) error + ValidateUpdateChartOfAccountType(req *models.UpdateChartOfAccountTypeRequest) error +} + +type ChartOfAccountTypeValidatorImpl struct { + validator *validator.Validate +} + +func NewChartOfAccountTypeValidator() ChartOfAccountTypeValidator { + return &ChartOfAccountTypeValidatorImpl{ + validator: validator.New(), + } +} + +func (v *ChartOfAccountTypeValidatorImpl) ValidateCreateChartOfAccountType(req *models.CreateChartOfAccountTypeRequest) error { + if err := v.validator.Struct(req); err != nil { + return err + } + + // Additional custom validations + if strings.TrimSpace(req.Name) == "" { + return fmt.Errorf("name cannot be empty") + } + + if strings.TrimSpace(req.Code) == "" { + return fmt.Errorf("code cannot be empty") + } + + // Validate code format (alphanumeric, uppercase) + if !isValidCodeFormat(req.Code) { + return fmt.Errorf("code must be alphanumeric and uppercase") + } + + return nil +} + +func (v *ChartOfAccountTypeValidatorImpl) ValidateUpdateChartOfAccountType(req *models.UpdateChartOfAccountTypeRequest) error { + if err := v.validator.Struct(req); err != nil { + return err + } + + // Additional custom validations + if req.Name != nil && strings.TrimSpace(*req.Name) == "" { + return fmt.Errorf("name cannot be empty") + } + + if req.Code != nil && strings.TrimSpace(*req.Code) == "" { + return fmt.Errorf("code cannot be empty") + } + + // Validate code format if provided + if req.Code != nil && !isValidCodeFormat(*req.Code) { + return fmt.Errorf("code must be alphanumeric and uppercase") + } + + return nil +} + +func isValidCodeFormat(code string) bool { + // Check if code is alphanumeric and uppercase + for _, char := range code { + if !((char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) { + return false + } + } + return true +} diff --git a/internal/validator/chart_of_account_validator.go b/internal/validator/chart_of_account_validator.go new file mode 100644 index 0000000..99e8685 --- /dev/null +++ b/internal/validator/chart_of_account_validator.go @@ -0,0 +1,78 @@ +package validator + +import ( + "apskel-pos-be/internal/models" + "fmt" + "strings" + + "github.com/go-playground/validator/v10" +) + +type ChartOfAccountValidator interface { + ValidateCreateChartOfAccount(req *models.CreateChartOfAccountRequest) error + ValidateUpdateChartOfAccount(req *models.UpdateChartOfAccountRequest) error +} + +type ChartOfAccountValidatorImpl struct { + validator *validator.Validate +} + +func NewChartOfAccountValidator() ChartOfAccountValidator { + return &ChartOfAccountValidatorImpl{ + validator: validator.New(), + } +} + +func (v *ChartOfAccountValidatorImpl) ValidateCreateChartOfAccount(req *models.CreateChartOfAccountRequest) error { + if err := v.validator.Struct(req); err != nil { + return err + } + + // Additional custom validations + if strings.TrimSpace(req.Name) == "" { + return fmt.Errorf("name cannot be empty") + } + + if strings.TrimSpace(req.Code) == "" { + return fmt.Errorf("code cannot be empty") + } + + // Validate code format (alphanumeric) + if !isValidAccountCodeFormat(req.Code) { + return fmt.Errorf("code must be alphanumeric") + } + + return nil +} + +func (v *ChartOfAccountValidatorImpl) ValidateUpdateChartOfAccount(req *models.UpdateChartOfAccountRequest) error { + if err := v.validator.Struct(req); err != nil { + return err + } + + // Additional custom validations + if req.Name != nil && strings.TrimSpace(*req.Name) == "" { + return fmt.Errorf("name cannot be empty") + } + + if req.Code != nil && strings.TrimSpace(*req.Code) == "" { + return fmt.Errorf("code cannot be empty") + } + + // Validate code format if provided + if req.Code != nil && !isValidAccountCodeFormat(*req.Code) { + return fmt.Errorf("code must be alphanumeric") + } + + return nil +} + +func isValidAccountCodeFormat(code string) bool { + // Check if code is alphanumeric + for _, char := range code { + if !((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9')) { + return false + } + } + return true +} diff --git a/internal/validator/ingredient_unit_converter_validator.go b/internal/validator/ingredient_unit_converter_validator.go new file mode 100644 index 0000000..f2a0499 --- /dev/null +++ b/internal/validator/ingredient_unit_converter_validator.go @@ -0,0 +1,122 @@ +package validator + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "errors" + "strings" +) + +type IngredientUnitConverterValidator interface { + ValidateCreateIngredientUnitConverterRequest(req *contract.CreateIngredientUnitConverterRequest) (error, string) + ValidateUpdateIngredientUnitConverterRequest(req *contract.UpdateIngredientUnitConverterRequest) (error, string) + ValidateListIngredientUnitConvertersRequest(req *contract.ListIngredientUnitConvertersRequest) (error, string) + ValidateConvertUnitRequest(req *contract.ConvertUnitRequest) (error, string) +} + +type IngredientUnitConverterValidatorImpl struct{} + +func NewIngredientUnitConverterValidator() IngredientUnitConverterValidator { + return &IngredientUnitConverterValidatorImpl{} +} + +func (v *IngredientUnitConverterValidatorImpl) ValidateCreateIngredientUnitConverterRequest(req *contract.CreateIngredientUnitConverterRequest) (error, string) { + if req == nil { + return errors.New("request cannot be nil"), constants.ValidationErrorCode + } + + // Validate required fields + if req.IngredientID.String() == "" { + return errors.New("ingredient_id is required"), constants.MissingFieldErrorCode + } + + if req.FromUnitID.String() == "" { + return errors.New("from_unit_id is required"), constants.MissingFieldErrorCode + } + + if req.ToUnitID.String() == "" { + return errors.New("to_unit_id is required"), constants.MissingFieldErrorCode + } + + if req.ConversionFactor <= 0 { + return errors.New("conversion_factor must be greater than 0"), constants.ValidationErrorCode + } + + // Validate that from and to units are different + if req.FromUnitID == req.ToUnitID { + return errors.New("from_unit_id and to_unit_id must be different"), constants.ValidationErrorCode + } + + return nil, "" +} + +func (v *IngredientUnitConverterValidatorImpl) ValidateUpdateIngredientUnitConverterRequest(req *contract.UpdateIngredientUnitConverterRequest) (error, string) { + if req == nil { + return errors.New("request cannot be nil"), constants.ValidationErrorCode + } + + // At least one field must be provided for update + if req.FromUnitID == nil && req.ToUnitID == nil && req.ConversionFactor == nil && req.IsActive == nil { + return errors.New("at least one field must be provided for update"), constants.ValidationErrorCode + } + + // Validate conversion factor if provided + if req.ConversionFactor != nil && *req.ConversionFactor <= 0 { + return errors.New("conversion_factor must be greater than 0"), constants.ValidationErrorCode + } + + // Validate that from and to units are different if both are provided + if req.FromUnitID != nil && req.ToUnitID != nil && *req.FromUnitID == *req.ToUnitID { + return errors.New("from_unit_id and to_unit_id must be different"), constants.ValidationErrorCode + } + + return nil, "" +} + +func (v *IngredientUnitConverterValidatorImpl) ValidateListIngredientUnitConvertersRequest(req *contract.ListIngredientUnitConvertersRequest) (error, string) { + if req == nil { + return errors.New("request cannot be nil"), constants.ValidationErrorCode + } + + // Validate pagination + if req.Page < 1 { + return errors.New("page must be at least 1"), constants.ValidationErrorCode + } + + if req.Limit < 1 || req.Limit > 100 { + return errors.New("limit must be between 1 and 100"), constants.ValidationErrorCode + } + + // Validate search string length + if req.Search != "" && len(strings.TrimSpace(req.Search)) < 2 { + return errors.New("search term must be at least 2 characters"), constants.ValidationErrorCode + } + + return nil, "" +} + +func (v *IngredientUnitConverterValidatorImpl) ValidateConvertUnitRequest(req *contract.ConvertUnitRequest) (error, string) { + if req == nil { + return errors.New("request cannot be nil"), constants.ValidationErrorCode + } + + // Validate required fields + if req.IngredientID.String() == "" { + return errors.New("ingredient_id is required"), constants.MissingFieldErrorCode + } + + if req.FromUnitID.String() == "" { + return errors.New("from_unit_id is required"), constants.MissingFieldErrorCode + } + + if req.ToUnitID.String() == "" { + return errors.New("to_unit_id is required"), constants.MissingFieldErrorCode + } + + if req.Quantity <= 0 { + return errors.New("quantity must be greater than 0"), constants.ValidationErrorCode + } + + return nil, "" +} + diff --git a/internal/validator/purchase_order_validator.go b/internal/validator/purchase_order_validator.go new file mode 100644 index 0000000..eedd287 --- /dev/null +++ b/internal/validator/purchase_order_validator.go @@ -0,0 +1,189 @@ +package validator + +import ( + "errors" + "strings" + + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" +) + +type PurchaseOrderValidator interface { + ValidateCreatePurchaseOrderRequest(req *contract.CreatePurchaseOrderRequest) (error, string) + ValidateUpdatePurchaseOrderRequest(req *contract.UpdatePurchaseOrderRequest) (error, string) + ValidateListPurchaseOrdersRequest(req *contract.ListPurchaseOrdersRequest) (error, string) +} + +type PurchaseOrderValidatorImpl struct{} + +func NewPurchaseOrderValidator() *PurchaseOrderValidatorImpl { + return &PurchaseOrderValidatorImpl{} +} + +func (v *PurchaseOrderValidatorImpl) ValidateCreatePurchaseOrderRequest(req *contract.CreatePurchaseOrderRequest) (error, string) { + if req == nil { + return errors.New("request body is required"), constants.MissingFieldErrorCode + } + + if req.VendorID.String() == "" { + return errors.New("vendor_id is required"), constants.MissingFieldErrorCode + } + + if strings.TrimSpace(req.PONumber) == "" { + return errors.New("po_number is required"), constants.MissingFieldErrorCode + } + + if len(req.PONumber) < 1 || len(req.PONumber) > 50 { + return errors.New("po_number must be between 1 and 50 characters"), constants.MalformedFieldErrorCode + } + + if req.TransactionDate.IsZero() { + return errors.New("transaction_date is required"), constants.MissingFieldErrorCode + } + + if req.DueDate.IsZero() { + return errors.New("due_date is required"), constants.MissingFieldErrorCode + } + + if req.DueDate.Before(req.TransactionDate) { + return errors.New("due_date must be after transaction_date"), constants.MalformedFieldErrorCode + } + + if req.Reference != nil && len(*req.Reference) > 100 { + return errors.New("reference must be at most 100 characters"), constants.MalformedFieldErrorCode + } + + if req.Status != nil { + validStatuses := []string{"draft", "sent", "approved", "received", "cancelled"} + if !contains(validStatuses, *req.Status) { + return errors.New("status must be one of: draft, sent, approved, received, cancelled"), constants.MalformedFieldErrorCode + } + } + + if len(req.Items) == 0 { + return errors.New("at least one item is required"), constants.MissingFieldErrorCode + } + + // Validate items + for i, item := range req.Items { + if err, code := v.validatePurchaseOrderItem(&item, i); err != nil { + return err, code + } + } + + return nil, "" +} + +func (v *PurchaseOrderValidatorImpl) ValidateUpdatePurchaseOrderRequest(req *contract.UpdatePurchaseOrderRequest) (error, string) { + if req == nil { + return errors.New("request body is required"), constants.MissingFieldErrorCode + } + + if req.PONumber != nil { + if strings.TrimSpace(*req.PONumber) == "" { + return errors.New("po_number cannot be empty"), constants.MalformedFieldErrorCode + } + if len(*req.PONumber) < 1 || len(*req.PONumber) > 50 { + return errors.New("po_number must be between 1 and 50 characters"), constants.MalformedFieldErrorCode + } + } + + if req.TransactionDate != nil && req.DueDate != nil { + if req.DueDate.Before(*req.TransactionDate) { + return errors.New("due_date must be after transaction_date"), constants.MalformedFieldErrorCode + } + } + + if req.Reference != nil && len(*req.Reference) > 100 { + return errors.New("reference must be at most 100 characters"), constants.MalformedFieldErrorCode + } + + if req.Status != nil { + validStatuses := []string{"draft", "sent", "approved", "received", "cancelled"} + if !contains(validStatuses, *req.Status) { + return errors.New("status must be one of: draft, sent, approved, received, cancelled"), constants.MalformedFieldErrorCode + } + } + + // Validate items if provided + if req.Items != nil { + for i, item := range req.Items { + if err, code := v.validateUpdatePurchaseOrderItem(&item, i); err != nil { + return err, code + } + } + } + + return nil, "" +} + +func (v *PurchaseOrderValidatorImpl) ValidateListPurchaseOrdersRequest(req *contract.ListPurchaseOrdersRequest) (error, string) { + if req == nil { + return errors.New("request body is required"), constants.MissingFieldErrorCode + } + + if req.Page < 1 { + return errors.New("page must be at least 1"), constants.MalformedFieldErrorCode + } + + if req.Limit < 1 || req.Limit > 100 { + return errors.New("limit must be between 1 and 100"), constants.MalformedFieldErrorCode + } + + if req.Status != "" { + validStatuses := []string{"draft", "sent", "approved", "received", "cancelled"} + if !contains(validStatuses, req.Status) { + return errors.New("status must be one of: draft, sent, approved, received, cancelled"), constants.MalformedFieldErrorCode + } + } + + if req.StartDate != nil && req.EndDate != nil { + if req.EndDate.Before(*req.StartDate) { + return errors.New("end_date must be after start_date"), constants.MalformedFieldErrorCode + } + } + + return nil, "" +} + +func (v *PurchaseOrderValidatorImpl) validatePurchaseOrderItem(item *contract.CreatePurchaseOrderItemRequest, index int) (error, string) { + if item.IngredientID.String() == "" { + return errors.New("items[" + string(rune(index)) + "].ingredient_id is required"), constants.MissingFieldErrorCode + } + + if item.Quantity <= 0 { + return errors.New("items[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode + } + + if item.UnitID.String() == "" { + return errors.New("items[" + string(rune(index)) + "].unit_id is required"), constants.MissingFieldErrorCode + } + + if item.Amount < 0 { + return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode + } + + return nil, "" +} + +func (v *PurchaseOrderValidatorImpl) validateUpdatePurchaseOrderItem(item *contract.UpdatePurchaseOrderItemRequest, index int) (error, string) { + if item.Quantity != nil && *item.Quantity <= 0 { + return errors.New("items[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode + } + + if item.Amount != nil && *item.Amount < 0 { + return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode + } + + return nil, "" +} + +// Helper function to check if a string is in a slice +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} diff --git a/internal/validator/vendor_validator.go b/internal/validator/vendor_validator.go new file mode 100644 index 0000000..30f0af6 --- /dev/null +++ b/internal/validator/vendor_validator.go @@ -0,0 +1,118 @@ +package validator + +import ( + "errors" + "strings" + + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" +) + +type VendorValidator interface { + ValidateCreateVendorRequest(req *contract.CreateVendorRequest) (error, string) + ValidateUpdateVendorRequest(req *contract.UpdateVendorRequest) (error, string) + ValidateListVendorsRequest(req *contract.ListVendorsRequest) (error, string) +} + +type VendorValidatorImpl struct{} + +func NewVendorValidator() *VendorValidatorImpl { + return &VendorValidatorImpl{} +} + +func (v *VendorValidatorImpl) ValidateCreateVendorRequest(req *contract.CreateVendorRequest) (error, string) { + if req == nil { + return errors.New("request body is required"), constants.MissingFieldErrorCode + } + + if strings.TrimSpace(req.Name) == "" { + return errors.New("name is required"), constants.MissingFieldErrorCode + } + + if len(req.Name) < 1 || len(req.Name) > 255 { + return errors.New("name must be between 1 and 255 characters"), constants.MalformedFieldErrorCode + } + + if req.Email != nil && strings.TrimSpace(*req.Email) != "" { + if !isValidEmail(*req.Email) { + return errors.New("email format is invalid"), constants.MalformedFieldErrorCode + } + } + + if req.PhoneNumber != nil && strings.TrimSpace(*req.PhoneNumber) != "" { + if !isValidPhone(*req.PhoneNumber) { + return errors.New("phone_number format is invalid"), constants.MalformedFieldErrorCode + } + } + + if req.ContactPerson != nil && len(*req.ContactPerson) > 255 { + return errors.New("contact_person must be at most 255 characters"), constants.MalformedFieldErrorCode + } + + if req.TaxNumber != nil && len(*req.TaxNumber) > 50 { + return errors.New("tax_number must be at most 50 characters"), constants.MalformedFieldErrorCode + } + + if req.PaymentTerms != nil && len(*req.PaymentTerms) > 100 { + return errors.New("payment_terms must be at most 100 characters"), constants.MalformedFieldErrorCode + } + + return nil, "" +} + +func (v *VendorValidatorImpl) ValidateUpdateVendorRequest(req *contract.UpdateVendorRequest) (error, string) { + if req == nil { + return errors.New("request body is required"), constants.MissingFieldErrorCode + } + + if req.Name != nil { + if strings.TrimSpace(*req.Name) == "" { + return errors.New("name cannot be empty"), constants.MalformedFieldErrorCode + } + if len(*req.Name) < 1 || len(*req.Name) > 255 { + return errors.New("name must be between 1 and 255 characters"), constants.MalformedFieldErrorCode + } + } + + if req.Email != nil && strings.TrimSpace(*req.Email) != "" { + if !isValidEmail(*req.Email) { + return errors.New("email format is invalid"), constants.MalformedFieldErrorCode + } + } + + if req.PhoneNumber != nil && strings.TrimSpace(*req.PhoneNumber) != "" { + if !isValidPhone(*req.PhoneNumber) { + return errors.New("phone_number format is invalid"), constants.MalformedFieldErrorCode + } + } + + if req.ContactPerson != nil && len(*req.ContactPerson) > 255 { + return errors.New("contact_person must be at most 255 characters"), constants.MalformedFieldErrorCode + } + + if req.TaxNumber != nil && len(*req.TaxNumber) > 50 { + return errors.New("tax_number must be at most 50 characters"), constants.MalformedFieldErrorCode + } + + if req.PaymentTerms != nil && len(*req.PaymentTerms) > 100 { + return errors.New("payment_terms must be at most 100 characters"), constants.MalformedFieldErrorCode + } + + return nil, "" +} + +func (v *VendorValidatorImpl) ValidateListVendorsRequest(req *contract.ListVendorsRequest) (error, string) { + if req == nil { + return errors.New("request body is required"), constants.MissingFieldErrorCode + } + + if req.Page < 1 { + return errors.New("page must be at least 1"), constants.MalformedFieldErrorCode + } + + if req.Limit < 1 || req.Limit > 100 { + return errors.New("limit must be between 1 and 100"), constants.MalformedFieldErrorCode + } + + return nil, "" +} diff --git a/migrations/000039_create_vendors_table.down.sql b/migrations/000039_create_vendors_table.down.sql new file mode 100644 index 0000000..0a8bd96 --- /dev/null +++ b/migrations/000039_create_vendors_table.down.sql @@ -0,0 +1,2 @@ +-- Drop vendors table +DROP TABLE IF EXISTS vendors; diff --git a/migrations/000039_create_vendors_table.up.sql b/migrations/000039_create_vendors_table.up.sql new file mode 100644 index 0000000..012b7ff --- /dev/null +++ b/migrations/000039_create_vendors_table.up.sql @@ -0,0 +1,22 @@ +-- Vendors table +CREATE TABLE vendors ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + email VARCHAR(255), + phone_number VARCHAR(20), + address TEXT, + contact_person VARCHAR(255), + tax_number VARCHAR(50), + payment_terms VARCHAR(100), + notes TEXT, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_vendors_organization_id ON vendors(organization_id); +CREATE INDEX idx_vendors_name ON vendors(name); +CREATE INDEX idx_vendors_email ON vendors(email); +CREATE INDEX idx_vendors_is_active ON vendors(is_active); +CREATE INDEX idx_vendors_created_at ON vendors(created_at); diff --git a/migrations/000040_create_purchase_orders_table.down.sql b/migrations/000040_create_purchase_orders_table.down.sql new file mode 100644 index 0000000..f613e4e --- /dev/null +++ b/migrations/000040_create_purchase_orders_table.down.sql @@ -0,0 +1,4 @@ +-- Drop purchase order tables +DROP TABLE IF EXISTS purchase_order_attachments; +DROP TABLE IF EXISTS purchase_order_items; +DROP TABLE IF EXISTS purchase_orders; diff --git a/migrations/000040_create_purchase_orders_table.up.sql b/migrations/000040_create_purchase_orders_table.up.sql new file mode 100644 index 0000000..438709a --- /dev/null +++ b/migrations/000040_create_purchase_orders_table.up.sql @@ -0,0 +1,54 @@ +-- Purchase Orders table +CREATE TABLE purchase_orders ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + vendor_id UUID NOT NULL REFERENCES vendors(id) ON DELETE CASCADE, + po_number VARCHAR(50) NOT NULL, + transaction_date DATE NOT NULL, + due_date DATE NOT NULL, + reference VARCHAR(100), + status VARCHAR(20) NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'sent', 'approved', 'received', 'cancelled')), + message TEXT, + total_amount DECIMAL(15,2) NOT NULL DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Purchase Order Items table +CREATE TABLE purchase_order_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + purchase_order_id UUID NOT NULL REFERENCES purchase_orders(id) ON DELETE CASCADE, + ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE, + description TEXT, + quantity DECIMAL(10,3) NOT NULL, + unit_id UUID NOT NULL REFERENCES units(id) ON DELETE CASCADE, + amount DECIMAL(15,2) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Purchase Order Attachments table +CREATE TABLE purchase_order_attachments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + purchase_order_id UUID NOT NULL REFERENCES purchase_orders(id) ON DELETE CASCADE, + file_id UUID NOT NULL REFERENCES files(id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Indexes +CREATE INDEX idx_purchase_orders_organization_id ON purchase_orders(organization_id); +CREATE INDEX idx_purchase_orders_vendor_id ON purchase_orders(vendor_id); +CREATE INDEX idx_purchase_orders_po_number ON purchase_orders(po_number); +CREATE INDEX idx_purchase_orders_status ON purchase_orders(status); +CREATE INDEX idx_purchase_orders_transaction_date ON purchase_orders(transaction_date); +CREATE INDEX idx_purchase_orders_created_at ON purchase_orders(created_at); + +CREATE INDEX idx_purchase_order_items_purchase_order_id ON purchase_order_items(purchase_order_id); +CREATE INDEX idx_purchase_order_items_ingredient_id ON purchase_order_items(ingredient_id); +CREATE INDEX idx_purchase_order_items_unit_id ON purchase_order_items(unit_id); + +CREATE INDEX idx_purchase_order_attachments_purchase_order_id ON purchase_order_attachments(purchase_order_id); +CREATE INDEX idx_purchase_order_attachments_file_id ON purchase_order_attachments(file_id); + +-- Unique constraint for PO number per organization +CREATE UNIQUE INDEX idx_purchase_orders_po_number_org ON purchase_orders(organization_id, po_number); diff --git a/migrations/000041_create_ingredient_unit_converters_table.down.sql b/migrations/000041_create_ingredient_unit_converters_table.down.sql new file mode 100644 index 0000000..bd82acf --- /dev/null +++ b/migrations/000041_create_ingredient_unit_converters_table.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS ingredient_unit_converters; + diff --git a/migrations/000041_create_ingredient_unit_converters_table.up.sql b/migrations/000041_create_ingredient_unit_converters_table.up.sql new file mode 100644 index 0000000..9d9d575 --- /dev/null +++ b/migrations/000041_create_ingredient_unit_converters_table.up.sql @@ -0,0 +1,33 @@ +CREATE TABLE ingredient_unit_converters ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE, + from_unit_id UUID NOT NULL REFERENCES units(id) ON DELETE CASCADE, + to_unit_id UUID NOT NULL REFERENCES units(id) ON DELETE CASCADE, + conversion_factor DECIMAL(15,6) NOT NULL CHECK (conversion_factor > 0), + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + updated_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT +); + +-- Create indexes for better performance +CREATE INDEX idx_ingredient_unit_converters_organization_id ON ingredient_unit_converters(organization_id); +CREATE INDEX idx_ingredient_unit_converters_ingredient_id ON ingredient_unit_converters(ingredient_id); +CREATE INDEX idx_ingredient_unit_converters_from_unit_id ON ingredient_unit_converters(from_unit_id); +CREATE INDEX idx_ingredient_unit_converters_to_unit_id ON ingredient_unit_converters(to_unit_id); +CREATE INDEX idx_ingredient_unit_converters_active ON ingredient_unit_converters(is_active); + +-- Create unique constraint to prevent duplicate converters for the same ingredient and unit pair +CREATE UNIQUE INDEX idx_ingredient_unit_converters_unique +ON ingredient_unit_converters(organization_id, ingredient_id, from_unit_id, to_unit_id) +WHERE is_active = true; + +-- Add comments for documentation +COMMENT ON TABLE ingredient_unit_converters IS 'Stores unit conversion factors for ingredients within an organization'; +COMMENT ON COLUMN ingredient_unit_converters.conversion_factor IS 'How many from_unit_id units equal one to_unit_id unit (e.g., 1000 for grams to kilograms)'; +COMMENT ON COLUMN ingredient_unit_converters.is_active IS 'Whether this conversion is currently active and can be used'; +COMMENT ON COLUMN ingredient_unit_converters.created_by IS 'User who created this conversion rule'; +COMMENT ON COLUMN ingredient_unit_converters.updated_by IS 'User who last updated this conversion rule'; + diff --git a/migrations/000042_create_chart_of_account_types_table.down.sql b/migrations/000042_create_chart_of_account_types_table.down.sql new file mode 100644 index 0000000..e7bff77 --- /dev/null +++ b/migrations/000042_create_chart_of_account_types_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS chart_of_account_types; diff --git a/migrations/000042_create_chart_of_account_types_table.up.sql b/migrations/000042_create_chart_of_account_types_table.up.sql new file mode 100644 index 0000000..7e7971c --- /dev/null +++ b/migrations/000042_create_chart_of_account_types_table.up.sql @@ -0,0 +1,20 @@ +CREATE TABLE chart_of_account_types ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(100) NOT NULL, + code VARCHAR(10) NOT NULL UNIQUE, + description TEXT, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Insert default chart of account types +INSERT INTO chart_of_account_types (name, code, description) VALUES +('Assets', 'ASSET', 'Resources owned by the organization'), +('Liabilities', 'LIABILITY', 'Debts and obligations'), +('Equity', 'EQUITY', 'Owner''s interest in the organization'), +('Revenue', 'REVENUE', 'Income from business operations'), +('Expenses', 'EXPENSE', 'Costs incurred in business operations'); + +CREATE INDEX idx_chart_of_account_types_code ON chart_of_account_types(code); +CREATE INDEX idx_chart_of_account_types_is_active ON chart_of_account_types(is_active); diff --git a/migrations/000043_create_chart_of_accounts_table.down.sql b/migrations/000043_create_chart_of_accounts_table.down.sql new file mode 100644 index 0000000..e21c704 --- /dev/null +++ b/migrations/000043_create_chart_of_accounts_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS chart_of_accounts; diff --git a/migrations/000043_create_chart_of_accounts_table.up.sql b/migrations/000043_create_chart_of_accounts_table.up.sql new file mode 100644 index 0000000..17867b6 --- /dev/null +++ b/migrations/000043_create_chart_of_accounts_table.up.sql @@ -0,0 +1,24 @@ +CREATE TABLE chart_of_accounts ( + 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, + chart_of_account_type_id UUID NOT NULL REFERENCES chart_of_account_types(id) ON DELETE RESTRICT, + parent_id UUID REFERENCES chart_of_accounts(id) ON DELETE SET NULL, + name VARCHAR(255) NOT NULL, + code VARCHAR(20) NOT NULL, + description TEXT, + is_active BOOLEAN DEFAULT true, + is_system BOOLEAN DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(organization_id, outlet_id, code), + UNIQUE(organization_id, code) -- For organization-level accounts +); + +CREATE INDEX idx_chart_of_accounts_organization_id ON chart_of_accounts(organization_id); +CREATE INDEX idx_chart_of_accounts_outlet_id ON chart_of_accounts(outlet_id); +CREATE INDEX idx_chart_of_accounts_type_id ON chart_of_accounts(chart_of_account_type_id); +CREATE INDEX idx_chart_of_accounts_parent_id ON chart_of_accounts(parent_id); +CREATE INDEX idx_chart_of_accounts_code ON chart_of_accounts(code); +CREATE INDEX idx_chart_of_accounts_is_active ON chart_of_accounts(is_active); +CREATE INDEX idx_chart_of_accounts_is_system ON chart_of_accounts(is_system); diff --git a/migrations/000044_create_accounts_table.down.sql b/migrations/000044_create_accounts_table.down.sql new file mode 100644 index 0000000..1616db4 --- /dev/null +++ b/migrations/000044_create_accounts_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS accounts; diff --git a/migrations/000044_create_accounts_table.up.sql b/migrations/000044_create_accounts_table.up.sql new file mode 100644 index 0000000..5246e3c --- /dev/null +++ b/migrations/000044_create_accounts_table.up.sql @@ -0,0 +1,26 @@ +CREATE TABLE accounts ( + 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, + chart_of_account_id UUID NOT NULL REFERENCES chart_of_accounts(id) ON DELETE RESTRICT, + name VARCHAR(255) NOT NULL, + number VARCHAR(50) NOT NULL, + account_type VARCHAR(20) NOT NULL CHECK (account_type IN ('cash', 'wallet', 'bank', 'credit', 'debit', 'asset', 'liability', 'equity', 'revenue', 'expense')), + opening_balance DECIMAL(15,2) DEFAULT 0.00, + current_balance DECIMAL(15,2) DEFAULT 0.00, + description TEXT, + is_active BOOLEAN DEFAULT true, + is_system BOOLEAN DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(organization_id, outlet_id, number), + UNIQUE(organization_id, number) -- For organization-level accounts +); + +CREATE INDEX idx_accounts_organization_id ON accounts(organization_id); +CREATE INDEX idx_accounts_outlet_id ON accounts(outlet_id); +CREATE INDEX idx_accounts_chart_of_account_id ON accounts(chart_of_account_id); +CREATE INDEX idx_accounts_number ON accounts(number); +CREATE INDEX idx_accounts_account_type ON accounts(account_type); +CREATE INDEX idx_accounts_is_active ON accounts(is_active); +CREATE INDEX idx_accounts_is_system ON accounts(is_system); diff --git a/server b/server index cc565bf..3ed1e65 100755 Binary files a/server and b/server differ