Add coa purchase and vendors

This commit is contained in:
Aditya Siregar 2025-09-12 01:12:11 +07:00
parent c107733add
commit efe09c21e4
86 changed files with 8517 additions and 212 deletions

View File

@ -80,6 +80,18 @@ func (a *App) Initialize(cfg *config.Config) error {
services.unitService, services.unitService,
services.ingredientService, services.ingredientService,
services.productRecipeService, 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 return nil
@ -146,6 +158,12 @@ type repositories struct {
unitRepo *repository.UnitRepository unitRepo *repository.UnitRepository
ingredientRepo *repository.IngredientRepository ingredientRepo *repository.IngredientRepository
productRecipeRepo *repository.ProductRecipeRepository productRecipeRepo *repository.ProductRecipeRepository
vendorRepo *repository.VendorRepositoryImpl
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl
chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
accountRepo *repository.AccountRepositoryImpl
txManager *repository.TxManager txManager *repository.TxManager
} }
@ -172,6 +190,12 @@ func (a *App) initRepositories() *repositories {
unitRepo: repository.NewUnitRepository(a.db), unitRepo: repository.NewUnitRepository(a.db),
ingredientRepo: repository.NewIngredientRepository(a.db), ingredientRepo: repository.NewIngredientRepository(a.db),
productRecipeRepo: repository.NewProductRecipeRepository(a.db), productRecipeRepo: repository.NewProductRecipeRepository(a.db),
vendorRepo: repository.NewVendorRepositoryImpl(a.db),
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), txManager: repository.NewTxManager(a.db),
} }
} }
@ -194,6 +218,12 @@ type processors struct {
unitProcessor *processor.UnitProcessorImpl unitProcessor *processor.UnitProcessorImpl
ingredientProcessor *processor.IngredientProcessorImpl ingredientProcessor *processor.IngredientProcessorImpl
productRecipeProcessor *processor.ProductRecipeProcessorImpl productRecipeProcessor *processor.ProductRecipeProcessorImpl
vendorProcessor *processor.VendorProcessorImpl
purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl
unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl
chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
accountProcessor *processor.AccountProcessorImpl
fileClient processor.FileClient fileClient processor.FileClient
inventoryMovementService service.InventoryMovementService inventoryMovementService service.InventoryMovementService
} }
@ -220,6 +250,12 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
unitProcessor: processor.NewUnitProcessor(repos.unitRepo), unitProcessor: processor.NewUnitProcessor(repos.unitRepo),
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo), ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo),
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo), productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
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, fileClient: fileClient,
inventoryMovementService: inventoryMovementService, inventoryMovementService: inventoryMovementService,
} }
@ -245,6 +281,12 @@ type services struct {
unitService *service.UnitServiceImpl unitService *service.UnitServiceImpl
ingredientService *service.IngredientServiceImpl ingredientService *service.IngredientServiceImpl
productRecipeService *service.ProductRecipeServiceImpl 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 { func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
@ -268,6 +310,12 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
unitService := service.NewUnitService(processors.unitProcessor) unitService := service.NewUnitService(processors.unitProcessor)
ingredientService := service.NewIngredientService(processors.ingredientProcessor) ingredientService := service.NewIngredientService(processors.ingredientProcessor)
productRecipeService := service.NewProductRecipeService(processors.productRecipeProcessor) 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{ return &services{
userService: service.NewUserService(processors.userProcessor), userService: service.NewUserService(processors.userProcessor),
@ -289,6 +337,12 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
unitService: unitService, unitService: unitService,
ingredientService: ingredientService, ingredientService: ingredientService,
productRecipeService: productRecipeService, productRecipeService: productRecipeService,
vendorService: vendorService,
purchaseOrderService: purchaseOrderService,
unitConverterService: unitConverterService,
chartOfAccountTypeService: chartOfAccountTypeService,
chartOfAccountService: chartOfAccountService,
accountService: accountService,
} }
} }
@ -315,6 +369,12 @@ type validators struct {
fileValidator validator.FileValidator fileValidator validator.FileValidator
customerValidator validator.CustomerValidator customerValidator validator.CustomerValidator
tableValidator *validator.TableValidator 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 { func (a *App) initValidators() *validators {
@ -331,5 +391,11 @@ func (a *App) initValidators() *validators {
fileValidator: validator.NewFileValidatorImpl(), fileValidator: validator.NewFileValidatorImpl(),
customerValidator: validator.NewCustomerValidator(), customerValidator: validator.NewCustomerValidator(),
tableValidator: validator.NewTableValidator(), 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),
} }
} }

View File

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

View File

@ -38,6 +38,9 @@ const (
PaymentMethodValidatorEntity = "payment_method_validator" PaymentMethodValidatorEntity = "payment_method_validator"
PaymentMethodHandlerEntity = "payment_method_handler" PaymentMethodHandlerEntity = "payment_method_handler"
OutletServiceEntity = "outlet_service" OutletServiceEntity = "outlet_service"
VendorServiceEntity = "vendor_service"
PurchaseOrderServiceEntity = "purchase_order_service"
IngredientUnitConverterServiceEntity = "ingredient_unit_converter_service"
TableEntity = "table" TableEntity = "table"
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,11 @@ func GetAllEntities() []interface{} {
&Payment{}, &Payment{},
&Customer{}, &Customer{},
&Table{}, &Table{},
&Vendor{},
&PurchaseOrder{},
&PurchaseOrderItem{},
&PurchaseOrderAttachment{},
&IngredientUnitConverter{},
// Analytics entities are not database tables, they are query results // Analytics entities are not database tables, they are query results
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -74,3 +74,26 @@ func MapIngredientModelsToEntities(models []*models.Ingredient) []*entities.Ingr
return entities 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),
}
}

View File

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

View File

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

View File

@ -66,3 +66,22 @@ func MapUnitModelsToEntities(models []*models.Unit) []*entities.Unit {
return entities 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,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,5 +26,10 @@ func GetAllModelNames() []string {
"PaymentMethod", "PaymentMethod",
"Payment", "Payment",
"Customer", "Customer",
"Vendor",
"PurchaseOrder",
"PurchaseOrderItem",
"PurchaseOrderAttachment",
"IngredientUnitConverter",
} }
} }

View File

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

78
internal/models/vendor.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,6 +33,12 @@ type Router struct {
unitHandler *handler.UnitHandler unitHandler *handler.UnitHandler
ingredientHandler *handler.IngredientHandler ingredientHandler *handler.IngredientHandler
productRecipeHandler *handler.ProductRecipeHandler productRecipeHandler *handler.ProductRecipeHandler
vendorHandler *handler.VendorHandler
purchaseOrderHandler *handler.PurchaseOrderHandler
unitConverterHandler *handler.IngredientUnitConverterHandler
chartOfAccountTypeHandler *handler.ChartOfAccountTypeHandler
chartOfAccountHandler *handler.ChartOfAccountHandler
accountHandler *handler.AccountHandler
authMiddleware *middleware.AuthMiddleware authMiddleware *middleware.AuthMiddleware
} }
@ -69,7 +75,19 @@ func NewRouter(cfg *config.Config,
tableValidator *validator.TableValidator, tableValidator *validator.TableValidator,
unitService handler.UnitService, unitService handler.UnitService,
ingredientService handler.IngredientService, 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{ return &Router{
config: cfg, config: cfg,
@ -92,6 +110,12 @@ func NewRouter(cfg *config.Config,
unitHandler: handler.NewUnitHandler(unitService), unitHandler: handler.NewUnitHandler(unitService),
ingredientHandler: handler.NewIngredientHandler(ingredientService), ingredientHandler: handler.NewIngredientHandler(ingredientService),
productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService), productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService),
vendorHandler: handler.NewVendorHandler(vendorService, vendorValidator),
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, authMiddleware: authMiddleware,
} }
} }
@ -297,6 +321,42 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
ingredients.DELETE("/:id", r.ingredientHandler.Delete) 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 := protected.Group("/product-recipes")
productRecipes.Use(r.authMiddleware.RequireAdminOrManager()) productRecipes.Use(r.authMiddleware.RequireAdminOrManager())
{ {
@ -309,6 +369,43 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
productRecipes.GET("/ingredient/:ingredient_id", r.productRecipeHandler.GetByIngredientID) 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 := protected.Group("/outlets")
outlets.Use(r.authMiddleware.RequireAdminOrManager()) outlets.Use(r.authMiddleware.RequireAdminOrManager())
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
-- Drop vendors table
DROP TABLE IF EXISTS vendors;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
DROP TABLE IF EXISTS ingredient_unit_converters;

View File

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

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS chart_of_account_types;

View File

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

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS chart_of_accounts;

View File

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

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS accounts;

View File

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

BIN
server

Binary file not shown.