Compare commits
1 Commits
main
...
enaklo-sel
| Author | SHA1 | Date | |
|---|---|---|---|
| 15805a4853 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,5 +7,4 @@ config/env/*
|
||||
|
||||
vendor
|
||||
|
||||
# Firebase service account credentials
|
||||
infra/firebase-service-account.json
|
||||
*firebase-adminsdk*.json
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1,5 +1,5 @@
|
||||
# 1) Build stage
|
||||
FROM golang:1.24-alpine AS build
|
||||
FROM golang:1.21-alpine AS build
|
||||
RUN apk --no-cache add ca-certificates tzdata git curl
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
6
Makefile
6
Makefile
@ -83,12 +83,6 @@ migration-up:
|
||||
migration-down:
|
||||
@migrate -database $(DB_URL) -path ./migrations down 1
|
||||
|
||||
# Force migration to specific version
|
||||
|
||||
.SILENT: migration-force
|
||||
migration-force:
|
||||
@migrate -database $(DB_URL) -path ./migrations force $(version)
|
||||
|
||||
.SILENT: seeder-create
|
||||
seeder-create:
|
||||
@migrate create -ext sql -dir ./seeders -seq $(name)
|
||||
|
||||
@ -31,7 +31,7 @@ type Config struct {
|
||||
Log Log `mapstructure:"log"`
|
||||
S3Config S3Config `mapstructure:"s3"`
|
||||
Fonnte Fonnte `mapstructure:"fonnte"`
|
||||
FCM FCM `mapstructure:"fcm"`
|
||||
Firebase Firebase `mapstructure:"firebase"`
|
||||
}
|
||||
|
||||
var (
|
||||
@ -97,6 +97,6 @@ func (c *Config) GetFonnte() *Fonnte {
|
||||
return &c.Fonnte
|
||||
}
|
||||
|
||||
func (c *Config) GetFCM() *FCM {
|
||||
return &c.FCM
|
||||
func (c *Config) GetFirebase() *Firebase {
|
||||
return &c.Firebase
|
||||
}
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
package config
|
||||
|
||||
type FCM struct {
|
||||
CredentialsFile string `mapstructure:"credentials_file"`
|
||||
ProjectID string `mapstructure:"project_id"`
|
||||
}
|
||||
|
||||
func (f *FCM) GetCredentialsFile() string {
|
||||
return f.CredentialsFile
|
||||
}
|
||||
|
||||
func (f *FCM) GetProjectID() string {
|
||||
return f.ProjectID
|
||||
}
|
||||
9
config/firebase.go
Normal file
9
config/firebase.go
Normal file
@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
type Firebase struct {
|
||||
CredentialsFile string `mapstructure:"credentials_file"`
|
||||
}
|
||||
|
||||
func (f *Firebase) GetCredentialsFile() string {
|
||||
return f.CredentialsFile
|
||||
}
|
||||
@ -1,8 +1,7 @@
|
||||
package config
|
||||
|
||||
type Server struct {
|
||||
Port string `mapstructure:"port"`
|
||||
BaseUrl string `mapstructure:"common-url"`
|
||||
LocalUrl string `mapstructure:"local-url"`
|
||||
SelfOrderUrl string `mapstructure:"self-order-url"`
|
||||
Port string `mapstructure:"port"`
|
||||
BaseUrl string `mapstructure:"common-url"`
|
||||
LocalUrl string `mapstructure:"local-url"`
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
server:
|
||||
base-url:
|
||||
local-url:
|
||||
self-order-url: http://localhost:5173
|
||||
port: 4000
|
||||
|
||||
jwt:
|
||||
@ -29,7 +28,7 @@ postgresql:
|
||||
debug: false
|
||||
|
||||
redis:
|
||||
host: 194.233.78.1
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
password: "CmICdmnX1EZPhVBYzQPEGw==U"
|
||||
db: 0
|
||||
@ -56,6 +55,5 @@ fonnte:
|
||||
token: "bADQrf9NTXfLZQCK2wGg"
|
||||
timeout: 30
|
||||
|
||||
fcm:
|
||||
credentials_file: "infra/firebase-service-account.json"
|
||||
project_id: "apskel-pos-v2"
|
||||
firebase:
|
||||
credentials_file: "apskel-pos-v2-firebase-adminsdk-fbsvc-ae00499526.json"
|
||||
@ -25,12 +25,11 @@ import (
|
||||
)
|
||||
|
||||
type App struct {
|
||||
server *http.Server
|
||||
db *gorm.DB
|
||||
redisClient *redis.Client
|
||||
router *router.Router
|
||||
shutdown chan os.Signal
|
||||
omsetScheduler *service.OmsetMilestoneScheduler
|
||||
server *http.Server
|
||||
db *gorm.DB
|
||||
redisClient *redis.Client
|
||||
router *router.Router
|
||||
shutdown chan os.Signal
|
||||
}
|
||||
|
||||
func NewApp(db *gorm.DB, redisClient *redis.Client) *App {
|
||||
@ -44,15 +43,6 @@ func NewApp(db *gorm.DB, redisClient *redis.Client) *App {
|
||||
func (a *App) Initialize(cfg *config.Config) error {
|
||||
repos := a.initRepositories()
|
||||
processors := a.initProcessors(cfg, repos)
|
||||
|
||||
// Initialize omset milestone scheduler
|
||||
a.omsetScheduler = service.NewOmsetMilestoneScheduler(
|
||||
repos.organizationRepo,
|
||||
repos.outletRepo,
|
||||
repos.userRepo,
|
||||
processors.notificationProcessor,
|
||||
)
|
||||
|
||||
services := a.initServices(processors, repos, cfg)
|
||||
validators := a.initValidators()
|
||||
middleware := a.initMiddleware(services, cfg)
|
||||
@ -66,7 +56,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
repos.userRepo,
|
||||
repos.sessionRepo,
|
||||
repos.orderRepo,
|
||||
services.productOutletPriceService,
|
||||
processors.fcmClient,
|
||||
)
|
||||
|
||||
a.router = router.NewRouter(
|
||||
@ -108,8 +98,6 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
validators.vendorValidator,
|
||||
services.purchaseOrderService,
|
||||
validators.purchaseOrderValidator,
|
||||
services.purchaseCategoryService,
|
||||
validators.purchaseCategoryValidator,
|
||||
services.unitConverterService,
|
||||
validators.unitConverterValidator,
|
||||
services.chartOfAccountTypeService,
|
||||
@ -131,27 +119,13 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
services.customerPointsService,
|
||||
services.spinGameService,
|
||||
middleware.customerAuthMiddleware,
|
||||
services.userDeviceService,
|
||||
validators.userDeviceValidator,
|
||||
services.notificationService,
|
||||
validators.notificationValidator,
|
||||
services.productOutletPriceService,
|
||||
validators.productOutletPriceValidator,
|
||||
selfOrderHandler,
|
||||
services.expenseService,
|
||||
validators.expenseValidator,
|
||||
a.redisClient,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) Start(port string) error {
|
||||
// Start the omset milestone scheduler (checks every 5 minutes for daily omset milestones)
|
||||
if a.omsetScheduler != nil {
|
||||
a.omsetScheduler.Start(5 * time.Minute)
|
||||
}
|
||||
|
||||
engine := a.router.Init()
|
||||
|
||||
a.server = &http.Server{
|
||||
@ -187,9 +161,6 @@ func (a *App) Start(port string) error {
|
||||
}
|
||||
|
||||
func (a *App) Shutdown() {
|
||||
if a.omsetScheduler != nil {
|
||||
a.omsetScheduler.Stop()
|
||||
}
|
||||
close(a.shutdown)
|
||||
}
|
||||
|
||||
@ -218,7 +189,6 @@ type repositories struct {
|
||||
productRecipeRepo *repository.ProductRecipeRepository
|
||||
vendorRepo *repository.VendorRepositoryImpl
|
||||
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
|
||||
purchaseCategoryRepo *repository.PurchaseCategoryRepositoryImpl
|
||||
unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl
|
||||
chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl
|
||||
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
||||
@ -238,12 +208,6 @@ type repositories struct {
|
||||
otpRepo repository.OtpRepository
|
||||
sessionRepo repository.SessionRepository
|
||||
txManager *repository.TxManager
|
||||
userDeviceRepo *repository.UserDeviceRepositoryImpl
|
||||
notificationRepo *repository.NotificationRepositoryImpl
|
||||
notificationReceiverRepo *repository.NotificationReceiverRepositoryImpl
|
||||
notificationDeliveryRepo *repository.NotificationDeliveryRepositoryImpl
|
||||
productOutletPriceRepo *repository.ProductOutletPriceRepositoryImpl
|
||||
expenseRepo *repository.ExpenseRepositoryImpl
|
||||
}
|
||||
|
||||
func (a *App) initRepositories() *repositories {
|
||||
@ -272,7 +236,6 @@ func (a *App) initRepositories() *repositories {
|
||||
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
|
||||
vendorRepo: repository.NewVendorRepositoryImpl(a.db),
|
||||
purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db),
|
||||
purchaseCategoryRepo: repository.NewPurchaseCategoryRepositoryImpl(a.db),
|
||||
unitConverterRepo: repository.NewIngredientUnitConverterRepositoryImpl(a.db).(*repository.IngredientUnitConverterRepositoryImpl),
|
||||
chartOfAccountTypeRepo: repository.NewChartOfAccountTypeRepositoryImpl(a.db),
|
||||
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
|
||||
@ -292,12 +255,6 @@ func (a *App) initRepositories() *repositories {
|
||||
otpRepo: repository.NewOtpRepository(a.db),
|
||||
sessionRepo: repository.NewSessionRepository(a.redisClient),
|
||||
txManager: repository.NewTxManager(a.db),
|
||||
userDeviceRepo: repository.NewUserDeviceRepositoryImpl(a.db),
|
||||
notificationRepo: repository.NewNotificationRepository(a.db),
|
||||
notificationReceiverRepo: repository.NewNotificationReceiverRepository(a.db),
|
||||
notificationDeliveryRepo: repository.NewNotificationDeliveryRepository(a.db),
|
||||
productOutletPriceRepo: repository.NewProductOutletPriceRepositoryImpl(a.db),
|
||||
expenseRepo: repository.NewExpenseRepositoryImpl(a.db),
|
||||
}
|
||||
}
|
||||
|
||||
@ -321,7 +278,6 @@ type processors struct {
|
||||
productRecipeProcessor *processor.ProductRecipeProcessorImpl
|
||||
vendorProcessor *processor.VendorProcessorImpl
|
||||
purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl
|
||||
purchaseCategoryProcessor *processor.PurchaseCategoryProcessorImpl
|
||||
unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl
|
||||
chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl
|
||||
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
|
||||
@ -340,16 +296,14 @@ type processors struct {
|
||||
customerPointsProcessor *processor.CustomerPointsProcessor
|
||||
otpProcessor processor.OtpProcessor
|
||||
fileClient processor.FileClient
|
||||
fcmClient client.FcmClient
|
||||
inventoryMovementService service.InventoryMovementService
|
||||
userDeviceProcessor *processor.UserDeviceProcessorImpl
|
||||
notificationProcessor *processor.NotificationProcessorImpl
|
||||
productOutletPriceProcessor processor.ProductOutletPriceProcessor
|
||||
expenseProcessor *processor.ExpenseProcessorImpl
|
||||
}
|
||||
|
||||
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
||||
fileClient := client.NewFileClient(cfg.S3Config)
|
||||
fonnteClient := client.NewFonnteClient(cfg.GetFonnte())
|
||||
fcmClient := client.NewFcmClient(cfg.GetFirebase())
|
||||
otpProcessor := processor.NewOtpProcessor(fonnteClient, repos.otpRepo)
|
||||
inventoryMovementService := service.NewInventoryMovementService(repos.inventoryMovementRepo, repos.ingredientRepo)
|
||||
|
||||
@ -359,21 +313,20 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
outletProcessor: processor.NewOutletProcessorImpl(repos.outletRepo),
|
||||
outletSettingProcessor: processor.NewOutletSettingProcessorImpl(repos.outletSettingRepo, repos.outletRepo),
|
||||
categoryProcessor: processor.NewCategoryProcessorImpl(repos.categoryRepo),
|
||||
productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo, repos.productOutletPriceRepo),
|
||||
productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo),
|
||||
productVariantProcessor: processor.NewProductVariantProcessorImpl(repos.productVariantRepo, repos.productRepo),
|
||||
inventoryProcessor: processor.NewInventoryProcessorImpl(repos.inventoryRepo, repos.productRepo, repos.outletRepo, repos.ingredientRepo, repos.inventoryMovementRepo),
|
||||
orderProcessor: processor.NewOrderProcessorImpl(repos.orderRepo, repos.orderItemRepo, repos.paymentRepo, repos.paymentOrderItemRepo, repos.productRepo, repos.paymentMethodRepo, repos.inventoryRepo, repos.inventoryMovementRepo, repos.productVariantRepo, repos.outletRepo, repos.customerRepo, repos.txManager, repos.productRecipeRepo, repos.ingredientRepo, inventoryMovementService, repos.productOutletPriceRepo),
|
||||
orderProcessor: processor.NewOrderProcessorImpl(repos.orderRepo, repos.orderItemRepo, repos.paymentRepo, repos.paymentOrderItemRepo, repos.productRepo, repos.paymentMethodRepo, repos.inventoryRepo, repos.inventoryMovementRepo, repos.productVariantRepo, repos.outletRepo, repos.customerRepo, repos.txManager, repos.productRecipeRepo, repos.ingredientRepo, inventoryMovementService),
|
||||
paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo),
|
||||
fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient),
|
||||
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
|
||||
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo, repos.expenseRepo),
|
||||
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo),
|
||||
tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo),
|
||||
unitProcessor: processor.NewUnitProcessor(repos.unitRepo),
|
||||
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo, repos.ingredientCompositionRepo),
|
||||
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
||||
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
|
||||
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.purchaseCategoryRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
||||
purchaseCategoryProcessor: processor.NewPurchaseCategoryProcessorImpl(repos.purchaseCategoryRepo),
|
||||
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),
|
||||
@ -392,11 +345,8 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
customerPointsProcessor: processor.NewCustomerPointsProcessor(repos.customerPointsRepo, repos.gameRepo),
|
||||
otpProcessor: otpProcessor,
|
||||
fileClient: fileClient,
|
||||
fcmClient: fcmClient,
|
||||
inventoryMovementService: inventoryMovementService,
|
||||
userDeviceProcessor: processor.NewUserDeviceProcessorImpl(repos.userDeviceRepo),
|
||||
notificationProcessor: buildNotificationProcessor(cfg, repos),
|
||||
productOutletPriceProcessor: processor.NewProductOutletPriceProcessorImpl(repos.productOutletPriceRepo, repos.productRepo, repos.outletRepo),
|
||||
expenseProcessor: processor.NewExpenseProcessorImpl(repos.expenseRepo, repos.purchaseCategoryRepo),
|
||||
}
|
||||
}
|
||||
|
||||
@ -422,7 +372,6 @@ type services struct {
|
||||
productRecipeService *service.ProductRecipeServiceImpl
|
||||
vendorService *service.VendorServiceImpl
|
||||
purchaseOrderService *service.PurchaseOrderServiceImpl
|
||||
purchaseCategoryService service.PurchaseCategoryService
|
||||
unitConverterService *service.IngredientUnitConverterServiceImpl
|
||||
chartOfAccountTypeService service.ChartOfAccountTypeService
|
||||
chartOfAccountService service.ChartOfAccountService
|
||||
@ -434,15 +383,11 @@ type services struct {
|
||||
customerAuthService service.CustomerAuthService
|
||||
customerPointsService service.CustomerPointsService
|
||||
spinGameService service.SpinGameService
|
||||
userDeviceService service.UserDeviceService
|
||||
notificationService service.NotificationService
|
||||
productOutletPriceService service.ProductOutletPriceService
|
||||
expenseService *service.ExpenseServiceImpl
|
||||
}
|
||||
|
||||
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
||||
authConfig := cfg.Auth()
|
||||
authService := service.NewAuthService(processors.userProcessor, processors.userDeviceProcessor, authConfig)
|
||||
authService := service.NewAuthService(processors.userProcessor, authConfig)
|
||||
organizationService := service.NewOrganizationService(processors.organizationProcessor)
|
||||
outletService := service.NewOutletService(processors.outletProcessor)
|
||||
outletSettingService := service.NewOutletSettingService(processors.outletSettingProcessor)
|
||||
@ -450,7 +395,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
productService := service.NewProductService(processors.productProcessor)
|
||||
productVariantService := service.NewProductVariantService(processors.productVariantProcessor)
|
||||
inventoryService := service.NewInventoryService(processors.inventoryProcessor)
|
||||
orderService := service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, nil, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo, processors.notificationProcessor, repos.userRepo) // Will be updated after orderIngredientTransactionService is created
|
||||
orderService := service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, nil, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo) // Will be updated after orderIngredientTransactionService is created
|
||||
paymentMethodService := service.NewPaymentMethodService(processors.paymentMethodProcessor)
|
||||
fileService := service.NewFileServiceImpl(processors.fileProcessor)
|
||||
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor)
|
||||
@ -462,7 +407,6 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
productRecipeService := service.NewProductRecipeService(processors.productRecipeProcessor)
|
||||
vendorService := service.NewVendorService(processors.vendorProcessor)
|
||||
purchaseOrderService := service.NewPurchaseOrderService(processors.purchaseOrderProcessor)
|
||||
purchaseCategoryService := service.NewPurchaseCategoryService(processors.purchaseCategoryProcessor)
|
||||
unitConverterService := service.NewIngredientUnitConverterService(processors.unitConverterProcessor)
|
||||
chartOfAccountTypeService := service.NewChartOfAccountTypeService(processors.chartOfAccountTypeProcessor)
|
||||
chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor)
|
||||
@ -474,11 +418,9 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
customerAuthService := service.NewCustomerAuthService(processors.customerAuthProcessor)
|
||||
customerPointsService := service.NewCustomerPointsService(processors.customerPointsProcessor)
|
||||
spinGameService := service.NewSpinGameService(processors.gamePlayProcessor, repos.txManager)
|
||||
userDeviceService := service.NewUserDeviceService(processors.userDeviceProcessor)
|
||||
notificationService := service.NewNotificationService(processors.notificationProcessor)
|
||||
|
||||
// Update order service with order ingredient transaction service
|
||||
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo, processors.notificationProcessor, repos.userRepo)
|
||||
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo)
|
||||
|
||||
return &services{
|
||||
userService: service.NewUserService(processors.userProcessor),
|
||||
@ -502,7 +444,6 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
productRecipeService: productRecipeService,
|
||||
vendorService: vendorService,
|
||||
purchaseOrderService: purchaseOrderService,
|
||||
purchaseCategoryService: purchaseCategoryService,
|
||||
unitConverterService: unitConverterService,
|
||||
chartOfAccountTypeService: chartOfAccountTypeService,
|
||||
chartOfAccountService: chartOfAccountService,
|
||||
@ -514,10 +455,6 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
customerAuthService: customerAuthService,
|
||||
customerPointsService: customerPointsService,
|
||||
spinGameService: spinGameService,
|
||||
userDeviceService: userDeviceService,
|
||||
notificationService: notificationService,
|
||||
productOutletPriceService: service.NewProductOutletPriceService(processors.productOutletPriceProcessor),
|
||||
expenseService: service.NewExpenseService(processors.expenseProcessor),
|
||||
}
|
||||
}
|
||||
|
||||
@ -548,7 +485,6 @@ type validators struct {
|
||||
tableValidator *validator.TableValidator
|
||||
vendorValidator *validator.VendorValidatorImpl
|
||||
purchaseOrderValidator *validator.PurchaseOrderValidatorImpl
|
||||
purchaseCategoryValidator *validator.PurchaseCategoryValidatorImpl
|
||||
unitConverterValidator *validator.IngredientUnitConverterValidatorImpl
|
||||
chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl
|
||||
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
|
||||
@ -558,10 +494,6 @@ type validators struct {
|
||||
rewardValidator validator.RewardValidator
|
||||
campaignValidator validator.CampaignValidator
|
||||
customerAuthValidator validator.CustomerAuthValidator
|
||||
userDeviceValidator *validator.UserDeviceValidatorImpl
|
||||
notificationValidator *validator.NotificationValidatorImpl
|
||||
productOutletPriceValidator *validator.ProductOutletPriceValidatorImpl
|
||||
expenseValidator *validator.ExpenseValidatorImpl
|
||||
}
|
||||
|
||||
func (a *App) initValidators() *validators {
|
||||
@ -580,7 +512,6 @@ func (a *App) initValidators() *validators {
|
||||
tableValidator: validator.NewTableValidator(),
|
||||
vendorValidator: validator.NewVendorValidator(),
|
||||
purchaseOrderValidator: validator.NewPurchaseOrderValidator(),
|
||||
purchaseCategoryValidator: validator.NewPurchaseCategoryValidator(),
|
||||
unitConverterValidator: validator.NewIngredientUnitConverterValidator().(*validator.IngredientUnitConverterValidatorImpl),
|
||||
chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl),
|
||||
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
|
||||
@ -590,32 +521,5 @@ func (a *App) initValidators() *validators {
|
||||
rewardValidator: validator.NewRewardValidator(),
|
||||
campaignValidator: validator.NewCampaignValidator(),
|
||||
customerAuthValidator: validator.NewCustomerAuthValidator(),
|
||||
userDeviceValidator: validator.NewUserDeviceValidator(),
|
||||
notificationValidator: validator.NewNotificationValidator(),
|
||||
productOutletPriceValidator: validator.NewProductOutletPriceValidator(),
|
||||
expenseValidator: validator.NewExpenseValidator(),
|
||||
}
|
||||
}
|
||||
|
||||
// buildNotificationProcessor creates the notification processor with FCM integration.
|
||||
// If FCM is not configured, it returns a processor with a nil FCM client (FCM dispatch will be skipped).
|
||||
func buildNotificationProcessor(cfg *config.Config, repos *repositories) *processor.NotificationProcessorImpl {
|
||||
var fcmClient client.FCMClient
|
||||
if cfg.FCM.CredentialsFile != "" {
|
||||
var err error
|
||||
fcmClient, err = client.NewFCMClient(&cfg.FCM)
|
||||
if err != nil {
|
||||
// FCM init failure is non-fatal; notifications will still be persisted.
|
||||
fcmClient = nil
|
||||
}
|
||||
}
|
||||
|
||||
return processor.NewNotificationProcessor(
|
||||
repos.notificationRepo,
|
||||
repos.notificationReceiverRepo,
|
||||
repos.notificationDeliveryRepo,
|
||||
repos.userDeviceRepo,
|
||||
repos.userRepo,
|
||||
fcmClient,
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,140 +3,81 @@ package client
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"apskel-pos-be/config"
|
||||
|
||||
firebase "firebase.google.com/go/v4"
|
||||
"firebase.google.com/go/v4/messaging"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
type FCMConfig interface {
|
||||
GetCredentialsFile() string
|
||||
GetProjectID() string
|
||||
}
|
||||
|
||||
type FCMClient interface {
|
||||
SendNotification(ctx context.Context, token string, title string, body string, data map[string]string) error
|
||||
SendMulticastNotification(ctx context.Context, tokens []string, title string, body string, data map[string]string) error
|
||||
SendToTopic(ctx context.Context, topic string, title string, body string, data map[string]string) error
|
||||
type FcmClient interface {
|
||||
SendMulticastNotification(ctx context.Context, tokens []string, title string, body string) error
|
||||
}
|
||||
|
||||
type fcmClient struct {
|
||||
messaging *messaging.Client
|
||||
messagingClient *messaging.Client
|
||||
}
|
||||
|
||||
func NewFCMClient(cfg FCMConfig) (FCMClient, error) {
|
||||
ctx := context.Background()
|
||||
func NewFcmClient(cfg *config.Firebase) FcmClient {
|
||||
if cfg == nil || cfg.GetCredentialsFile() == "" {
|
||||
log.Println("FCM: credentials file not configured, FCM client is disabled")
|
||||
return &fcmClient{messagingClient: nil}
|
||||
}
|
||||
|
||||
opt := option.WithCredentialsFile(cfg.GetCredentialsFile())
|
||||
|
||||
app, err := firebase.NewApp(ctx, &firebase.Config{
|
||||
ProjectID: cfg.GetProjectID(),
|
||||
}, opt)
|
||||
app, err := firebase.NewApp(context.Background(), nil, opt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize firebase app: %w", err)
|
||||
log.Printf("FCM: failed to initialize Firebase app: %v", err)
|
||||
return &fcmClient{messagingClient: nil}
|
||||
}
|
||||
|
||||
msgClient, err := app.Messaging(ctx)
|
||||
client, err := app.Messaging(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize firebase messaging client: %w", err)
|
||||
log.Printf("FCM: failed to create messaging client: %v", err)
|
||||
return &fcmClient{messagingClient: nil}
|
||||
}
|
||||
|
||||
return &fcmClient{
|
||||
messaging: msgClient,
|
||||
}, nil
|
||||
log.Println("FCM: client initialized successfully")
|
||||
return &fcmClient{messagingClient: client}
|
||||
}
|
||||
|
||||
// SendNotification sends a push notification to a single device token.
|
||||
func (f *fcmClient) SendNotification(ctx context.Context, token string, title string, body string, data map[string]string) error {
|
||||
message := &messaging.Message{
|
||||
Token: token,
|
||||
Notification: &messaging.Notification{
|
||||
Title: title,
|
||||
Body: body,
|
||||
},
|
||||
Data: data,
|
||||
Android: &messaging.AndroidConfig{
|
||||
Priority: "high",
|
||||
},
|
||||
APNS: &messaging.APNSConfig{
|
||||
Payload: &messaging.APNSPayload{
|
||||
Aps: &messaging.Aps{
|
||||
Sound: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
func (c *fcmClient) SendMulticastNotification(ctx context.Context, tokens []string, title string, body string) error {
|
||||
if c.messagingClient == nil {
|
||||
log.Println("FCM: client not initialized, skipping notification")
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := f.messaging.Send(ctx, message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send FCM notification: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendMulticastNotification sends a push notification to multiple device tokens.
|
||||
func (f *fcmClient) SendMulticastNotification(ctx context.Context, tokens []string, title string, body string, data map[string]string) error {
|
||||
if len(tokens) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
message := &messaging.MulticastMessage{
|
||||
Notification: &messaging.Notification{
|
||||
Title: title,
|
||||
Body: body,
|
||||
},
|
||||
Tokens: tokens,
|
||||
Notification: &messaging.Notification{
|
||||
Title: title,
|
||||
Body: body,
|
||||
},
|
||||
Data: data,
|
||||
Android: &messaging.AndroidConfig{
|
||||
Priority: "high",
|
||||
},
|
||||
APNS: &messaging.APNSConfig{
|
||||
Payload: &messaging.APNSPayload{
|
||||
Aps: &messaging.Aps{
|
||||
Sound: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
batchResp, err := f.messaging.SendEachForMulticast(ctx, message)
|
||||
response, err := c.messagingClient.SendMulticast(ctx, message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send FCM multicast notification: %w", err)
|
||||
return fmt.Errorf("FCM: failed to send multicast notification: %w", err)
|
||||
}
|
||||
|
||||
if batchResp.FailureCount > 0 {
|
||||
return fmt.Errorf("FCM multicast: %d/%d messages failed to send", batchResp.FailureCount, len(tokens))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendToTopic sends a push notification to all devices subscribed to a topic.
|
||||
func (f *fcmClient) SendToTopic(ctx context.Context, topic string, title string, body string, data map[string]string) error {
|
||||
message := &messaging.Message{
|
||||
Topic: topic,
|
||||
Notification: &messaging.Notification{
|
||||
Title: title,
|
||||
Body: body,
|
||||
},
|
||||
Data: data,
|
||||
Android: &messaging.AndroidConfig{
|
||||
Priority: "high",
|
||||
},
|
||||
APNS: &messaging.APNSConfig{
|
||||
Payload: &messaging.APNSPayload{
|
||||
Aps: &messaging.Aps{
|
||||
Sound: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := f.messaging.Send(ctx, message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send FCM topic notification: %w", err)
|
||||
if response.FailureCount > 0 {
|
||||
log.Printf("FCM: %d tokens failed out of %d", response.FailureCount, len(tokens))
|
||||
for i, resp := range response.Responses {
|
||||
if !resp.Success {
|
||||
log.Printf("FCM: token[%d] failed: %v", i, resp.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("FCM: sent %d/%d notifications successfully", response.SuccessCount, len(tokens))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -40,28 +40,22 @@ const (
|
||||
OutletServiceEntity = "outlet_service"
|
||||
VendorServiceEntity = "vendor_service"
|
||||
PurchaseOrderServiceEntity = "purchase_order_service"
|
||||
PurchaseCategoryServiceEntity = "purchase_category_service"
|
||||
IngredientUnitConverterServiceEntity = "ingredient_unit_converter_service"
|
||||
IngredientCompositionServiceEntity = "ingredient_composition_service"
|
||||
TableEntity = "table"
|
||||
// Gamification entities
|
||||
CustomerPointsEntity = "customer_points"
|
||||
CustomerTokensEntity = "customer_tokens"
|
||||
TierEntity = "tier"
|
||||
GameEntity = "game"
|
||||
GamePrizeEntity = "game_prize"
|
||||
GamePlayEntity = "game_play"
|
||||
OmsetTrackerEntity = "omset_tracker"
|
||||
RewardEntity = "reward"
|
||||
CampaignEntity = "campaign"
|
||||
CampaignRuleEntity = "campaign_rule"
|
||||
CustomerEntity = "customer"
|
||||
SpinGameHandlerEntity = "spin_game_handler"
|
||||
UserDeviceServiceEntity = "user_device_service"
|
||||
NotificationServiceEntity = "notification_service"
|
||||
NotificationHandlerEntity = "notification_handler"
|
||||
ProductOutletPriceServiceEntity = "product_outlet_price_service"
|
||||
ExpenseServiceEntity = "expense_service"
|
||||
CustomerPointsEntity = "customer_points"
|
||||
CustomerTokensEntity = "customer_tokens"
|
||||
TierEntity = "tier"
|
||||
GameEntity = "game"
|
||||
GamePrizeEntity = "game_prize"
|
||||
GamePlayEntity = "game_play"
|
||||
OmsetTrackerEntity = "omset_tracker"
|
||||
RewardEntity = "reward"
|
||||
CampaignEntity = "campaign"
|
||||
CampaignRuleEntity = "campaign_rule"
|
||||
CustomerEntity = "customer"
|
||||
SpinGameHandlerEntity = "spin_game_handler"
|
||||
)
|
||||
|
||||
var HttpErrorMap = map[string]int{
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
package constants
|
||||
|
||||
type ExpenseStatus string
|
||||
|
||||
const (
|
||||
ExpenseStatusDraft ExpenseStatus = "draft"
|
||||
ExpenseStatusSent ExpenseStatus = "sent"
|
||||
ExpenseStatusApproved ExpenseStatus = "approved"
|
||||
ExpenseStatusCancel ExpenseStatus = "cancel"
|
||||
)
|
||||
|
||||
func GetAllExpenseStatuses() []ExpenseStatus {
|
||||
return []ExpenseStatus{
|
||||
ExpenseStatusDraft,
|
||||
ExpenseStatusSent,
|
||||
ExpenseStatusApproved,
|
||||
ExpenseStatusCancel,
|
||||
}
|
||||
}
|
||||
|
||||
func IsValidExpenseStatus(status ExpenseStatus) bool {
|
||||
for _, validStatus := range GetAllExpenseStatuses() {
|
||||
if status == validStatus {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -3,12 +3,10 @@ package constants
|
||||
type UserRole string
|
||||
|
||||
const (
|
||||
RoleAdmin UserRole = "admin"
|
||||
RoleManager UserRole = "manager"
|
||||
RoleCashier UserRole = "cashier"
|
||||
RoleWaiter UserRole = "waiter"
|
||||
RoleOwner UserRole = "owner"
|
||||
RolePurchasing UserRole = "purchasing"
|
||||
RoleAdmin UserRole = "admin"
|
||||
RoleManager UserRole = "manager"
|
||||
RoleCashier UserRole = "cashier"
|
||||
RoleWaiter UserRole = "waiter"
|
||||
)
|
||||
|
||||
func GetAllUserRoles() []UserRole {
|
||||
@ -17,8 +15,6 @@ func GetAllUserRoles() []UserRole {
|
||||
RoleManager,
|
||||
RoleCashier,
|
||||
RoleWaiter,
|
||||
RoleOwner,
|
||||
RolePurchasing,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,12 +5,12 @@ import (
|
||||
)
|
||||
|
||||
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"`
|
||||
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 {
|
||||
@ -24,21 +24,21 @@ type UpdateAccountRequest struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
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 {
|
||||
|
||||
@ -7,18 +7,17 @@ import (
|
||||
)
|
||||
|
||||
type PaymentMethodAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID `form:"organization_id"`
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
OrganizationID uuid.UUID `form:"organization_id"`
|
||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
}
|
||||
|
||||
// PaymentMethodAnalyticsResponse represents the response for payment method analytics
|
||||
type PaymentMethodAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
@ -46,16 +45,15 @@ type PaymentMethodAnalyticsData struct {
|
||||
|
||||
type SalesAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
}
|
||||
|
||||
type SalesAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
@ -85,85 +83,19 @@ type SalesAnalyticsData struct {
|
||||
NetSales float64 `json:"net_sales"`
|
||||
}
|
||||
|
||||
type PurchasingAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
}
|
||||
|
||||
type PurchasingAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
Summary PurchasingSummary `json:"summary"`
|
||||
Data []PurchasingAnalyticsData `json:"data"`
|
||||
IngredientData []PurchasingIngredientData `json:"ingredient_data"`
|
||||
VendorData []PurchasingVendorData `json:"vendor_data"`
|
||||
}
|
||||
|
||||
type PurchasingSummary struct {
|
||||
TotalPurchases float64 `json:"total_purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
TotalQuantity float64 `json:"total_quantity"`
|
||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||
TotalIngredients int64 `json:"total_ingredients"`
|
||||
TotalVendors int64 `json:"total_vendors"`
|
||||
}
|
||||
|
||||
type PurchasingAnalyticsData struct {
|
||||
Date time.Time `json:"date"`
|
||||
Purchases float64 `json:"purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
PurchaseOrders int64 `json:"purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Ingredients int64 `json:"ingredients"`
|
||||
Vendors int64 `json:"vendors"`
|
||||
}
|
||||
|
||||
type PurchasingIngredientData struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
IngredientName string `json:"ingredient_name"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
AverageUnitCost float64 `json:"average_unit_cost"`
|
||||
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||
}
|
||||
|
||||
type PurchasingVendorData struct {
|
||||
VendorID *uuid.UUID `json:"vendor_id"`
|
||||
VendorName string `json:"vendor_name"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||
IngredientCount int64 `json:"ingredient_count"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
}
|
||||
|
||||
// ProductAnalyticsRequest represents the request for product analytics
|
||||
type ProductAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
Limit int `form:"limit,default=1000" validate:"min=1,max=1000"`
|
||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
Limit int `form:"limit,default=1000" validate:"min=1,max=1000"`
|
||||
}
|
||||
|
||||
// ProductAnalyticsResponse represents the response for product analytics
|
||||
type ProductAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
Data []ProductAnalyticsData `json:"data"`
|
||||
@ -173,7 +105,6 @@ type ProductAnalyticsData struct {
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
ProductSku string `json:"product_sku"`
|
||||
ProductPrice float64 `json:"product_price"`
|
||||
CategoryID uuid.UUID `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
CategoryOrder int `json:"category_order"`
|
||||
@ -192,16 +123,15 @@ type ProductAnalyticsData struct {
|
||||
// ProductAnalyticsPerCategoryRequest represents the request for product analytics per category
|
||||
type ProductAnalyticsPerCategoryRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
}
|
||||
|
||||
// ProductAnalyticsPerCategoryResponse represents the response for product analytics per category
|
||||
type ProductAnalyticsPerCategoryResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
Data []ProductAnalyticsPerCategoryData `json:"data"`
|
||||
@ -222,16 +152,15 @@ type ProductAnalyticsPerCategoryData struct {
|
||||
// DashboardAnalyticsRequest represents the request for dashboard analytics
|
||||
type DashboardAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
}
|
||||
|
||||
// DashboardAnalyticsResponse represents the response for dashboard analytics
|
||||
type DashboardAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
Overview DashboardOverview `json:"overview"`
|
||||
@ -242,58 +171,36 @@ type DashboardAnalyticsResponse struct {
|
||||
|
||||
// DashboardOverview represents the overview data for dashboard
|
||||
type DashboardOverview struct {
|
||||
TotalSales float64 `json:"total_sales"`
|
||||
TotalOrders int64 `json:"total_orders"`
|
||||
AverageOrderValue float64 `json:"average_order_value"`
|
||||
TotalCustomers int64 `json:"total_customers"`
|
||||
VoidedOrders int64 `json:"voided_orders"`
|
||||
RefundedOrders int64 `json:"refunded_orders"`
|
||||
TotalItemSold int64 `json:"total_item_sold"`
|
||||
TotalLowStock int64 `json:"total_low_stock"`
|
||||
TotalProductActive int64 `json:"total_product_active"`
|
||||
TotalSales float64 `json:"total_sales"`
|
||||
TotalOrders int64 `json:"total_orders"`
|
||||
AverageOrderValue float64 `json:"average_order_value"`
|
||||
TotalCustomers int64 `json:"total_customers"`
|
||||
VoidedOrders int64 `json:"voided_orders"`
|
||||
RefundedOrders int64 `json:"refunded_orders"`
|
||||
}
|
||||
|
||||
// ProfitLossAnalyticsRequest represents the request for profit and loss analytics
|
||||
type ProfitLossAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
}
|
||||
|
||||
// ProfitLossAnalyticsResponse represents the response for profit and loss analytics
|
||||
type ProfitLossAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
Summary ProfitLossSummary `json:"summary"`
|
||||
Data []ProfitLossData `json:"data"`
|
||||
ProductData []ProductProfitData `json:"product_data"`
|
||||
MainSummary []ProfitLossSummaryRow `json:"main_summary"`
|
||||
Purchasing ProfitLossPurchasing `json:"purchasing"`
|
||||
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
|
||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||
}
|
||||
|
||||
type ProfitLossPurchasing struct {
|
||||
TodayTotal float64 `json:"today_total"`
|
||||
MtdTotal float64 `json:"mtd_total"`
|
||||
TodayRawMaterial float64 `json:"today_raw_material"`
|
||||
MtdRawMaterial float64 `json:"mtd_raw_material"`
|
||||
TodayExpense float64 `json:"today_expense"`
|
||||
MtdExpense float64 `json:"mtd_expense"`
|
||||
Items []ProfitLossPurchasingItem `json:"items"`
|
||||
}
|
||||
|
||||
type ProfitLossPurchasingItem struct {
|
||||
Date time.Time `json:"date"`
|
||||
Item string `json:"item"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Nominal float64 `json:"nominal"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
Summary ProfitLossSummary `json:"summary"`
|
||||
Data []ProfitLossData `json:"data"`
|
||||
ProductData []ProductProfitData `json:"product_data"`
|
||||
}
|
||||
|
||||
// ProfitLossSummary represents the summary of profit and loss analytics
|
||||
type ProfitLossSummary struct {
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
@ -308,6 +215,7 @@ type ProfitLossSummary struct {
|
||||
ProfitabilityRatio float64 `json:"profitability_ratio"`
|
||||
}
|
||||
|
||||
// ProfitLossData represents individual profit and loss data point by time period
|
||||
type ProfitLossData struct {
|
||||
Date time.Time `json:"date"`
|
||||
Revenue float64 `json:"revenue"`
|
||||
@ -321,6 +229,7 @@ type ProfitLossData struct {
|
||||
Orders int64 `json:"orders"`
|
||||
}
|
||||
|
||||
// ProductProfitData represents profit data for individual products
|
||||
type ProductProfitData struct {
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
@ -335,139 +244,3 @@ type ProductProfitData struct {
|
||||
AverageCost float64 `json:"average_cost"`
|
||||
ProfitPerUnit float64 `json:"profit_per_unit"`
|
||||
}
|
||||
|
||||
type ProfitLossSummaryRow struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
IsBold bool `json:"is_bold"`
|
||||
TodayNominal float64 `json:"today_nominal"`
|
||||
TodayPct float64 `json:"today_pct"`
|
||||
MtdNominal float64 `json:"mtd_nominal"`
|
||||
MtdPct float64 `json:"mtd_pct"`
|
||||
SubItems []ProfitLossSummaryRow `json:"sub_items,omitempty"`
|
||||
}
|
||||
|
||||
type OperationalExpenseItem struct {
|
||||
Item string `json:"item"`
|
||||
Nominal float64 `json:"nominal"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryPeriodRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
ExcludeGajiStaffFromReimburse bool `form:"exclude_gaji_staff_from_reimburse"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryMonthlyRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
Month string `form:"month" validate:"required"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryMTDRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
ExcludeGajiStaffFromReimburse bool `form:"exclude_gaji_staff_from_reimburse"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryPeriodResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
Period ExclusiveSummaryPeriodRange `json:"period"`
|
||||
Summary ExclusiveSummaryPeriodSummary `json:"summary"`
|
||||
Reimburse ExclusiveSummaryReimburse `json:"reimburse"`
|
||||
HPPBreakdown []ExclusiveSummaryCategoryBreakdown `json:"hpp_breakdown"`
|
||||
OperationalExpenseBreakdown []ExclusiveSummaryCategoryBreakdown `json:"operational_expense_breakdown"`
|
||||
DailySummary []ExclusiveSummaryDailySummary `json:"daily_summary"`
|
||||
DailyTransactions []ExclusiveSummaryDailyTransaction `json:"daily_transactions"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryPeriodRange struct {
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryPeriodSummary struct {
|
||||
Sales float64 `json:"sales"`
|
||||
HPP float64 `json:"hpp"`
|
||||
GrossProfit float64 `json:"gross_profit"`
|
||||
SalaryTotal float64 `json:"salary_total"`
|
||||
SalaryDW float64 `json:"salary_dw"`
|
||||
SalaryStaff float64 `json:"salary_staff"`
|
||||
SalaryOther float64 `json:"salary_other"`
|
||||
OtherOperationalExpenses float64 `json:"other_operational_expenses"`
|
||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
NetProfit float64 `json:"net_profit"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryReimburse struct {
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
ExcludedSalaryStaff float64 `json:"excluded_salary_staff"`
|
||||
TotalReimburse float64 `json:"total_reimburse"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryCategoryBreakdown struct {
|
||||
CategoryCode string `json:"category_code"`
|
||||
CategoryName string `json:"category_name"`
|
||||
Amount float64 `json:"amount"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryDailySummary struct {
|
||||
Date time.Time `json:"date"`
|
||||
TransactionCount int64 `json:"transaction_count"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryDailyTransaction struct {
|
||||
Date time.Time `json:"date"`
|
||||
CategoryCode string `json:"category_code"`
|
||||
CategoryName string `json:"category_name"`
|
||||
Description string `json:"description"`
|
||||
Amount float64 `json:"amount"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryMonthlyResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
Month string `json:"month"`
|
||||
Summary ExclusiveSummaryMonthlySummary `json:"summary"`
|
||||
Periods []ExclusiveSummaryMonthlyPeriod `json:"periods"`
|
||||
BankBalance []ExclusiveSummaryBankBalance `json:"bank_balance"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryMonthlySummary struct {
|
||||
TotalSales float64 `json:"total_sales"`
|
||||
HPP float64 `json:"hpp"`
|
||||
GrossProfit float64 `json:"gross_profit"`
|
||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
NetProfit float64 `json:"net_profit"`
|
||||
NetProfitMargin float64 `json:"net_profit_margin"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryMonthlyPeriod struct {
|
||||
Label string `json:"label"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
Sales float64 `json:"sales"`
|
||||
HPP float64 `json:"hpp"`
|
||||
GrossProfit float64 `json:"gross_profit"`
|
||||
GrossMargin float64 `json:"gross_margin"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryBankBalance struct {
|
||||
Bank string `json:"bank"`
|
||||
OpeningBalance *float64 `json:"opening_balance"`
|
||||
IncomingMutation *float64 `json:"incoming_mutation"`
|
||||
OutgoingMutation *float64 `json:"outgoing_mutation"`
|
||||
ClosingBalance *float64 `json:"closing_balance"`
|
||||
Notes *string `json:"notes"`
|
||||
}
|
||||
|
||||
@ -10,8 +10,7 @@ type CreateCategoryRequest struct {
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
BusinessType *string `json:"business_type,omitempty"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
Order *int `json:"order,omitempty"`
|
||||
Order *int `json:"order,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
@ -19,14 +18,12 @@ type UpdateCategoryRequest struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
BusinessType *string `json:"business_type,omitempty"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
Order *int `json:"order,omitempty"`
|
||||
Order *int `json:"order,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type ListCategoriesRequest struct {
|
||||
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
BusinessType string `json:"business_type,omitempty"`
|
||||
Search string `json:"search,omitempty"`
|
||||
Page int `json:"page" validate:"required,min=1"`
|
||||
@ -37,11 +34,10 @@ type ListCategoriesRequest struct {
|
||||
type CategoryResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
BusinessType string `json:"business_type"`
|
||||
Order int `json:"order"`
|
||||
Order int `json:"order"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
@ -1,161 +0,0 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CreateExpenseRequest struct {
|
||||
Receiver string `json:"receiver" validate:"required"`
|
||||
TransactionDate string `json:"transaction_date" validate:"required"`
|
||||
CodeNumber string `json:"code_number" validate:"required"`
|
||||
OutletID string `json:"outlet_id" validate:"required"`
|
||||
Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved cancel"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Tax float64 `json:"tax"`
|
||||
Total float64 `json:"total" validate:"required"`
|
||||
Items []CreateExpenseItemRequest `json:"items" validate:"required"`
|
||||
}
|
||||
|
||||
type CreateExpenseItemRequest struct {
|
||||
ChartOfAccountID string `json:"chart_of_account_id" validate:"required"`
|
||||
PurchaseCategoryID string `json:"purchase_category_id" validate:"required"`
|
||||
Item string `json:"item" validate:"required"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Amount float64 `json:"amount" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateExpenseRequest struct {
|
||||
Receiver *string `json:"receiver,omitempty"`
|
||||
TransactionDate *string `json:"transaction_date,omitempty"`
|
||||
CodeNumber *string `json:"code_number,omitempty"`
|
||||
OutletID *string `json:"outlet_id,omitempty"`
|
||||
Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved cancel"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Tax *float64 `json:"tax,omitempty"`
|
||||
Total *float64 `json:"total,omitempty"`
|
||||
Reserved1 *string `json:"reserved1,omitempty"`
|
||||
Items []UpdateExpenseItemRequest `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateExpenseItemRequest struct {
|
||||
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
||||
PurchaseCategoryID *string `json:"purchase_category_id,omitempty"`
|
||||
Item *string `json:"item,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Amount *float64 `json:"amount,omitempty"`
|
||||
}
|
||||
|
||||
type ExpenseResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID uuid.UUID `json:"outlet_id"`
|
||||
Receiver string `json:"receiver"`
|
||||
TransactionDate time.Time `json:"transaction_date"`
|
||||
CodeNumber string `json:"code_number"`
|
||||
Status string `json:"status"`
|
||||
Description *string `json:"description"`
|
||||
Tax float64 `json:"tax"`
|
||||
Total float64 `json:"total"`
|
||||
Reserved1 *string `json:"reserved1,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Items []ExpenseItemResponse `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
type ExpenseItemResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ExpenseID uuid.UUID `json:"expense_id"`
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
PurchaseCategoryName string `json:"purchase_category_name,omitempty"`
|
||||
PurchaseCategoryType string `json:"purchase_category_type,omitempty"`
|
||||
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||
Item string `json:"item"`
|
||||
Description *string `json:"description"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ListExpenseRequest struct {
|
||||
Page int `json:"page" validate:"min=1"`
|
||||
Limit int `json:"limit" validate:"min=1,max=100"`
|
||||
Search string `json:"search,omitempty"`
|
||||
OutletID string `json:"outlet_id,omitempty"`
|
||||
Status string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved cancel"`
|
||||
StartDate string `json:"start_date,omitempty"`
|
||||
EndDate string `json:"end_date,omitempty"`
|
||||
}
|
||||
|
||||
type ListExpenseResponse struct {
|
||||
Expenses []ExpenseResponse `json:"expenses"`
|
||||
TotalCount int `json:"total_count"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsRequest struct {
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
Summary ExpenseAnalyticsSummary `json:"summary"`
|
||||
Data []ExpenseAnalyticsData `json:"data"`
|
||||
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
|
||||
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData `json:"chart_of_account_data"`
|
||||
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsSummary struct {
|
||||
TotalExpenses float64 `json:"total_expenses"`
|
||||
TotalExpenseCount int64 `json:"total_expense_count"`
|
||||
TotalTax float64 `json:"total_tax"`
|
||||
AverageExpenseValue float64 `json:"average_expense_value"`
|
||||
TotalCategories int64 `json:"total_categories"`
|
||||
TotalItems int64 `json:"total_items"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsData struct {
|
||||
Date time.Time `json:"date"`
|
||||
Expenses float64 `json:"expenses"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
Tax float64 `json:"tax"`
|
||||
Items int64 `json:"items"`
|
||||
Categories int64 `json:"categories"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsCategoryData struct {
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
PurchaseCategoryName string `json:"purchase_category_name"`
|
||||
PurchaseCategoryType string `json:"purchase_category_type"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
ItemCount int64 `json:"item_count"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsChartOfAccountData struct {
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
ChartOfAccountName string `json:"chart_of_account_name"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
ItemCount int64 `json:"item_count"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsItemData struct {
|
||||
Item string `json:"item"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
ItemCount int64 `json:"item_count"`
|
||||
}
|
||||
@ -81,3 +81,4 @@ type IngredientUnitsResponse struct {
|
||||
BaseUnitName string `json:"base_unit_name"`
|
||||
Units []*UnitResponse `json:"units"`
|
||||
}
|
||||
|
||||
|
||||
@ -26,9 +26,9 @@ type AdjustInventoryRequest struct {
|
||||
}
|
||||
|
||||
type RestockInventoryRequest struct {
|
||||
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
||||
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
||||
Items []RestockItem `json:"items" validate:"required,min=1,dive"`
|
||||
Reason string `json:"reason" validate:"required,min=1,max=255"`
|
||||
Reason string `json:"reason" validate:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
type RestockItem struct {
|
||||
@ -82,10 +82,10 @@ type InventoryAdjustmentResponse struct {
|
||||
}
|
||||
|
||||
type RestockInventoryResponse struct {
|
||||
OutletID uuid.UUID `json:"outlet_id"`
|
||||
Items []RestockItemResult `json:"items"`
|
||||
Reason string `json:"reason"`
|
||||
RestockedAt time.Time `json:"restocked_at"`
|
||||
OutletID uuid.UUID `json:"outlet_id"`
|
||||
Items []RestockItemResult `json:"items"`
|
||||
Reason string `json:"reason"`
|
||||
RestockedAt time.Time `json:"restocked_at"`
|
||||
}
|
||||
|
||||
type RestockItemResult struct {
|
||||
|
||||
@ -1,92 +0,0 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ---- Request contracts ----
|
||||
|
||||
type SendNotificationRequest struct {
|
||||
Title string `json:"title" validate:"required,min=1,max=255"`
|
||||
Body string `json:"body" validate:"required"`
|
||||
Type string `json:"type,omitempty" validate:"omitempty,max=100"`
|
||||
Category string `json:"category,omitempty" validate:"omitempty,max=100"`
|
||||
Priority entities.NotificationPriority `json:"priority,omitempty" validate:"omitempty,oneof=low normal high"`
|
||||
ImageURL string `json:"image_url,omitempty" validate:"omitempty,max=512"`
|
||||
ActionURL string `json:"action_url,omitempty" validate:"omitempty,max=512"`
|
||||
NotifiableType string `json:"notifiable_type,omitempty" validate:"omitempty,max=100"`
|
||||
NotifiableID *uuid.UUID `json:"notifiable_id,omitempty"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
ReceiverIDs []uuid.UUID `json:"receiver_ids" validate:"required,min=1"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
|
||||
ExpiredAt *time.Time `json:"expired_at,omitempty"`
|
||||
}
|
||||
|
||||
type BroadcastNotificationRequest struct {
|
||||
Title string `json:"title" validate:"required,min=1,max=255"`
|
||||
Body string `json:"body" validate:"required"`
|
||||
Type string `json:"type,omitempty" validate:"omitempty,max=100"`
|
||||
Category string `json:"category,omitempty" validate:"omitempty,max=100"`
|
||||
Priority entities.NotificationPriority `json:"priority,omitempty" validate:"omitempty,oneof=low normal high"`
|
||||
ImageURL string `json:"image_url,omitempty" validate:"omitempty,max=512"`
|
||||
ActionURL string `json:"action_url,omitempty" validate:"omitempty,max=512"`
|
||||
NotifiableType string `json:"notifiable_type,omitempty" validate:"omitempty,max=100"`
|
||||
NotifiableID *uuid.UUID `json:"notifiable_id,omitempty"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
|
||||
ExpiredAt *time.Time `json:"expired_at,omitempty"`
|
||||
}
|
||||
|
||||
type ListNotificationsRequest struct {
|
||||
Page int `form:"page" validate:"min=1"`
|
||||
Limit int `form:"limit" validate:"min=1,max=100"`
|
||||
IsRead *bool `form:"is_read"`
|
||||
}
|
||||
|
||||
// ---- Response contracts ----
|
||||
|
||||
type NotificationResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Type string `json:"type"`
|
||||
Category string `json:"category"`
|
||||
Priority entities.NotificationPriority `json:"priority"`
|
||||
ImageURL string `json:"image_url"`
|
||||
ActionURL string `json:"action_url"`
|
||||
NotifiableType string `json:"notifiable_type"`
|
||||
NotifiableID *uuid.UUID `json:"notifiable_id"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at"`
|
||||
SentAt *time.Time `json:"sent_at"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
CreatedBy *uuid.UUID `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type NotificationReceiverResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
NotificationID uuid.UUID `json:"notification_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
IsRead bool `json:"is_read"`
|
||||
ReadAt *time.Time `json:"read_at"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
DeletedAt *time.Time `json:"deleted_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Notification *NotificationResponse `json:"notification,omitempty"`
|
||||
}
|
||||
|
||||
type ListNotificationsResponse struct {
|
||||
Notifications []*NotificationReceiverResponse `json:"notifications"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
UnreadCount int64 `json:"unread_count"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
@ -98,8 +98,6 @@ type OrderItemResponse struct {
|
||||
ProductName string `json:"product_name"`
|
||||
ProductVariantID *uuid.UUID `json:"product_variant_id"`
|
||||
ProductVariantName *string `json:"product_variant_name,omitempty"`
|
||||
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
||||
CategoryName *string `json:"category_name,omitempty"`
|
||||
Quantity int `json:"quantity"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
TotalPrice float64 `json:"total_price"`
|
||||
@ -110,7 +108,6 @@ type OrderItemResponse struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PrinterType string `json:"printer_type"`
|
||||
PrintToChecker bool `json:"print_to_checker"`
|
||||
PaidQuantity int `json:"paid_quantity"`
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
|
||||
type CreateProductRequest struct {
|
||||
CategoryID uuid.UUID `json:"category_id" validate:"required"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
SKU *string `json:"sku,omitempty"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
@ -17,30 +16,28 @@ type CreateProductRequest struct {
|
||||
BusinessType *string `json:"business_type,omitempty"`
|
||||
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
|
||||
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
|
||||
PrintToChecker *bool `json:"print_to_checker,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
IsActive *bool `json:"is_active,omitempty"`
|
||||
Variants []CreateProductVariantRequest `json:"variants,omitempty"`
|
||||
InitialStock *int `json:"initial_stock,omitempty" validate:"omitempty,min=0"`
|
||||
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"`
|
||||
CreateInventory bool `json:"create_inventory,omitempty"`
|
||||
InitialStock *int `json:"initial_stock,omitempty" validate:"omitempty,min=0"` // Initial stock quantity for all outlets
|
||||
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"` // Reorder level for all outlets
|
||||
CreateInventory bool `json:"create_inventory,omitempty"` // Whether to create inventory records for all outlets
|
||||
}
|
||||
|
||||
type UpdateProductRequest struct {
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
||||
SKU *string `json:"sku,omitempty"`
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Price *float64 `json:"price,omitempty" validate:"omitempty,min=0"`
|
||||
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
|
||||
BusinessType *string `json:"business_type,omitempty"`
|
||||
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
|
||||
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
|
||||
PrintToChecker *bool `json:"print_to_checker,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
IsActive *bool `json:"is_active,omitempty"`
|
||||
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"`
|
||||
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
||||
SKU *string `json:"sku,omitempty"`
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Price *float64 `json:"price,omitempty" validate:"omitempty,min=0"`
|
||||
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
|
||||
BusinessType *string `json:"business_type,omitempty"`
|
||||
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
|
||||
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
IsActive *bool `json:"is_active,omitempty"`
|
||||
// Stock management fields
|
||||
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"` // Update reorder level for all existing inventory records
|
||||
}
|
||||
|
||||
type CreateProductVariantRequest struct {
|
||||
@ -59,27 +56,24 @@ type UpdateProductVariantRequest struct {
|
||||
}
|
||||
|
||||
type ProductResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
CategoryID uuid.UUID `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
SKU *string `json:"sku"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Price float64 `json:"price"`
|
||||
OutletPrice *float64 `json:"outlet_price,omitempty"`
|
||||
OutletPrices []ProductOutletPriceResponse `json:"outlet_prices,omitempty"`
|
||||
Cost float64 `json:"cost"`
|
||||
BusinessType string `json:"business_type"`
|
||||
ImageURL *string `json:"image_url"`
|
||||
PrinterType string `json:"printer_type"`
|
||||
PrintToChecker bool `json:"print_to_checker"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Category *CategoryResponse `json:"category,omitempty"`
|
||||
Variants []ProductVariantResponse `json:"variants,omitempty"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
CategoryID uuid.UUID `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
SKU *string `json:"sku"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Price float64 `json:"price"`
|
||||
Cost float64 `json:"cost"`
|
||||
BusinessType string `json:"business_type"`
|
||||
ImageURL *string `json:"image_url"`
|
||||
PrinterType string `json:"printer_type"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Category *CategoryResponse `json:"category,omitempty"`
|
||||
Variants []ProductVariantResponse `json:"variants,omitempty"`
|
||||
}
|
||||
|
||||
type ProductVariantResponse struct {
|
||||
@ -95,7 +89,6 @@ type ProductVariantResponse struct {
|
||||
|
||||
type ListProductsRequest struct {
|
||||
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
||||
BusinessType string `json:"business_type,omitempty"`
|
||||
IsActive *bool `json:"is_active,omitempty"`
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CreateProductOutletPriceRequest struct {
|
||||
ProductID uuid.UUID `json:"product_id" validate:"required"`
|
||||
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
||||
Price float64 `json:"price" validate:"required,min=0"`
|
||||
PrintToChecker bool `json:"print_to_checker"`
|
||||
}
|
||||
|
||||
type UpdateProductOutletPriceRequest struct {
|
||||
Price float64 `json:"price" validate:"required,min=0"`
|
||||
PrintToChecker *bool `json:"print_to_checker"`
|
||||
}
|
||||
|
||||
type ProductOutletPriceResponse struct {
|
||||
ID uuid.UUID `json:"id,omitempty"`
|
||||
ProductID uuid.UUID `json:"product_id,omitempty"`
|
||||
OutletID uuid.UUID `json:"outlet_id"`
|
||||
OutletName string `json:"outlet_name,omitempty"`
|
||||
Price float64 `json:"price"`
|
||||
PrintToChecker bool `json:"print_to_checker"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
type ListProductOutletPricesResponse struct {
|
||||
Prices []ProductOutletPriceResponse `json:"prices"`
|
||||
TotalCount int `json:"total_count"`
|
||||
}
|
||||
|
||||
type BulkCreateProductOutletPriceRequest struct {
|
||||
ProductID uuid.UUID `json:"product_id" validate:"required"`
|
||||
Prices []CreateProductOutletPricePerOutletRequest `json:"prices" validate:"required,dive"`
|
||||
}
|
||||
|
||||
type CreateProductOutletPricePerOutletRequest struct {
|
||||
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
||||
Price float64 `json:"price" validate:"required,min=0"`
|
||||
PrintToChecker bool `json:"print_to_checker"`
|
||||
}
|
||||
@ -34,34 +34,34 @@ type BulkCreateProductRecipeRequest struct {
|
||||
|
||||
// Response structures
|
||||
type ProductRecipeResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
VariantID *uuid.UUID `json:"variant_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Product *ProductResponse `json:"product,omitempty"`
|
||||
ProductVariant *ProductVariantResponse `json:"product_variant,omitempty"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
VariantID *uuid.UUID `json:"variant_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Product *ProductResponse `json:"product,omitempty"`
|
||||
ProductVariant *ProductVariantResponse `json:"product_variant,omitempty"`
|
||||
Ingredient *ProductRecipeIngredientResponse `json:"ingredient,omitempty"`
|
||||
}
|
||||
|
||||
type ProductRecipeIngredientResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Name string `json:"name"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
Cost float64 `json:"cost"`
|
||||
Stock float64 `json:"stock"`
|
||||
IsSemiFinished bool `json:"is_semi_finished"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Name string `json:"name"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
Cost float64 `json:"cost"`
|
||||
Stock float64 `json:"stock"`
|
||||
IsSemiFinished bool `json:"is_semi_finished"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Unit *ProductRecipeUnitResponse `json:"unit,omitempty"`
|
||||
}
|
||||
|
||||
@ -71,4 +71,4 @@ type ProductRecipeUnitResponse struct {
|
||||
Symbol string `json:"symbol"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CreatePurchaseCategoryRequest struct {
|
||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||
Code *string `json:"code,omitempty"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Type string `json:"type" validate:"required,oneof=raw_material expense"`
|
||||
SortOrder *int `json:"sort_order,omitempty"`
|
||||
IsActive *bool `json:"is_active,omitempty"`
|
||||
}
|
||||
|
||||
type UpdatePurchaseCategoryRequest struct {
|
||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||
Code *string `json:"code,omitempty"`
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||
Type *string `json:"type,omitempty" validate:"omitempty,oneof=raw_material expense"`
|
||||
SortOrder *int `json:"sort_order,omitempty"`
|
||||
IsActive *bool `json:"is_active,omitempty"`
|
||||
}
|
||||
|
||||
type ListPurchaseCategoriesRequest struct {
|
||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||
Type string `json:"type,omitempty" validate:"omitempty,oneof=raw_material expense"`
|
||||
Search string `json:"search,omitempty"`
|
||||
IsActive *bool `json:"is_active,omitempty"`
|
||||
Page int `json:"page" validate:"required,min=1"`
|
||||
Limit int `json:"limit" validate:"required,min=1,max=100"`
|
||||
}
|
||||
|
||||
type PurchaseCategoryResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
PresetID *uuid.UUID `json:"preset_id"`
|
||||
ParentID *uuid.UUID `json:"parent_id"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
IsSystem bool `json:"is_system"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ListPurchaseCategoriesResponse struct {
|
||||
PurchaseCategories []PurchaseCategoryResponse `json:"purchase_categories"`
|
||||
TotalCount int `json:"total_count"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
@ -7,10 +7,10 @@ import (
|
||||
)
|
||||
|
||||
type CreatePurchaseOrderRequest struct {
|
||||
VendorID *uuid.UUID `json:"vendor_id,omitempty" validate:"omitempty"`
|
||||
VendorID uuid.UUID `json:"vendor_id" validate:"required"`
|
||||
PONumber string `json:"po_number" validate:"required,min=1,max=50"`
|
||||
TransactionDate string `json:"transaction_date" validate:"required"` // Format: YYYY-MM-DD
|
||||
DueDate *string `json:"due_date,omitempty" validate:"omitempty"` // Format: YYYY-MM-DD
|
||||
TransactionDate string `json:"transaction_date" validate:"required"` // Format: YYYY-MM-DD
|
||||
DueDate string `json:"due_date" validate:"required"` // Format: YYYY-MM-DD
|
||||
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"`
|
||||
@ -19,19 +19,18 @@ type CreatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type CreatePurchaseOrderItemRequest struct {
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id" validate:"required"`
|
||||
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" validate:"required,gte=0"`
|
||||
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 *string `json:"transaction_date,omitempty" validate:"omitempty"` // Format: YYYY-MM-DD
|
||||
DueDate *string `json:"due_date,omitempty" validate:"omitempty"` // Format: YYYY-MM-DD
|
||||
DueDate *string `json:"due_date,omitempty" validate:"omitempty"` // Format: YYYY-MM-DD
|
||||
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"`
|
||||
@ -40,23 +39,21 @@ type UpdatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type UpdatePurchaseOrderItemRequest struct {
|
||||
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
||||
PurchaseCategoryID *uuid.UUID `json:"purchase_category_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"`
|
||||
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"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
VendorID *uuid.UUID `json:"vendor_id"`
|
||||
VendorID uuid.UUID `json:"vendor_id"`
|
||||
PONumber string `json:"po_number"`
|
||||
TransactionDate time.Time `json:"transaction_date"`
|
||||
DueDate *time.Time `json:"due_date"`
|
||||
DueDate time.Time `json:"due_date"`
|
||||
Reference *string `json:"reference"`
|
||||
Status string `json:"status"`
|
||||
Message *string `json:"message"`
|
||||
@ -69,19 +66,17 @@ type PurchaseOrderResponse struct {
|
||||
}
|
||||
|
||||
type PurchaseOrderItemResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||
IngredientID *uuid.UUID `json:"ingredient_id"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_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"`
|
||||
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||
Unit *UnitResponse `json:"unit,omitempty"`
|
||||
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 {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@ -48,10 +50,8 @@ type SelfOrderMenuVariant struct {
|
||||
}
|
||||
|
||||
type SelfOrderCreateOrderRequest struct {
|
||||
SessionID string `json:"session_id" validate:"required"`
|
||||
CustomerName string `json:"customer_name" validate:"required"`
|
||||
OrderType string `json:"order_type" validate:"required,oneof=dine_in takeaway delivery"`
|
||||
OrderItems []SelfOrderCreateOrderItem `json:"order_items" validate:"required,min=1,dive"`
|
||||
SessionID string `json:"session_id" validate:"required"`
|
||||
OrderItems []SelfOrderCreateOrderItem `json:"order_items" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type SelfOrderCreateOrderItem struct {
|
||||
@ -62,7 +62,7 @@ type SelfOrderCreateOrderItem struct {
|
||||
}
|
||||
|
||||
type SelfOrderListCategoriesRequest struct {
|
||||
OrganizationID string `form:"organization_id" validate:"required"`
|
||||
OrganizationID string `form:"organisasi_id" validate:"required"`
|
||||
OutletID string `form:"outlet_id" validate:"required"`
|
||||
}
|
||||
|
||||
@ -78,5 +78,35 @@ type SelfOrderListCategoriesResponse struct {
|
||||
}
|
||||
|
||||
type SelfOrderListOrdersResponse struct {
|
||||
Orders []OrderResponse `json:"orders"`
|
||||
Orders []SelfOrderOrderItem `json:"orders"`
|
||||
}
|
||||
|
||||
type SelfOrderOrderItem struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrderNumber string `json:"order_number"`
|
||||
TableNumber *string `json:"table_number,omitempty"`
|
||||
OrderType string `json:"order_type"`
|
||||
Status string `json:"status"`
|
||||
Subtotal float64 `json:"subtotal"`
|
||||
TaxAmount float64 `json:"tax_amount"`
|
||||
DiscountAmount float64 `json:"discount_amount"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
RemainingAmount float64 `json:"remaining_amount"`
|
||||
PaymentStatus string `json:"payment_status"`
|
||||
IsVoid bool `json:"is_void"`
|
||||
IsRefund bool `json:"is_refund"`
|
||||
Items []SelfOrderOrderLineItem `json:"items,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type SelfOrderOrderLineItem struct {
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"`
|
||||
ProductVariantNam *string `json:"product_variant_name,omitempty"`
|
||||
Quantity int `json:"quantity"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
TotalPrice float64 `json:"total_price"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
@ -12,14 +12,14 @@ type CreateUserRequest struct {
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=6"`
|
||||
Role string `json:"role" validate:"required,oneof=admin manager cashier waiter owner purchasing"`
|
||||
Role string `json:"role" validate:"required,oneof=admin manager cashier waiter"`
|
||||
Permissions map[string]interface{} `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateUserRequest struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||
Email *string `json:"email,omitempty" validate:"omitempty,email"`
|
||||
Role *string `json:"role,omitempty" validate:"omitempty,oneof=admin manager cashier waiter owner purchasing"`
|
||||
Role *string `json:"role,omitempty" validate:"omitempty,oneof=admin manager cashier waiter"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
IsActive *bool `json:"is_active,omitempty"`
|
||||
Permissions *map[string]interface{} `json:"permissions,omitempty"`
|
||||
@ -35,15 +35,9 @@ type UpdateUserOutletRequest struct {
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
DeviceID string `json:"device_id,omitempty"`
|
||||
DeviceName string `json:"device_name,omitempty"`
|
||||
DeviceType string `json:"device_type,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
FCMToken string `json:"fcm_token,omitempty"`
|
||||
AppVersion string `json:"app_version,omitempty"`
|
||||
OsVersion string `json:"os_version,omitempty"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
FcmToken *string `json:"fcm_token,omitempty"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type RegisterUserDeviceRequest struct {
|
||||
DeviceID string `json:"device_id" validate:"required,min=1,max=255"`
|
||||
DeviceName string `json:"device_name,omitempty" validate:"omitempty,max=255"`
|
||||
DeviceType entities.DeviceType `json:"device_type,omitempty" validate:"omitempty,oneof=mobile tablet desktop"`
|
||||
Platform entities.DevicePlatform `json:"platform,omitempty" validate:"omitempty,oneof=android ios web"`
|
||||
FCMToken string `json:"fcm_token,omitempty" validate:"omitempty,max=512"`
|
||||
AppVersion string `json:"app_version,omitempty" validate:"omitempty,max=50"`
|
||||
OsVersion string `json:"os_version,omitempty" validate:"omitempty,max=50"`
|
||||
}
|
||||
|
||||
type UpdateUserDeviceRequest struct {
|
||||
DeviceName string `json:"device_name,omitempty" validate:"omitempty,max=255"`
|
||||
DeviceType entities.DeviceType `json:"device_type,omitempty" validate:"omitempty,oneof=mobile tablet desktop"`
|
||||
Platform entities.DevicePlatform `json:"platform,omitempty" validate:"omitempty,oneof=android ios web"`
|
||||
FCMToken string `json:"fcm_token,omitempty" validate:"omitempty,max=512"`
|
||||
AppVersion string `json:"app_version,omitempty" validate:"omitempty,max=50"`
|
||||
OsVersion string `json:"os_version,omitempty" validate:"omitempty,max=50"`
|
||||
}
|
||||
|
||||
type UserDeviceResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceType entities.DeviceType `json:"device_type"`
|
||||
Platform entities.DevicePlatform `json:"platform"`
|
||||
FCMToken string `json:"fcm_token"`
|
||||
AppVersion string `json:"app_version"`
|
||||
OsVersion string `json:"os_version"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
LastActiveAt *time.Time `json:"last_active_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ListUserDevicesRequest struct {
|
||||
Page int `json:"page" validate:"min=1"`
|
||||
Limit int `json:"limit" validate:"min=1,max=100"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
}
|
||||
|
||||
type ListUserDevicesResponse struct {
|
||||
Devices []UserDeviceResponse `json:"devices"`
|
||||
TotalCount int `json:"total_count"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
@ -27,64 +27,10 @@ type SalesAnalytics struct {
|
||||
NetSales float64 `json:"net_sales"`
|
||||
}
|
||||
|
||||
// PurchasingAnalytics represents purchasing analytics data
|
||||
type PurchasingAnalytics struct {
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
Summary PurchasingSummary `json:"summary"`
|
||||
Data []PurchasingAnalyticsData `json:"data"`
|
||||
IngredientData []PurchasingIngredientData `json:"ingredient_data"`
|
||||
VendorData []PurchasingVendorData `json:"vendor_data"`
|
||||
}
|
||||
|
||||
type PurchasingSummary struct {
|
||||
TotalPurchases float64 `json:"total_purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
TotalQuantity float64 `json:"total_quantity"`
|
||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||
TotalIngredients int64 `json:"total_ingredients"`
|
||||
TotalVendors int64 `json:"total_vendors"`
|
||||
}
|
||||
|
||||
type PurchasingAnalyticsData struct {
|
||||
Date time.Time `json:"date"`
|
||||
Purchases float64 `json:"purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
PurchaseOrders int64 `json:"purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Ingredients int64 `json:"ingredients"`
|
||||
Vendors int64 `json:"vendors"`
|
||||
}
|
||||
|
||||
type PurchasingIngredientData struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
IngredientName string `json:"ingredient_name"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
AverageUnitCost float64 `json:"average_unit_cost"`
|
||||
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||
}
|
||||
|
||||
type PurchasingVendorData struct {
|
||||
VendorID *uuid.UUID `json:"vendor_id"`
|
||||
VendorName string `json:"vendor_name"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||
IngredientCount int64 `json:"ingredient_count"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
}
|
||||
|
||||
type ProductAnalytics struct {
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
ProductSku string `json:"product_sku"`
|
||||
ProductPrice float64 `json:"product_price"`
|
||||
CategoryID uuid.UUID `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
CategoryOrder int `json:"category_order"`
|
||||
@ -120,125 +66,56 @@ type DashboardOverview struct {
|
||||
TotalCustomers int64 `json:"total_customers"`
|
||||
VoidedOrders int64 `json:"voided_orders"`
|
||||
RefundedOrders int64 `json:"refunded_orders"`
|
||||
TotalItemSold int64 `json:"total_item_sold"`
|
||||
TotalLowStock int64 `json:"total_low_stock"`
|
||||
TotalProductActive int64 `json:"total_product_active"`
|
||||
}
|
||||
|
||||
// ProfitLossAnalytics represents profit and loss analytics data
|
||||
type ProfitLossAnalytics struct {
|
||||
Summary ProfitLossSummary
|
||||
Data []ProfitLossData
|
||||
ProductData []ProductProfitData
|
||||
TodayRevenue float64
|
||||
TodayCost float64
|
||||
MtdRevenue float64
|
||||
MtdCost float64
|
||||
TodayPurchasing float64
|
||||
MtdPurchasing float64
|
||||
TodayPurchasingRawMaterial float64
|
||||
MtdPurchasingRawMaterial float64
|
||||
TodayPurchasingExpense float64
|
||||
MtdPurchasingExpense float64
|
||||
PurchasingItems []PurchasingItemDetail
|
||||
TodayExpenseByCategory []ExpenseCategoryTotal
|
||||
MtdExpenseByCategory []ExpenseCategoryTotal
|
||||
OperationalExpenseItems []OperationalExpenseItem
|
||||
}
|
||||
|
||||
type PurchasingItemDetail struct {
|
||||
Date time.Time
|
||||
Item string
|
||||
Quantity float64
|
||||
Amount float64
|
||||
Summary ProfitLossSummary `json:"summary"`
|
||||
Data []ProfitLossData `json:"data"`
|
||||
ProductData []ProductProfitData `json:"product_data"`
|
||||
}
|
||||
|
||||
// ProfitLossSummary represents profit and loss summary data
|
||||
type ProfitLossSummary struct {
|
||||
TotalRevenue float64
|
||||
TotalCost float64
|
||||
GrossProfit float64
|
||||
GrossProfitMargin float64
|
||||
TotalTax float64
|
||||
TotalDiscount float64
|
||||
NetProfit float64
|
||||
NetProfitMargin float64
|
||||
TotalOrders int64
|
||||
AverageProfit float64
|
||||
ProfitabilityRatio float64
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
GrossProfit float64 `json:"gross_profit"`
|
||||
GrossProfitMargin float64 `json:"gross_profit_margin"`
|
||||
TotalTax float64 `json:"total_tax"`
|
||||
TotalDiscount float64 `json:"total_discount"`
|
||||
NetProfit float64 `json:"net_profit"`
|
||||
NetProfitMargin float64 `json:"net_profit_margin"`
|
||||
TotalOrders int64 `json:"total_orders"`
|
||||
AverageProfit float64 `json:"average_profit"`
|
||||
ProfitabilityRatio float64 `json:"profitability_ratio"`
|
||||
}
|
||||
|
||||
// ProfitLossData represents profit and loss data by time period
|
||||
type ProfitLossData struct {
|
||||
Date time.Time
|
||||
Revenue float64
|
||||
Cost float64
|
||||
GrossProfit float64
|
||||
GrossProfitMargin float64
|
||||
Tax float64
|
||||
Discount float64
|
||||
NetProfit float64
|
||||
NetProfitMargin float64
|
||||
Orders int64
|
||||
Date time.Time `json:"date"`
|
||||
Revenue float64 `json:"revenue"`
|
||||
Cost float64 `json:"cost"`
|
||||
GrossProfit float64 `json:"gross_profit"`
|
||||
GrossProfitMargin float64 `json:"gross_profit_margin"`
|
||||
Tax float64 `json:"tax"`
|
||||
Discount float64 `json:"discount"`
|
||||
NetProfit float64 `json:"net_profit"`
|
||||
NetProfitMargin float64 `json:"net_profit_margin"`
|
||||
Orders int64 `json:"orders"`
|
||||
}
|
||||
|
||||
// ProductProfitData represents profit data for individual products
|
||||
type ProductProfitData struct {
|
||||
ProductID uuid.UUID
|
||||
ProductName string
|
||||
CategoryID uuid.UUID
|
||||
CategoryName string
|
||||
QuantitySold int64
|
||||
Revenue float64
|
||||
Cost float64
|
||||
GrossProfit float64
|
||||
GrossProfitMargin float64
|
||||
AveragePrice float64
|
||||
AverageCost float64
|
||||
ProfitPerUnit float64
|
||||
}
|
||||
|
||||
type ExpenseCategoryTotal struct {
|
||||
CategoryName string
|
||||
Amount float64
|
||||
}
|
||||
|
||||
type OperationalExpenseItem struct {
|
||||
Item string
|
||||
Amount float64
|
||||
}
|
||||
|
||||
type ExclusiveSummaryAnalytics struct {
|
||||
SalesTotal float64
|
||||
SalesCount int64
|
||||
HPPBreakdown []ExclusiveSummaryCategoryTotal
|
||||
OperationalExpenseBreakdown []ExclusiveSummaryCategoryTotal
|
||||
DailySummary []ExclusiveSummaryDailySummary
|
||||
DailyTransactions []ExclusiveSummaryDailyTransaction
|
||||
}
|
||||
|
||||
type ExclusiveSummaryCategoryTotal struct {
|
||||
CategoryCode string
|
||||
CategoryName string
|
||||
Amount float64
|
||||
}
|
||||
|
||||
type ExclusiveSummaryDailySummary struct {
|
||||
Date time.Time
|
||||
TransactionCount int64
|
||||
TotalCost float64
|
||||
}
|
||||
|
||||
type ExclusiveSummaryDailyTransaction struct {
|
||||
Date time.Time
|
||||
CategoryCode string
|
||||
CategoryName string
|
||||
Description string
|
||||
Amount float64
|
||||
Source string
|
||||
}
|
||||
|
||||
type ExclusiveSummaryBankBalance struct {
|
||||
Bank string
|
||||
OpeningBalance *float64
|
||||
IncomingMutation *float64
|
||||
OutgoingMutation *float64
|
||||
ClosingBalance *float64
|
||||
Notes *string
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
CategoryID uuid.UUID `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
QuantitySold int64 `json:"quantity_sold"`
|
||||
Revenue float64 `json:"revenue"`
|
||||
Cost float64 `json:"cost"`
|
||||
GrossProfit float64 `json:"gross_profit"`
|
||||
GrossProfitMargin float64 `json:"gross_profit_margin"`
|
||||
AveragePrice float64 `json:"average_price"`
|
||||
AverageCost float64 `json:"average_cost"`
|
||||
ProfitPerUnit float64 `json:"profit_per_unit"`
|
||||
}
|
||||
|
||||
@ -31,16 +31,15 @@ func (m *Metadata) Scan(value interface{}) error {
|
||||
}
|
||||
|
||||
type Category 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"`
|
||||
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
||||
Description *string `gorm:"type:text" json:"description"`
|
||||
Order int `gorm:"default:0" json:"order"`
|
||||
BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"`
|
||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
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"`
|
||||
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
||||
Description *string `gorm:"type:text" json:"description"`
|
||||
Order int `gorm:"default:0" json:"order"`
|
||||
BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"`
|
||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Products []Product `gorm:"foreignKey:CategoryID" json:"products,omitempty"`
|
||||
|
||||
@ -36,13 +36,6 @@ func GetAllEntities() []interface{} {
|
||||
&CampaignRule{},
|
||||
&OtpSession{},
|
||||
// Analytics entities are not database tables, they are query results
|
||||
&UserDevice{},
|
||||
// Notification entities
|
||||
&Notification{},
|
||||
&NotificationReceiver{},
|
||||
&NotificationDelivery{},
|
||||
&ProductOutletPrice{},
|
||||
&Expense{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,90 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Expense 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"`
|
||||
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id"`
|
||||
Receiver string `gorm:"not null;size:255" json:"receiver"`
|
||||
TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date"`
|
||||
CodeNumber string `gorm:"not null;size:50" json:"code_number"`
|
||||
Status string `gorm:"not null;size:20;default:'draft'" json:"status"`
|
||||
Description *string `gorm:"type:text" json:"description"`
|
||||
Tax float64 `gorm:"type:decimal(15,2);not null;default:0" json:"tax"`
|
||||
Total float64 `gorm:"type:decimal(15,2);not null;default:0" json:"total"`
|
||||
Reserved1 *string `gorm:"type:text" json:"reserved1"`
|
||||
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"`
|
||||
Items []ExpenseItem `gorm:"foreignKey:ExpenseID" json:"items,omitempty"`
|
||||
}
|
||||
|
||||
type ExpenseAnalytics struct {
|
||||
Summary ExpenseAnalyticsSummary
|
||||
Data []ExpenseAnalyticsData
|
||||
CategoryData []ExpenseAnalyticsCategoryData
|
||||
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData
|
||||
ItemData []ExpenseAnalyticsItemData
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsSummary struct {
|
||||
TotalExpenses float64
|
||||
TotalExpenseCount int64
|
||||
TotalTax float64
|
||||
AverageExpenseValue float64
|
||||
TotalCategories int64
|
||||
TotalItems int64
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsData struct {
|
||||
Date time.Time
|
||||
Expenses float64
|
||||
ExpenseCount int64
|
||||
Tax float64
|
||||
Items int64
|
||||
Categories int64
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsCategoryData struct {
|
||||
PurchaseCategoryID uuid.UUID
|
||||
PurchaseCategoryName string
|
||||
PurchaseCategoryType string
|
||||
TotalAmount float64
|
||||
ExpenseCount int64
|
||||
ItemCount int64
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsChartOfAccountData struct {
|
||||
ChartOfAccountID uuid.UUID
|
||||
ChartOfAccountName string
|
||||
TotalAmount float64
|
||||
ExpenseCount int64
|
||||
ItemCount int64
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsItemData struct {
|
||||
Item string
|
||||
TotalAmount float64
|
||||
ExpenseCount int64
|
||||
ItemCount int64
|
||||
}
|
||||
|
||||
func (e *Expense) BeforeCreate(tx *gorm.DB) error {
|
||||
if e.ID == uuid.Nil {
|
||||
e.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Expense) TableName() string {
|
||||
return "expenses"
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ExpenseItem struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
ExpenseID uuid.UUID `gorm:"type:uuid;not null;index" json:"expense_id"`
|
||||
ChartOfAccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"chart_of_account_id"`
|
||||
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id"`
|
||||
Item string `gorm:"not null;size:255" json:"item"`
|
||||
Description *string `gorm:"type:text" json:"description"`
|
||||
Amount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"amount"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
Expense *Expense `gorm:"foreignKey:ExpenseID" json:"expense,omitempty"`
|
||||
ChartOfAccount *ChartOfAccount `gorm:"foreignKey:ChartOfAccountID" json:"chart_of_account,omitempty"`
|
||||
PurchaseCategory *PurchaseCategory `gorm:"foreignKey:PurchaseCategoryID" json:"purchase_category,omitempty"`
|
||||
}
|
||||
|
||||
func (e *ExpenseItem) BeforeCreate(tx *gorm.DB) error {
|
||||
if e.ID == uuid.Nil {
|
||||
e.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ExpenseItem) TableName() string {
|
||||
return "expense_items"
|
||||
}
|
||||
@ -39,3 +39,4 @@ func (iuc *IngredientUnitConverter) BeforeCreate() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -36,36 +36,34 @@ const (
|
||||
)
|
||||
|
||||
type InventoryMovement 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;not null;index" json:"outlet_id" validate:"required"`
|
||||
ItemID uuid.UUID `gorm:"type:uuid;not null;index" json:"item_id" validate:"required"`
|
||||
ItemType string `gorm:"not null;size:20" json:"item_type" validate:"required"` // "PRODUCT" or "INGREDIENT"
|
||||
MovementType InventoryMovementType `gorm:"not null;size:50" json:"movement_type" validate:"required"`
|
||||
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity" validate:"required"`
|
||||
PreviousQuantity float64 `gorm:"type:decimal(12,3)" json:"previous_quantity"`
|
||||
NewQuantity float64 `gorm:"type:decimal(12,3)" json:"new_quantity"`
|
||||
UnitCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"unit_cost"`
|
||||
TotalCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"total_cost"`
|
||||
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
|
||||
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
|
||||
PurchaseOrderItemID *uuid.UUID `gorm:"type:uuid;index" json:"purchase_order_item_id"`
|
||||
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
|
||||
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
|
||||
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
|
||||
Reason *string `gorm:"size:255" json:"reason"`
|
||||
Notes *string `gorm:"type:text" json:"notes"`
|
||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
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;not null;index" json:"outlet_id" validate:"required"`
|
||||
ItemID uuid.UUID `gorm:"type:uuid;not null;index" json:"item_id" validate:"required"`
|
||||
ItemType string `gorm:"not null;size:20" json:"item_type" validate:"required"` // "PRODUCT" or "INGREDIENT"
|
||||
MovementType InventoryMovementType `gorm:"not null;size:50" json:"movement_type" validate:"required"`
|
||||
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity" validate:"required"`
|
||||
PreviousQuantity float64 `gorm:"type:decimal(12,3)" json:"previous_quantity"`
|
||||
NewQuantity float64 `gorm:"type:decimal(12,3)" json:"new_quantity"`
|
||||
UnitCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"unit_cost"`
|
||||
TotalCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"total_cost"`
|
||||
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
|
||||
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
|
||||
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
|
||||
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
|
||||
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
|
||||
Reason *string `gorm:"size:255" json:"reason"`
|
||||
Notes *string `gorm:"type:text" json:"notes"`
|
||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||
Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"`
|
||||
Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,omitempty"`
|
||||
PurchaseOrderItem *PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderItemID" json:"purchase_order_item,omitempty"`
|
||||
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||
Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"`
|
||||
Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,omitempty"`
|
||||
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
func (im *InventoryMovement) BeforeCreate(tx *gorm.DB) error {
|
||||
|
||||
@ -1,150 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type NotificationPriority string
|
||||
type NotificationDeliveryStatus string
|
||||
type NotificationChannel string
|
||||
type NotificationProvider string
|
||||
|
||||
const (
|
||||
NotificationPriorityLow NotificationPriority = "low"
|
||||
NotificationPriorityNormal NotificationPriority = "normal"
|
||||
NotificationPriorityHigh NotificationPriority = "high"
|
||||
|
||||
NotificationDeliveryStatusPending NotificationDeliveryStatus = "pending"
|
||||
NotificationDeliveryStatusSent NotificationDeliveryStatus = "sent"
|
||||
NotificationDeliveryStatusDelivered NotificationDeliveryStatus = "delivered"
|
||||
NotificationDeliveryStatusFailed NotificationDeliveryStatus = "failed"
|
||||
|
||||
NotificationChannelPush NotificationChannel = "push"
|
||||
NotificationChannelWebsocket NotificationChannel = "websocket"
|
||||
NotificationChannelEmail NotificationChannel = "email"
|
||||
|
||||
NotificationProviderFirebase NotificationProvider = "firebase"
|
||||
)
|
||||
|
||||
// NotificationData is a JSON-serializable map for extra notification payload.
|
||||
type NotificationData map[string]interface{}
|
||||
|
||||
func (d NotificationData) Value() (driver.Value, error) {
|
||||
if d == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return json.Marshal(d)
|
||||
}
|
||||
|
||||
func (d *NotificationData) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*d = nil
|
||||
return nil
|
||||
}
|
||||
bytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.New("type assertion to []byte failed")
|
||||
}
|
||||
return json.Unmarshal(bytes, d)
|
||||
}
|
||||
|
||||
// Notification is the master notification record.
|
||||
type Notification struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
Title string `gorm:"not null;size:255" json:"title"`
|
||||
Body string `gorm:"type:text" json:"body"`
|
||||
Type string `gorm:"size:100" json:"type"`
|
||||
Category string `gorm:"size:100" json:"category"`
|
||||
Priority NotificationPriority `gorm:"size:50;default:'normal'" json:"priority"`
|
||||
ImageURL string `gorm:"size:512" json:"image_url"`
|
||||
ActionURL string `gorm:"size:512" json:"action_url"`
|
||||
NotifiableType string `gorm:"size:100" json:"notifiable_type"`
|
||||
NotifiableID *uuid.UUID `gorm:"type:uuid" json:"notifiable_id"`
|
||||
Data NotificationData `gorm:"type:jsonb" json:"data"`
|
||||
ScheduledAt *time.Time `gorm:"type:timestamptz" json:"scheduled_at"`
|
||||
SentAt *time.Time `gorm:"type:timestamptz" json:"sent_at"`
|
||||
ExpiredAt *time.Time `gorm:"type:timestamptz" json:"expired_at"`
|
||||
CreatedBy *uuid.UUID `gorm:"type:uuid" json:"created_by"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
Creator *User `gorm:"foreignKey:CreatedBy" json:"creator,omitempty"`
|
||||
Receivers []*NotificationReceiver `gorm:"foreignKey:NotificationID" json:"receivers,omitempty"`
|
||||
}
|
||||
|
||||
func (n *Notification) BeforeCreate(tx *gorm.DB) error {
|
||||
if n.ID == uuid.Nil {
|
||||
n.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Notification) TableName() string {
|
||||
return "notifications"
|
||||
}
|
||||
|
||||
// NotificationReceiver links a notification to a specific user.
|
||||
type NotificationReceiver struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
NotificationID uuid.UUID `gorm:"type:uuid;not null;index" json:"notification_id"`
|
||||
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"`
|
||||
IsRead bool `gorm:"default:false" json:"is_read"`
|
||||
ReadAt *time.Time `gorm:"type:timestamptz" json:"read_at"`
|
||||
IsDeleted bool `gorm:"default:false" json:"is_deleted"`
|
||||
DeletedAt *time.Time `gorm:"type:timestamptz" json:"deleted_at"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
Notification *Notification `gorm:"foreignKey:NotificationID" json:"notification,omitempty"`
|
||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Deliveries []*NotificationDelivery `gorm:"foreignKey:NotificationReceiverID" json:"deliveries,omitempty"`
|
||||
}
|
||||
|
||||
func (n *NotificationReceiver) BeforeCreate(tx *gorm.DB) error {
|
||||
if n.ID == uuid.Nil {
|
||||
n.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NotificationReceiver) TableName() string {
|
||||
return "notification_receivers"
|
||||
}
|
||||
|
||||
// NotificationDelivery tracks per-device delivery attempts.
|
||||
type NotificationDelivery struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
NotificationReceiverID uuid.UUID `gorm:"type:uuid;not null;index" json:"notification_receiver_id"`
|
||||
UserDeviceID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_device_id"`
|
||||
Channel NotificationChannel `gorm:"size:50;default:'push'" json:"channel"`
|
||||
DeliveryStatus NotificationDeliveryStatus `gorm:"size:50;default:'pending'" json:"delivery_status"`
|
||||
Provider NotificationProvider `gorm:"size:50" json:"provider"`
|
||||
ProviderMessageID string `gorm:"size:255" json:"provider_message_id"`
|
||||
SentAt *time.Time `gorm:"type:timestamptz" json:"sent_at"`
|
||||
DeliveredAt *time.Time `gorm:"type:timestamptz" json:"delivered_at"`
|
||||
FailedAt *time.Time `gorm:"type:timestamptz" json:"failed_at"`
|
||||
FailureReason string `gorm:"type:text" json:"failure_reason"`
|
||||
RetryCount int `gorm:"default:0" json:"retry_count"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
NotificationReceiver *NotificationReceiver `gorm:"foreignKey:NotificationReceiverID" json:"notification_receiver,omitempty"`
|
||||
UserDevice *UserDevice `gorm:"foreignKey:UserDeviceID" json:"user_device,omitempty"`
|
||||
}
|
||||
|
||||
func (n *NotificationDelivery) BeforeCreate(tx *gorm.DB) error {
|
||||
if n.ID == uuid.Nil {
|
||||
n.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NotificationDelivery) TableName() string {
|
||||
return "notification_deliveries"
|
||||
}
|
||||
@ -26,14 +26,14 @@ type OrderIngredientTransaction struct {
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||
Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||
OrderItem *OrderItem `gorm:"foreignKey:OrderItemID" json:"order_item,omitempty"`
|
||||
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||
Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||
OrderItem *OrderItem `gorm:"foreignKey:OrderItemID" json:"order_item,omitempty"`
|
||||
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
||||
ProductVariant *ProductVariant `gorm:"foreignKey:ProductVariantID" json:"product_variant,omitempty"`
|
||||
Ingredient Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
||||
CreatedByUser User `gorm:"foreignKey:CreatedBy" json:"created_by_user,omitempty"`
|
||||
Ingredient Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
||||
CreatedByUser User `gorm:"foreignKey:CreatedBy" json:"created_by_user,omitempty"`
|
||||
}
|
||||
|
||||
func (oit *OrderIngredientTransaction) BeforeCreate(tx *gorm.DB) error {
|
||||
|
||||
@ -26,14 +26,13 @@ type Product struct {
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
||||
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
|
||||
ProductRecipes []ProductRecipe `gorm:"foreignKey:ProductID" json:"product_recipes,omitempty"`
|
||||
Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"`
|
||||
OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,omitempty"`
|
||||
ProductOutletPrices []ProductOutletPrice `gorm:"foreignKey:ProductID" json:"product_outlet_prices,omitempty"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
||||
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
|
||||
ProductRecipes []ProductRecipe `gorm:"foreignKey:ProductID" json:"product_recipes,omitempty"`
|
||||
Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"`
|
||||
OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,omitempty"`
|
||||
}
|
||||
|
||||
func (p *Product) BeforeCreate(tx *gorm.DB) error {
|
||||
|
||||
@ -7,15 +7,15 @@ import (
|
||||
)
|
||||
|
||||
type ProductIngredient struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id" db:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity" db:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage" db:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id" db:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity" db:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage" db:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Product *Product `json:"product,omitempty"`
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductOutletPrice struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id"`
|
||||
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id"`
|
||||
Price float64 `gorm:"type:decimal(10,2);not null" json:"price"`
|
||||
PrintToChecker bool `gorm:"not null;default:true" json:"print_to_checker"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
||||
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||
}
|
||||
|
||||
func (p *ProductOutletPrice) BeforeCreate(tx *gorm.DB) error {
|
||||
if p.ID == uuid.Nil {
|
||||
p.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ProductOutletPrice) TableName() string {
|
||||
return "product_outlet_prices"
|
||||
}
|
||||
@ -34,4 +34,4 @@ func (pr *ProductRecipe) BeforeCreate(tx *gorm.DB) error {
|
||||
|
||||
func (ProductRecipe) TableName() string {
|
||||
return "product_recipes"
|
||||
}
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PurchaseCategoryType string
|
||||
|
||||
const (
|
||||
PurchaseCategoryTypeRawMaterial PurchaseCategoryType = "raw_material"
|
||||
PurchaseCategoryTypeExpense PurchaseCategoryType = "expense"
|
||||
)
|
||||
|
||||
type PurchaseCategoryPreset struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
ParentID *uuid.UUID `gorm:"type:uuid;index" json:"parent_id"`
|
||||
Code string `gorm:"not null;unique;size:100" json:"code"`
|
||||
Name string `gorm:"not null;size:255" json:"name"`
|
||||
Type PurchaseCategoryType `gorm:"not null;size:20" json:"type"`
|
||||
SortOrder int `gorm:"not null;default:0" json:"sort_order"`
|
||||
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"`
|
||||
|
||||
Parent *PurchaseCategoryPreset `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||
}
|
||||
|
||||
func (p *PurchaseCategoryPreset) BeforeCreate(tx *gorm.DB) error {
|
||||
if p.ID == uuid.Nil {
|
||||
p.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (PurchaseCategoryPreset) TableName() string {
|
||||
return "purchase_category_presets"
|
||||
}
|
||||
|
||||
type PurchaseCategory 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"`
|
||||
PresetID *uuid.UUID `gorm:"type:uuid;index" json:"preset_id"`
|
||||
ParentID *uuid.UUID `gorm:"type:uuid;index" json:"parent_id"`
|
||||
Code string `gorm:"not null;size:100" json:"code"`
|
||||
Name string `gorm:"not null;size:255" json:"name"`
|
||||
Type PurchaseCategoryType `gorm:"not null;size:20" json:"type"`
|
||||
SortOrder int `gorm:"not null;default:0" json:"sort_order"`
|
||||
IsSystem bool `gorm:"not null;default:false" json:"is_system"`
|
||||
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"`
|
||||
Preset *PurchaseCategoryPreset `gorm:"foreignKey:PresetID" json:"preset,omitempty"`
|
||||
Parent *PurchaseCategory `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||
Children []PurchaseCategory `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||
}
|
||||
|
||||
func (c *PurchaseCategory) BeforeCreate(tx *gorm.DB) error {
|
||||
if c.ID == uuid.Nil {
|
||||
c.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (PurchaseCategory) TableName() string {
|
||||
return "purchase_categories"
|
||||
}
|
||||
@ -9,22 +9,20 @@ import (
|
||||
)
|
||||
|
||||
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"`
|
||||
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id" validate:"omitempty"`
|
||||
VendorID *uuid.UUID `gorm:"type:uuid" json:"vendor_id" validate:"omitempty"`
|
||||
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" json:"due_date" validate:"omitempty"`
|
||||
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"`
|
||||
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"`
|
||||
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,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"`
|
||||
@ -43,21 +41,19 @@ func (PurchaseOrder) TableName() string {
|
||||
}
|
||||
|
||||
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" json:"ingredient_id" validate:"omitempty"`
|
||||
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id" validate:"required"`
|
||||
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
|
||||
Quantity *float64 `gorm:"type:decimal(10,3)" json:"quantity" validate:"omitempty,gt=0"`
|
||||
UnitID *uuid.UUID `gorm:"type:uuid" json:"unit_id" validate:"omitempty"`
|
||||
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"`
|
||||
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"`
|
||||
PurchaseCategory *PurchaseCategory `gorm:"foreignKey:PurchaseCategoryID" json:"purchase_category,omitempty"`
|
||||
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
||||
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 {
|
||||
|
||||
@ -13,12 +13,10 @@ import (
|
||||
type UserRole string
|
||||
|
||||
const (
|
||||
RoleAdmin UserRole = "admin"
|
||||
RoleManager UserRole = "manager"
|
||||
RoleCashier UserRole = "cashier"
|
||||
RoleWaiter UserRole = "waiter"
|
||||
RoleOwner UserRole = "owner"
|
||||
RolePurchasing UserRole = "purchasing"
|
||||
RoleAdmin UserRole = "admin"
|
||||
RoleManager UserRole = "manager"
|
||||
RoleCashier UserRole = "cashier"
|
||||
RoleWaiter UserRole = "waiter"
|
||||
)
|
||||
|
||||
type Permissions map[string]interface{}
|
||||
@ -48,9 +46,10 @@ type User struct {
|
||||
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
||||
Email string `gorm:"uniqueIndex;not null;size:255" json:"email" validate:"required,email"`
|
||||
PasswordHash string `gorm:"not null;size:255" json:"-"`
|
||||
Role UserRole `gorm:"not null;size:50" json:"role" validate:"required,oneof=admin manager cashier waiter owner purchasing"`
|
||||
Role UserRole `gorm:"not null;size:50" json:"role" validate:"required,oneof=admin manager cashier waiter"`
|
||||
Permissions Permissions `gorm:"type:jsonb;default:'{}'" json:"permissions"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
FcmToken *string `gorm:"size:512" json:"fcm_token,omitempty"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DeviceType string
|
||||
type DevicePlatform string
|
||||
|
||||
const (
|
||||
DeviceTypeMobile DeviceType = "mobile"
|
||||
DeviceTypeTablet DeviceType = "tablet"
|
||||
DeviceTypeDesktop DeviceType = "desktop"
|
||||
|
||||
DevicePlatformAndroid DevicePlatform = "android"
|
||||
DevicePlatformIOS DevicePlatform = "ios"
|
||||
DevicePlatformWeb DevicePlatform = "web"
|
||||
)
|
||||
|
||||
type UserDevice struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"`
|
||||
DeviceID string `gorm:"not null;size:255;index" json:"device_id"`
|
||||
DeviceName string `gorm:"size:255" json:"device_name"`
|
||||
DeviceType DeviceType `gorm:"size:50" json:"device_type"`
|
||||
Platform DevicePlatform `gorm:"size:50" json:"platform"`
|
||||
FCMToken string `gorm:"size:512" json:"fcm_token"`
|
||||
AppVersion string `gorm:"size:50" json:"app_version"`
|
||||
OsVersion string `gorm:"size:50" json:"os_version"`
|
||||
IPAddress string `gorm:"size:45" json:"ip_address"`
|
||||
LastActiveAt *time.Time `gorm:"type:timestamptz" json:"last_active_at"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
func (u *UserDevice) BeforeCreate(tx *gorm.DB) error {
|
||||
if u.ID == uuid.Nil {
|
||||
u.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (UserDevice) TableName() string {
|
||||
return "user_devices"
|
||||
}
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"apskel-pos-be/internal/util"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AnalyticsHandler struct {
|
||||
@ -26,17 +25,6 @@ func NewAnalyticsHandler(
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AnalyticsHandler) resolveOutletID(c *gin.Context, contextOutletID uuid.UUID) *string {
|
||||
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
||||
return &outletIDStr
|
||||
}
|
||||
if contextOutletID != uuid.Nil {
|
||||
s := contextOutletID.String()
|
||||
return &s
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AnalyticsHandler) GetPaymentMethodAnalytics(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
@ -48,7 +36,7 @@ func (h *AnalyticsHandler) GetPaymentMethodAnalytics(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
modelReq := transformer.PaymentMethodAnalyticsContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetPaymentMethodAnalytics(ctx, modelReq)
|
||||
@ -72,7 +60,7 @@ func (h *AnalyticsHandler) GetSalesAnalytics(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
modelReq := transformer.SalesAnalyticsContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetSalesAnalytics(ctx, modelReq)
|
||||
@ -85,30 +73,6 @@ func (h *AnalyticsHandler) GetSalesAnalytics(c *gin.Context) {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetSalesAnalytics")
|
||||
}
|
||||
|
||||
func (h *AnalyticsHandler) GetPurchasingAnalytics(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var req contract.PurchasingAnalyticsRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetPurchasingAnalytics", err.Error())}), "AnalyticsHandler::GetPurchasingAnalytics")
|
||||
return
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq := transformer.PurchasingAnalyticsContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetPurchasingAnalytics(ctx, modelReq)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetPurchasingAnalytics", err.Error())}), "AnalyticsHandler::GetPurchasingAnalytics")
|
||||
return
|
||||
}
|
||||
|
||||
contractResp := transformer.PurchasingAnalyticsModelToContract(response)
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetPurchasingAnalytics")
|
||||
}
|
||||
|
||||
func (h *AnalyticsHandler) GetProductAnalytics(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
@ -120,7 +84,7 @@ func (h *AnalyticsHandler) GetProductAnalytics(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
modelReq := transformer.ProductAnalyticsContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetProductAnalytics(ctx, modelReq)
|
||||
@ -144,7 +108,7 @@ func (h *AnalyticsHandler) GetProductAnalyticsPerCategory(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
modelReq := transformer.ProductAnalyticsPerCategoryContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetProductAnalyticsPerCategory(ctx, modelReq)
|
||||
@ -168,7 +132,7 @@ func (h *AnalyticsHandler) GetDashboardAnalytics(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
modelReq := transformer.DashboardAnalyticsContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetDashboardAnalytics(ctx, modelReq)
|
||||
@ -192,7 +156,6 @@ func (h *AnalyticsHandler) GetProfitLossAnalytics(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq, err := transformer.ProfitLossAnalyticsContractToModel(&req)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetProfitLossAnalytics", err.Error())}), "AnalyticsHandler::GetProfitLossAnalytics")
|
||||
@ -210,87 +173,3 @@ func (h *AnalyticsHandler) GetProfitLossAnalytics(c *gin.Context) {
|
||||
contractResp := transformer.ProfitLossAnalyticsModelToContract(response)
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetProfitLossAnalytics")
|
||||
}
|
||||
|
||||
func (h *AnalyticsHandler) GetExclusiveSummaryPeriod(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var req contract.ExclusiveSummaryPeriodRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryPeriod", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryPeriod")
|
||||
return
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq, err := transformer.ExclusiveSummaryPeriodContractToModel(&req)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryPeriod", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryPeriod")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.analyticsService.GetExclusiveSummaryPeriod(ctx, modelReq)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetExclusiveSummaryPeriod", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryPeriod")
|
||||
return
|
||||
}
|
||||
|
||||
contractResp := transformer.ExclusiveSummaryPeriodModelToContract(response)
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetExclusiveSummaryPeriod")
|
||||
}
|
||||
|
||||
func (h *AnalyticsHandler) GetExclusiveSummaryMonthly(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var req contract.ExclusiveSummaryMonthlyRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryMonthly", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMonthly")
|
||||
return
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq, err := transformer.ExclusiveSummaryMonthlyContractToModel(&req)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryMonthly", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMonthly")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.analyticsService.GetExclusiveSummaryMonthly(ctx, modelReq)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetExclusiveSummaryMonthly", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMonthly")
|
||||
return
|
||||
}
|
||||
|
||||
contractResp := transformer.ExclusiveSummaryMonthlyModelToContract(response)
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetExclusiveSummaryMonthly")
|
||||
}
|
||||
|
||||
func (h *AnalyticsHandler) GetExclusiveSummaryMTD(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var req contract.ExclusiveSummaryMTDRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryMTD", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMTD")
|
||||
return
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq, err := transformer.ExclusiveSummaryMTDContractToModel(&req)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryMTD", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMTD")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.analyticsService.GetExclusiveSummaryMTD(ctx, modelReq)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetExclusiveSummaryMTD", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMTD")
|
||||
return
|
||||
}
|
||||
|
||||
contractResp := transformer.ExclusiveSummaryPeriodModelToContract(response)
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetExclusiveSummaryMTD")
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ func (h *CategoryHandler) CreateCategory(c *gin.Context) {
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var req contract.CreateCategoryRequest
|
||||
fmt.Printf("CategoryHandler::CreateCategory -> Request: %+v\n", req)
|
||||
fmt.Printf("CategoryHandler::CreateCategory -> Request: %+v\n", req)
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("CategoryHandler::CreateCategory -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
@ -44,11 +44,6 @@ func (h *CategoryHandler) CreateCategory(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Inject outlet_id from context if user has one and request doesn't provide it
|
||||
if req.OutletID == nil && contextInfo.OutletID != uuid.Nil {
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.categoryValidator.ValidateCreateCategoryRequest(&req)
|
||||
if validationError != nil {
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
@ -154,11 +149,6 @@ func (h *CategoryHandler) ListCategories(c *gin.Context) {
|
||||
OrganizationID: &contextInfo.OrganizationID,
|
||||
}
|
||||
|
||||
// Inject outlet_id from context if user has one
|
||||
if contextInfo.OutletID != uuid.Nil {
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
if pageStr := c.Query("page"); pageStr != "" {
|
||||
if page, err := strconv.Atoi(pageStr); err == nil {
|
||||
@ -186,11 +176,6 @@ func (h *CategoryHandler) ListCategories(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
||||
if outletID, err := uuid.Parse(outletIDStr); err == nil {
|
||||
req.OutletID = &outletID
|
||||
}
|
||||
}
|
||||
validationError, validationErrorCode := h.categoryValidator.ValidateListCategoriesRequest(req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(ctx).WithError(validationError).Error("CategoryHandler::ListCategories -> request validation failed")
|
||||
|
||||
@ -99,7 +99,7 @@ func (h *ChartOfAccountTypeHandler) DeleteChartOfAccountType(c *gin.Context) {
|
||||
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
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/logger"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
@ -49,7 +47,7 @@ func (m *CommonMiddleware) Recovery(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.FromContext(r.Context()).Error("Recovery", fmt.Sprintf("panic recovered: %v", err))
|
||||
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
|
||||
@ -1,229 +0,0 @@
|
||||
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 ExpenseHandler struct {
|
||||
expenseService service.ExpenseService
|
||||
expenseValidator validator.ExpenseValidator
|
||||
}
|
||||
|
||||
func NewExpenseHandler(
|
||||
expenseService service.ExpenseService,
|
||||
expenseValidator validator.ExpenseValidator,
|
||||
) *ExpenseHandler {
|
||||
return &ExpenseHandler{
|
||||
expenseService: expenseService,
|
||||
expenseValidator: expenseValidator,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ExpenseHandler) CreateExpense(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var req contract.CreateExpenseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::CreateExpense -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::CreateExpense")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.expenseValidator.ValidateCreateExpenseRequest(&req)
|
||||
if validationError != nil {
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::CreateExpense")
|
||||
return
|
||||
}
|
||||
|
||||
expenseResponse := h.expenseService.CreateExpense(ctx, contextInfo, &req)
|
||||
if expenseResponse.HasErrors() {
|
||||
errorResp := expenseResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::CreateExpense -> Failed to create expense from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::CreateExpense")
|
||||
}
|
||||
|
||||
func (h *ExpenseHandler) UpdateExpense(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
expenseIDStr := c.Param("id")
|
||||
expenseID, err := uuid.Parse(expenseIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::UpdateExpense -> Invalid expense ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid expense ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::UpdateExpense")
|
||||
return
|
||||
}
|
||||
|
||||
var req contract.UpdateExpenseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::UpdateExpense -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::UpdateExpense")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.expenseValidator.ValidateUpdateExpenseRequest(&req)
|
||||
if validationError != nil {
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::UpdateExpense")
|
||||
return
|
||||
}
|
||||
|
||||
expenseResponse := h.expenseService.UpdateExpense(ctx, contextInfo, expenseID, &req)
|
||||
if expenseResponse.HasErrors() {
|
||||
errorResp := expenseResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::UpdateExpense -> Failed to update expense from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::UpdateExpense")
|
||||
}
|
||||
|
||||
func (h *ExpenseHandler) DeleteExpense(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
expenseIDStr := c.Param("id")
|
||||
expenseID, err := uuid.Parse(expenseIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::DeleteExpense -> Invalid expense ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid expense ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::DeleteExpense")
|
||||
return
|
||||
}
|
||||
|
||||
expenseResponse := h.expenseService.DeleteExpense(ctx, contextInfo, expenseID)
|
||||
if expenseResponse.HasErrors() {
|
||||
errorResp := expenseResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::DeleteExpense -> Failed to delete expense from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::DeleteExpense")
|
||||
}
|
||||
|
||||
func (h *ExpenseHandler) GetExpense(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
expenseIDStr := c.Param("id")
|
||||
expenseID, err := uuid.Parse(expenseIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::GetExpense -> Invalid expense ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid expense ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::GetExpense")
|
||||
return
|
||||
}
|
||||
|
||||
expenseResponse := h.expenseService.GetExpenseByID(ctx, contextInfo, expenseID)
|
||||
if expenseResponse.HasErrors() {
|
||||
errorResp := expenseResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::GetExpense -> Failed to get expense from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::GetExpense")
|
||||
}
|
||||
|
||||
func (h *ExpenseHandler) ListExpenses(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
req := &contract.ListExpenseRequest{
|
||||
Page: 1,
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Prioritize outlet_id from context (e.g. outlet-scoped user),
|
||||
// fall back to query param if context has no outlet.
|
||||
if contextInfo.OutletID != uuid.Nil {
|
||||
req.OutletID = contextInfo.OutletID.String()
|
||||
} else if outletID := c.Query("outlet_id"); outletID != "" {
|
||||
req.OutletID = outletID
|
||||
}
|
||||
|
||||
if startDate := c.Query("start_date"); startDate != "" {
|
||||
req.StartDate = startDate
|
||||
}
|
||||
|
||||
if endDate := c.Query("end_date"); endDate != "" {
|
||||
req.EndDate = endDate
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.expenseValidator.ValidateListExpenseRequest(req)
|
||||
if validationError != nil {
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::ListExpenses")
|
||||
return
|
||||
}
|
||||
|
||||
expenseResponse := h.expenseService.ListExpenses(ctx, contextInfo, req)
|
||||
if expenseResponse.HasErrors() {
|
||||
errorResp := expenseResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::ListExpenses -> Failed to list expenses from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::ListExpenses")
|
||||
}
|
||||
|
||||
func (h *ExpenseHandler) GetExpenseAnalytics(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var req contract.ExpenseAnalyticsRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::GetExpenseAnalytics -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::GetExpenseAnalytics")
|
||||
return
|
||||
}
|
||||
|
||||
if contextInfo.OutletID != uuid.Nil {
|
||||
outletID := contextInfo.OutletID.String()
|
||||
req.OutletID = &outletID
|
||||
} else if outletID := c.Query("outlet_id"); outletID != "" {
|
||||
req.OutletID = &outletID
|
||||
}
|
||||
|
||||
expenseResponse := h.expenseService.GetExpenseAnalytics(ctx, contextInfo, &req)
|
||||
if expenseResponse.HasErrors() {
|
||||
errorResp := expenseResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::GetExpenseAnalytics -> Failed to get expense analytics from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::GetExpenseAnalytics")
|
||||
}
|
||||
@ -275,3 +275,4 @@ func (h *IngredientUnitConverterHandler) GetUnitsByIngredientID(c *gin.Context)
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, unitsResponse, "IngredientUnitConverterHandler::GetUnitsByIngredientID")
|
||||
}
|
||||
|
||||
|
||||
@ -1,190 +0,0 @@
|
||||
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"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type NotificationHandler struct {
|
||||
notificationService service.NotificationService
|
||||
notificationValidator validator.NotificationValidator
|
||||
}
|
||||
|
||||
func NewNotificationHandler(
|
||||
notificationService service.NotificationService,
|
||||
notificationValidator validator.NotificationValidator,
|
||||
) *NotificationHandler {
|
||||
return &NotificationHandler{
|
||||
notificationService: notificationService,
|
||||
notificationValidator: notificationValidator,
|
||||
}
|
||||
}
|
||||
|
||||
// Send godoc
|
||||
// POST /api/v1/notifications/send
|
||||
// Sends a notification to specific users.
|
||||
func (h *NotificationHandler) Send(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var req contract.SendNotificationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::Send -> request binding failed")
|
||||
validationErr := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::Send")
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr, errCode := h.notificationValidator.ValidateSendRequest(&req); validationErr != nil {
|
||||
respErr := contract.NewResponseError(errCode, constants.RequestEntity, validationErr.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{respErr}), "NotificationHandler::Send")
|
||||
return
|
||||
}
|
||||
|
||||
resp := h.notificationService.Send(ctx, &req, contextInfo.UserID)
|
||||
if resp.HasErrors() {
|
||||
logger.FromContext(ctx).WithError(resp.GetErrors()[0]).Error("NotificationHandler::Send -> service error")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::Send")
|
||||
}
|
||||
|
||||
// Broadcast godoc
|
||||
// POST /api/v1/notifications/broadcast
|
||||
// Sends a notification to all active users in the caller's organization.
|
||||
func (h *NotificationHandler) Broadcast(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var req contract.BroadcastNotificationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::Broadcast -> request binding failed")
|
||||
validationErr := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::Broadcast")
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr, errCode := h.notificationValidator.ValidateBroadcastRequest(&req); validationErr != nil {
|
||||
respErr := contract.NewResponseError(errCode, constants.RequestEntity, validationErr.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{respErr}), "NotificationHandler::Broadcast")
|
||||
return
|
||||
}
|
||||
|
||||
resp := h.notificationService.Broadcast(ctx, &req, contextInfo.OrganizationID, contextInfo.UserID)
|
||||
if resp.HasErrors() {
|
||||
logger.FromContext(ctx).WithError(resp.GetErrors()[0]).Error("NotificationHandler::Broadcast -> service error")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::Broadcast")
|
||||
}
|
||||
|
||||
// List godoc
|
||||
// GET /api/v1/notifications
|
||||
// Returns paginated notifications for the authenticated user.
|
||||
func (h *NotificationHandler) List(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
req := contract.ListNotificationsRequest{
|
||||
Page: 1,
|
||||
Limit: 20,
|
||||
}
|
||||
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::List -> query binding failed")
|
||||
validationErr := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::List")
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr, errCode := h.notificationValidator.ValidateListRequest(&req); validationErr != nil {
|
||||
respErr := contract.NewResponseError(errCode, constants.RequestEntity, validationErr.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{respErr}), "NotificationHandler::List")
|
||||
return
|
||||
}
|
||||
|
||||
resp := h.notificationService.ListForUser(ctx, &req, contextInfo.UserID)
|
||||
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::List")
|
||||
}
|
||||
|
||||
// GetByID godoc
|
||||
// GET /api/v1/notifications/:id
|
||||
func (h *NotificationHandler) GetByID(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::GetByID -> invalid notification ID")
|
||||
validationErr := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid notification ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::GetByID")
|
||||
return
|
||||
}
|
||||
|
||||
resp := h.notificationService.GetByID(ctx, id)
|
||||
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::GetByID")
|
||||
}
|
||||
|
||||
// MarkAsRead godoc
|
||||
// PUT /api/v1/notifications/:id/read
|
||||
func (h *NotificationHandler) MarkAsRead(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
idStr := c.Param("id")
|
||||
receiverID, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::MarkAsRead -> invalid receiver ID")
|
||||
validationErr := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid notification receiver ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::MarkAsRead")
|
||||
return
|
||||
}
|
||||
|
||||
resp := h.notificationService.MarkAsRead(ctx, receiverID, contextInfo.UserID)
|
||||
if resp.HasErrors() {
|
||||
logger.FromContext(ctx).WithError(resp.GetErrors()[0]).Error("NotificationHandler::MarkAsRead -> service error")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::MarkAsRead")
|
||||
}
|
||||
|
||||
// MarkAllAsRead godoc
|
||||
// PUT /api/v1/notifications/read-all
|
||||
func (h *NotificationHandler) MarkAllAsRead(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
resp := h.notificationService.MarkAllAsRead(ctx, contextInfo.UserID)
|
||||
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::MarkAllAsRead")
|
||||
}
|
||||
|
||||
// Delete godoc
|
||||
// DELETE /api/v1/notifications/:id
|
||||
func (h *NotificationHandler) Delete(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
idStr := c.Param("id")
|
||||
receiverID, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::Delete -> invalid receiver ID")
|
||||
validationErr := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid notification receiver ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::Delete")
|
||||
return
|
||||
}
|
||||
|
||||
resp := h.notificationService.DeleteForUser(ctx, receiverID, contextInfo.UserID)
|
||||
if resp.HasErrors() {
|
||||
logger.FromContext(ctx).WithError(resp.GetErrors()[0]).Error("NotificationHandler::Delete -> service error")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::Delete")
|
||||
}
|
||||
@ -137,10 +137,6 @@ func (h *OrderHandler) ListOrders(c *gin.Context) {
|
||||
}
|
||||
|
||||
modelReq.OrganizationID = &contextInfo.OrganizationID
|
||||
if modelReq.OutletID == nil && contextInfo.OutletID != uuid.Nil {
|
||||
modelReq.OutletID = &contextInfo.OutletID
|
||||
}
|
||||
|
||||
response, err := h.orderService.ListOrders(c.Request.Context(), modelReq)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::ListOrders", err.Error())}), "OrderHandler::ListOrders")
|
||||
|
||||
@ -60,7 +60,6 @@ func (h *ProductHandler) CreateProduct(c *gin.Context) {
|
||||
|
||||
func (h *ProductHandler) UpdateProduct(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
productIDStr := c.Param("id")
|
||||
productID, err := uuid.Parse(productIDStr)
|
||||
@ -86,7 +85,7 @@ func (h *ProductHandler) UpdateProduct(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
productResponse := h.productService.UpdateProduct(ctx, contextInfo, productID, &req)
|
||||
productResponse := h.productService.UpdateProduct(ctx, productID, &req)
|
||||
if productResponse.HasErrors() {
|
||||
errorResp := productResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::UpdateProduct -> Failed to update product from service")
|
||||
@ -118,7 +117,6 @@ func (h *ProductHandler) DeleteProduct(c *gin.Context) {
|
||||
|
||||
func (h *ProductHandler) GetProduct(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
productIDStr := c.Param("id")
|
||||
productID, err := uuid.Parse(productIDStr)
|
||||
@ -129,7 +127,7 @@ func (h *ProductHandler) GetProduct(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
productResponse := h.productService.GetProductByID(ctx, productID, contextInfo.OutletID)
|
||||
productResponse := h.productService.GetProductByID(ctx, productID)
|
||||
if productResponse.HasErrors() {
|
||||
errorResp := productResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::GetProduct -> Failed to get product from service")
|
||||
@ -186,97 +184,6 @@ func (h *ProductHandler) ListProducts(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
||||
if outletID, err := uuid.Parse(outletIDStr); err == nil {
|
||||
req.OutletID = &outletID
|
||||
}
|
||||
} else if contextInfo.OutletID != uuid.Nil {
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
}
|
||||
|
||||
if minPriceStr := c.Query("min_price"); minPriceStr != "" {
|
||||
if minPrice, err := strconv.ParseFloat(minPriceStr, 64); err == nil {
|
||||
req.MinPrice = &minPrice
|
||||
}
|
||||
}
|
||||
|
||||
if maxPriceStr := c.Query("max_price"); maxPriceStr != "" {
|
||||
if maxPrice, err := strconv.ParseFloat(maxPriceStr, 64); err == nil {
|
||||
req.MaxPrice = &maxPrice
|
||||
}
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.productValidator.ValidateListProductsRequest(req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(ctx).WithError(validationError).Error("ProductHandler::ListProducts -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductHandler::ListProducts")
|
||||
return
|
||||
}
|
||||
|
||||
productsResponse := h.productService.ListProducts(ctx, req)
|
||||
if productsResponse.HasErrors() {
|
||||
errorResp := productsResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::ListProducts -> Failed to list products from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, productsResponse, "ProductHandler::ListProducts")
|
||||
}
|
||||
|
||||
func (h *ProductHandler) ListProductAll(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
req := &contract.ListProductsRequest{
|
||||
Page: 1,
|
||||
Limit: 10,
|
||||
OrganizationID: &contextInfo.OrganizationID,
|
||||
}
|
||||
|
||||
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 businessType := c.Query("business_type"); businessType != "" {
|
||||
req.BusinessType = businessType
|
||||
}
|
||||
|
||||
if organizationIDStr := c.Query("organization_id"); organizationIDStr != "" {
|
||||
if organizationID, err := uuid.Parse(organizationIDStr); err == nil {
|
||||
req.OrganizationID = &organizationID
|
||||
}
|
||||
}
|
||||
|
||||
if categoryIDStr := c.Query("category_id"); categoryIDStr != "" {
|
||||
if categoryID, err := uuid.Parse(categoryIDStr); err == nil {
|
||||
req.CategoryID = &categoryID
|
||||
}
|
||||
}
|
||||
|
||||
if isActiveStr := c.Query("is_active"); isActiveStr != "" {
|
||||
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
|
||||
req.IsActive = &isActive
|
||||
}
|
||||
}
|
||||
|
||||
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
||||
if outletID, err := uuid.Parse(outletIDStr); err == nil {
|
||||
req.OutletID = &outletID
|
||||
}
|
||||
}
|
||||
|
||||
if minPriceStr := c.Query("min_price"); minPriceStr != "" {
|
||||
if minPrice, err := strconv.ParseFloat(minPriceStr, 64); err == nil {
|
||||
req.MinPrice = &minPrice
|
||||
|
||||
@ -1,135 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"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"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ProductOutletPriceHandler struct {
|
||||
service service.ProductOutletPriceService
|
||||
validator validator.ProductOutletPriceValidator
|
||||
}
|
||||
|
||||
func NewProductOutletPriceHandler(svc service.ProductOutletPriceService, v validator.ProductOutletPriceValidator) *ProductOutletPriceHandler {
|
||||
return &ProductOutletPriceHandler{
|
||||
service: svc,
|
||||
validator: v,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ProductOutletPriceHandler) Upsert(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req contract.CreateProductOutletPriceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("ProductOutletPriceHandler::Upsert -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::Upsert")
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr, code := h.validator.ValidateCreateRequest(&req); validationErr != nil {
|
||||
validationResponseError := contract.NewResponseError(code, constants.RequestEntity, validationErr.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::Upsert")
|
||||
return
|
||||
}
|
||||
|
||||
resp := h.service.Upsert(ctx, &req)
|
||||
util.HandleResponse(c.Writer, c.Request, resp, "ProductOutletPriceHandler::Upsert")
|
||||
}
|
||||
|
||||
func (h *ProductOutletPriceHandler) GetByProductAndOutlet(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
productIDStr := c.Param("product_id")
|
||||
productID, err := uuid.Parse(productIDStr)
|
||||
if err != nil {
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid product ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::GetByProductAndOutlet")
|
||||
return
|
||||
}
|
||||
|
||||
outletIDStr := c.Param("outlet_id")
|
||||
outletID, err := uuid.Parse(outletIDStr)
|
||||
if err != nil {
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::GetByProductAndOutlet")
|
||||
return
|
||||
}
|
||||
|
||||
resp := h.service.GetByProductAndOutlet(ctx, productID, outletID)
|
||||
util.HandleResponse(c.Writer, c.Request, resp, "ProductOutletPriceHandler::GetByProductAndOutlet")
|
||||
}
|
||||
|
||||
func (h *ProductOutletPriceHandler) GetByProduct(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
productIDStr := c.Param("product_id")
|
||||
productID, err := uuid.Parse(productIDStr)
|
||||
if err != nil {
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid product ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::GetByProduct")
|
||||
return
|
||||
}
|
||||
|
||||
resp := h.service.GetByProduct(ctx, productID)
|
||||
util.HandleResponse(c.Writer, c.Request, resp, "ProductOutletPriceHandler::GetByProduct")
|
||||
}
|
||||
|
||||
func (h *ProductOutletPriceHandler) GetByOutlet(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
outletIDStr := c.Param("outlet_id")
|
||||
outletID, err := uuid.Parse(outletIDStr)
|
||||
if err != nil {
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::GetByOutlet")
|
||||
return
|
||||
}
|
||||
|
||||
resp := h.service.GetByOutlet(ctx, outletID)
|
||||
util.HandleResponse(c.Writer, c.Request, resp, "ProductOutletPriceHandler::GetByOutlet")
|
||||
}
|
||||
|
||||
func (h *ProductOutletPriceHandler) Delete(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::Delete")
|
||||
return
|
||||
}
|
||||
|
||||
resp := h.service.Delete(ctx, id)
|
||||
util.HandleResponse(c.Writer, c.Request, resp, "ProductOutletPriceHandler::Delete")
|
||||
}
|
||||
|
||||
func (h *ProductOutletPriceHandler) BulkUpsert(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req contract.BulkCreateProductOutletPriceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("ProductOutletPriceHandler::BulkUpsert -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::BulkUpsert")
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr, code := h.validator.ValidateBulkCreateRequest(&req); validationErr != nil {
|
||||
validationResponseError := contract.NewResponseError(code, constants.RequestEntity, validationErr.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::BulkUpsert")
|
||||
return
|
||||
}
|
||||
|
||||
resp := h.service.BulkUpsert(ctx, &req)
|
||||
util.HandleResponse(c.Writer, c.Request, resp, "ProductOutletPriceHandler::BulkUpsert")
|
||||
}
|
||||
@ -219,4 +219,4 @@ func (h *ProductRecipeHandler) BulkCreate(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(recipes))
|
||||
}
|
||||
}
|
||||
@ -1,160 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"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"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PurchaseCategoryHandler struct {
|
||||
purchaseCategoryService service.PurchaseCategoryService
|
||||
purchaseCategoryValidator validator.PurchaseCategoryValidator
|
||||
}
|
||||
|
||||
func NewPurchaseCategoryHandler(purchaseCategoryService service.PurchaseCategoryService, purchaseCategoryValidator validator.PurchaseCategoryValidator) *PurchaseCategoryHandler {
|
||||
return &PurchaseCategoryHandler{
|
||||
purchaseCategoryService: purchaseCategoryService,
|
||||
purchaseCategoryValidator: purchaseCategoryValidator,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PurchaseCategoryHandler) CreatePurchaseCategory(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var req contract.CreatePurchaseCategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("PurchaseCategoryHandler::CreatePurchaseCategory -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::CreatePurchaseCategory")
|
||||
return
|
||||
}
|
||||
|
||||
if validationError, validationErrorCode := h.purchaseCategoryValidator.ValidateCreatePurchaseCategoryRequest(&req); validationError != nil {
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::CreatePurchaseCategory")
|
||||
return
|
||||
}
|
||||
|
||||
response := h.purchaseCategoryService.CreatePurchaseCategory(ctx, contextInfo, &req)
|
||||
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::CreatePurchaseCategory")
|
||||
}
|
||||
|
||||
func (h *PurchaseCategoryHandler) UpdatePurchaseCategory(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
categoryID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase category ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||
return
|
||||
}
|
||||
|
||||
var req contract.UpdatePurchaseCategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("PurchaseCategoryHandler::UpdatePurchaseCategory -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||
return
|
||||
}
|
||||
|
||||
if validationError, validationErrorCode := h.purchaseCategoryValidator.ValidateUpdatePurchaseCategoryRequest(&req); validationError != nil {
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||
return
|
||||
}
|
||||
|
||||
response := h.purchaseCategoryService.UpdatePurchaseCategory(ctx, contextInfo, categoryID, &req)
|
||||
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||
}
|
||||
|
||||
func (h *PurchaseCategoryHandler) DeletePurchaseCategory(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
categoryID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase category ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::DeletePurchaseCategory")
|
||||
return
|
||||
}
|
||||
|
||||
response := h.purchaseCategoryService.DeletePurchaseCategory(ctx, contextInfo, categoryID)
|
||||
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::DeletePurchaseCategory")
|
||||
}
|
||||
|
||||
func (h *PurchaseCategoryHandler) GetPurchaseCategory(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
categoryID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase category ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::GetPurchaseCategory")
|
||||
return
|
||||
}
|
||||
|
||||
response := h.purchaseCategoryService.GetPurchaseCategoryByID(ctx, contextInfo, categoryID)
|
||||
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::GetPurchaseCategory")
|
||||
}
|
||||
|
||||
func (h *PurchaseCategoryHandler) ListPurchaseCategories(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
req := &contract.ListPurchaseCategoriesRequest{
|
||||
Page: 1,
|
||||
Limit: 100,
|
||||
}
|
||||
|
||||
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 parentIDStr := c.Query("parent_id"); parentIDStr != "" {
|
||||
if parentID, err := uuid.Parse(parentIDStr); err == nil {
|
||||
req.ParentID = &parentID
|
||||
}
|
||||
}
|
||||
|
||||
if categoryType := c.Query("type"); categoryType != "" {
|
||||
req.Type = categoryType
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if validationError, validationErrorCode := h.purchaseCategoryValidator.ValidateListPurchaseCategoriesRequest(req); validationError != nil {
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::ListPurchaseCategories")
|
||||
return
|
||||
}
|
||||
|
||||
response := h.purchaseCategoryService.ListPurchaseCategories(ctx, contextInfo, req)
|
||||
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::ListPurchaseCategories")
|
||||
}
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ReportHandler struct {
|
||||
@ -20,26 +19,11 @@ func NewReportHandler(reportService service.ReportService, userService UserServi
|
||||
return &ReportHandler{reportService: reportService, userService: userService}
|
||||
}
|
||||
|
||||
func (h *ReportHandler) resolveOutletID(c *gin.Context, contextOutletID uuid.UUID) string {
|
||||
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
||||
if _, err := uuid.Parse(outletIDStr); err == nil {
|
||||
return outletIDStr
|
||||
}
|
||||
}
|
||||
if pathOutletID := c.Param("outlet_id"); pathOutletID != "" {
|
||||
return pathOutletID
|
||||
}
|
||||
if contextOutletID != uuid.Nil {
|
||||
return contextOutletID.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *ReportHandler) GetDailyTransactionReportPDF(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
ci := appcontext.FromGinContext(ctx)
|
||||
|
||||
outletID := h.resolveOutletID(c, ci.OutletID)
|
||||
outletID := c.Param("outlet_id")
|
||||
var dayPtr *time.Time
|
||||
if d := c.Query("date"); d != "" {
|
||||
if t, err := time.Parse("2006-01-02", d); err == nil {
|
||||
@ -66,35 +50,3 @@ func (h *ReportHandler) GetDailyTransactionReportPDF(c *gin.Context) {
|
||||
"file_name": fileName,
|
||||
}), "ReportHandler::GetDailyTransactionReportPDF")
|
||||
}
|
||||
|
||||
func (h *ReportHandler) GetProfitLossReportPDF(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
ci := appcontext.FromGinContext(ctx)
|
||||
|
||||
outletID := h.resolveOutletID(c, ci.OutletID)
|
||||
var dayPtr *time.Time
|
||||
if d := c.Query("date"); d != "" {
|
||||
if t, err := time.Parse("2006-01-02", d); err == nil {
|
||||
dayPtr = &t
|
||||
}
|
||||
}
|
||||
|
||||
user, err := h.userService.GetUserByID(ctx, ci.UserID)
|
||||
var genBy string
|
||||
if err != nil {
|
||||
genBy = ci.UserID.String()
|
||||
} else {
|
||||
genBy = user.Name
|
||||
}
|
||||
|
||||
publicURL, fileName, err := h.reportService.GenerateProfitLossPDF(ctx, ci.OrganizationID.String(), outletID, dayPtr, genBy)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "ReportHandler::GetProfitLossReportPDF", err.Error())}), "ReportHandler::GetProfitLossReportPDF")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(map[string]string{
|
||||
"url": publicURL,
|
||||
"file_name": fileName,
|
||||
}), "ReportHandler::GetProfitLossReportPDF")
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/client"
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/logger"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/pkg/tabletoken"
|
||||
"apskel-pos-be/internal/processor"
|
||||
@ -15,21 +15,22 @@ import (
|
||||
"apskel-pos-be/internal/util"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type SelfOrderHandler struct {
|
||||
orderService service.OrderService
|
||||
categoryService service.CategoryService
|
||||
productService service.ProductService
|
||||
tableRepo repository.TableRepositoryInterface
|
||||
outletRepo processor.OutletRepository
|
||||
userRepo processor.UserRepository
|
||||
sessionRepo repository.SessionRepository
|
||||
orderRepo repository.OrderRepository
|
||||
productOutletPriceService service.ProductOutletPriceService
|
||||
orderService service.OrderService
|
||||
categoryService service.CategoryService
|
||||
productService service.ProductService
|
||||
tableRepo repository.TableRepositoryInterface
|
||||
outletRepo processor.OutletRepository
|
||||
userRepo processor.UserRepository
|
||||
sessionRepo repository.SessionRepository
|
||||
orderRepo repository.OrderRepository
|
||||
fcmClient client.FcmClient
|
||||
}
|
||||
|
||||
func NewSelfOrderHandler(
|
||||
@ -41,18 +42,18 @@ func NewSelfOrderHandler(
|
||||
userRepo processor.UserRepository,
|
||||
sessionRepo repository.SessionRepository,
|
||||
orderRepo repository.OrderRepository,
|
||||
productOutletPriceService service.ProductOutletPriceService,
|
||||
fcmClient client.FcmClient,
|
||||
) *SelfOrderHandler {
|
||||
return &SelfOrderHandler{
|
||||
orderService: orderService,
|
||||
categoryService: categoryService,
|
||||
productService: productService,
|
||||
tableRepo: tableRepo,
|
||||
outletRepo: outletRepo,
|
||||
userRepo: userRepo,
|
||||
sessionRepo: sessionRepo,
|
||||
orderRepo: orderRepo,
|
||||
productOutletPriceService: productOutletPriceService,
|
||||
orderService: orderService,
|
||||
categoryService: categoryService,
|
||||
productService: productService,
|
||||
tableRepo: tableRepo,
|
||||
outletRepo: outletRepo,
|
||||
userRepo: userRepo,
|
||||
sessionRepo: sessionRepo,
|
||||
orderRepo: orderRepo,
|
||||
fcmClient: fcmClient,
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,29 +220,16 @@ func (h *SelfOrderHandler) GetMenu(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
menu := h.buildMenuResponse(ctx, outlet, table, catList.Categories, prodList.Products)
|
||||
menu := h.buildMenuResponse(outlet, table, catList.Categories, prodList.Products)
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(menu), "SelfOrderHandler::GetMenu")
|
||||
}
|
||||
|
||||
func (h *SelfOrderHandler) buildMenuResponse(
|
||||
ctx context.Context,
|
||||
outlet *entities.Outlet,
|
||||
table *entities.Table,
|
||||
categories []contract.CategoryResponse,
|
||||
products []contract.ProductResponse,
|
||||
) *contract.SelfOrderMenuResponse {
|
||||
outletPriceMap := make(map[uuid.UUID]float64)
|
||||
if h.productOutletPriceService != nil {
|
||||
priceResp := h.productOutletPriceService.GetByOutlet(ctx, outlet.ID)
|
||||
if priceResp != nil && !priceResp.HasErrors() {
|
||||
if priceList, ok := priceResp.Data.(*contract.ListProductOutletPricesResponse); ok {
|
||||
for _, p := range priceList.Prices {
|
||||
outletPriceMap[p.ProductID] = p.Price
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
productMap := make(map[uuid.UUID][]contract.ProductResponse)
|
||||
for _, p := range products {
|
||||
productMap[p.CategoryID] = append(productMap[p.CategoryID], p)
|
||||
@ -252,15 +240,11 @@ func (h *SelfOrderHandler) buildMenuResponse(
|
||||
menuItems := make([]contract.SelfOrderMenuItem, 0)
|
||||
if prods, ok := productMap[cat.ID]; ok {
|
||||
for _, p := range prods {
|
||||
price := p.Price
|
||||
if outletPrice, exists := outletPriceMap[p.ID]; exists {
|
||||
price = outletPrice
|
||||
}
|
||||
item := contract.SelfOrderMenuItem{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Description: p.Description,
|
||||
Price: price,
|
||||
Price: p.Price,
|
||||
ImageURL: p.ImageURL,
|
||||
}
|
||||
for _, v := range p.Variants {
|
||||
@ -351,7 +335,6 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
|
||||
metadata := make(map[string]interface{})
|
||||
metadata["self_order"] = true
|
||||
metadata["session_id"] = session.ID
|
||||
metadata["customer_name"] = req.CustomerName
|
||||
|
||||
tableID := table.ID
|
||||
modelReq := &models.CreateOrderRequest{
|
||||
@ -359,7 +342,7 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
|
||||
UserID: userID,
|
||||
TableID: &tableID,
|
||||
TableNumber: &table.TableName,
|
||||
OrderType: constants.OrderType(req.OrderType),
|
||||
OrderType: constants.OrderTypeDineIn,
|
||||
OrderItems: orderItems,
|
||||
Metadata: metadata,
|
||||
}
|
||||
@ -373,13 +356,41 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
go h.sendNewOrderNotification(context.Background(), table.OrganizationID, table.TableName, len(req.OrderItems))
|
||||
|
||||
contractResp := transformer.OrderModelToContract(response)
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "SelfOrderHandler::CreateOrder")
|
||||
}
|
||||
|
||||
func (h *SelfOrderHandler) sendNewOrderNotification(ctx context.Context, organizationID uuid.UUID, tableName string, itemCount int) {
|
||||
users, err := h.userRepo.GetUsersWithFcmTokenByOrganization(ctx, organizationID)
|
||||
if err != nil {
|
||||
log.Printf("SelfOrderHandler::sendNewOrderNotification -> failed to get users with FCM token: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tokens := make([]string, 0, len(users))
|
||||
for _, u := range users {
|
||||
if u.FcmToken != nil && *u.FcmToken != "" {
|
||||
tokens = append(tokens, *u.FcmToken)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tokens) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
title := "Order Baru"
|
||||
body := fmt.Sprintf("Order baru dari Meja %s — %d item", tableName, itemCount)
|
||||
|
||||
if err := h.fcmClient.SendMulticastNotification(ctx, tokens, title, body); err != nil {
|
||||
log.Printf("SelfOrderHandler::sendNewOrderNotification -> failed to send FCM notification: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SelfOrderHandler) GetOrdersBySession(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
sessionID := c.Param("session_id")
|
||||
sessionID := c.Param("sessionId")
|
||||
|
||||
if sessionID == "" {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
@ -412,15 +423,47 @@ func (h *SelfOrderHandler) GetOrdersBySession(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
modelOrders := mappers.OrderEntitiesToResponses(orders)
|
||||
contractOrders := make([]contract.OrderResponse, len(modelOrders))
|
||||
for i := range modelOrders {
|
||||
contractOrders[i] = *transformer.OrderModelToContract(&modelOrders[i])
|
||||
resp := &contract.SelfOrderListOrdersResponse{
|
||||
Orders: make([]contract.SelfOrderOrderItem, 0, len(orders)),
|
||||
}
|
||||
for _, o := range orders {
|
||||
item := contract.SelfOrderOrderItem{
|
||||
ID: o.ID,
|
||||
OrderNumber: o.OrderNumber,
|
||||
TableNumber: o.TableNumber,
|
||||
OrderType: string(o.OrderType),
|
||||
Status: string(o.Status),
|
||||
Subtotal: o.Subtotal,
|
||||
TaxAmount: o.TaxAmount,
|
||||
DiscountAmount: o.DiscountAmount,
|
||||
TotalAmount: o.TotalAmount,
|
||||
RemainingAmount: o.RemainingAmount,
|
||||
PaymentStatus: string(o.PaymentStatus),
|
||||
IsVoid: o.IsVoid,
|
||||
IsRefund: o.IsRefund,
|
||||
CreatedAt: o.CreatedAt,
|
||||
}
|
||||
for _, oi := range o.OrderItems {
|
||||
lineItem := contract.SelfOrderOrderLineItem{
|
||||
ProductID: oi.ProductID,
|
||||
Quantity: oi.Quantity,
|
||||
UnitPrice: oi.UnitPrice,
|
||||
TotalPrice: oi.TotalPrice,
|
||||
Notes: oi.Notes,
|
||||
Status: string(oi.Status),
|
||||
ProductVariantID: oi.ProductVariantID,
|
||||
}
|
||||
if oi.Product.ID != uuid.Nil {
|
||||
lineItem.ProductName = oi.Product.Name
|
||||
}
|
||||
if oi.ProductVariant != nil {
|
||||
lineItem.ProductVariantNam = &oi.ProductVariant.Name
|
||||
}
|
||||
item.Items = append(item.Items, lineItem)
|
||||
}
|
||||
resp.Orders = append(resp.Orders, item)
|
||||
}
|
||||
|
||||
resp := &contract.SelfOrderListOrdersResponse{
|
||||
Orders: contractOrders,
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "SelfOrderHandler::GetOrdersBySession")
|
||||
}
|
||||
|
||||
@ -429,7 +472,6 @@ func (h *SelfOrderHandler) validateCreateOrderRequest(req *contract.SelfOrderCre
|
||||
return fmt.Errorf("session_id is required")
|
||||
}
|
||||
if len(req.OrderItems) == 0 {
|
||||
|
||||
return fmt.Errorf("at least one order item is required")
|
||||
}
|
||||
for i, item := range req.OrderItems {
|
||||
@ -457,7 +499,7 @@ func (h *SelfOrderHandler) ListCategories(c *gin.Context) {
|
||||
|
||||
if req.OrganizationID == "" {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "organization_id is required"),
|
||||
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "organisasi_id is required"),
|
||||
}), "SelfOrderHandler::ListCategories")
|
||||
return
|
||||
}
|
||||
@ -472,7 +514,7 @@ func (h *SelfOrderHandler) ListCategories(c *gin.Context) {
|
||||
orgID, err := uuid.Parse(req.OrganizationID)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "invalid organization_id format"),
|
||||
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "invalid organisasi_id format"),
|
||||
}), "SelfOrderHandler::ListCategories")
|
||||
return
|
||||
}
|
||||
|
||||
@ -19,14 +19,14 @@ import (
|
||||
type TableHandler struct {
|
||||
tableService TableService
|
||||
tableValidator *validator.TableValidator
|
||||
selfOrderURL string
|
||||
baseURL string
|
||||
}
|
||||
|
||||
func NewTableHandler(tableService TableService, tableValidator *validator.TableValidator, selfOrderURL string) *TableHandler {
|
||||
func NewTableHandler(tableService TableService, tableValidator *validator.TableValidator, baseURL string) *TableHandler {
|
||||
return &TableHandler{
|
||||
tableService: tableService,
|
||||
tableValidator: tableValidator,
|
||||
selfOrderURL: selfOrderURL,
|
||||
baseURL: baseURL,
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,11 +150,6 @@ func (h *TableHandler) List(c *gin.Context) {
|
||||
Limit: 100,
|
||||
}
|
||||
|
||||
// Fallback to context outlet ID if not provided in query
|
||||
if query.OutletID == "" && contextInfo.OutletID != uuid.Nil {
|
||||
query.OutletID = contextInfo.OutletID.String()
|
||||
}
|
||||
|
||||
if pageStr := c.Query("page"); pageStr != "" {
|
||||
if page, err := strconv.Atoi(pageStr); err == nil && page > 0 {
|
||||
query.Page = page
|
||||
@ -317,7 +312,7 @@ func (h *TableHandler) GenerateQRCode(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
selfOrderURLResult := fmt.Sprintf("%s/menu?token=%s", h.selfOrderURL, token)
|
||||
selfOrderURL := fmt.Sprintf("%s/api/v1/self-order/table/%s", h.baseURL, token)
|
||||
|
||||
size := 256
|
||||
if sizeStr := c.Query("size"); sizeStr != "" {
|
||||
@ -326,7 +321,7 @@ func (h *TableHandler) GenerateQRCode(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
pngBytes, err := qrcode.GeneratePNG(selfOrderURLResult, size)
|
||||
pngBytes, err := qrcode.GeneratePNG(selfOrderURL, size)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::GenerateQRCode -> QR generation failed")
|
||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, "Failed to generate QR code")
|
||||
|
||||
@ -1,215 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"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"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserDeviceHandler struct {
|
||||
userDeviceService service.UserDeviceService
|
||||
userDeviceValidator validator.UserDeviceValidator
|
||||
}
|
||||
|
||||
func NewUserDeviceHandler(
|
||||
userDeviceService service.UserDeviceService,
|
||||
userDeviceValidator validator.UserDeviceValidator,
|
||||
) *UserDeviceHandler {
|
||||
return &UserDeviceHandler{
|
||||
userDeviceService: userDeviceService,
|
||||
userDeviceValidator: userDeviceValidator,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *UserDeviceHandler) RegisterDevice(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var req contract.RegisterUserDeviceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UserDeviceHandler::RegisterDevice -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::RegisterDevice")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.userDeviceValidator.ValidateRegisterDeviceRequest(&req)
|
||||
if validationError != nil {
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::RegisterDevice")
|
||||
return
|
||||
}
|
||||
|
||||
deviceResponse := h.userDeviceService.RegisterDevice(ctx, contextInfo.UserID, &req)
|
||||
if deviceResponse.HasErrors() {
|
||||
errorResp := deviceResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("UserDeviceHandler::RegisterDevice -> Failed to register device from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, deviceResponse, "UserDeviceHandler::RegisterDevice")
|
||||
}
|
||||
|
||||
func (h *UserDeviceHandler) UpdateDevice(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
deviceIDStr := c.Param("id")
|
||||
deviceID, err := uuid.Parse(deviceIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UserDeviceHandler::UpdateDevice -> Invalid device ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid device ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::UpdateDevice")
|
||||
return
|
||||
}
|
||||
|
||||
var req contract.UpdateUserDeviceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UserDeviceHandler::UpdateDevice -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::UpdateDevice")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.userDeviceValidator.ValidateUpdateDeviceRequest(&req)
|
||||
if validationError != nil {
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::UpdateDevice")
|
||||
return
|
||||
}
|
||||
|
||||
deviceResponse := h.userDeviceService.UpdateDevice(ctx, deviceID, &req)
|
||||
if deviceResponse.HasErrors() {
|
||||
errorResp := deviceResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("UserDeviceHandler::UpdateDevice -> Failed to update device from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, deviceResponse, "UserDeviceHandler::UpdateDevice")
|
||||
}
|
||||
|
||||
func (h *UserDeviceHandler) DeleteDevice(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
deviceIDStr := c.Param("id")
|
||||
deviceID, err := uuid.Parse(deviceIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UserDeviceHandler::DeleteDevice -> Invalid device ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid device ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::DeleteDevice")
|
||||
return
|
||||
}
|
||||
|
||||
deviceResponse := h.userDeviceService.DeleteDevice(ctx, deviceID)
|
||||
if deviceResponse.HasErrors() {
|
||||
errorResp := deviceResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("UserDeviceHandler::DeleteDevice -> Failed to delete device from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, deviceResponse, "UserDeviceHandler::DeleteDevice")
|
||||
}
|
||||
|
||||
func (h *UserDeviceHandler) GetDevice(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
deviceIDStr := c.Param("id")
|
||||
deviceID, err := uuid.Parse(deviceIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UserDeviceHandler::GetDevice -> Invalid device ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid device ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::GetDevice")
|
||||
return
|
||||
}
|
||||
|
||||
deviceResponse := h.userDeviceService.GetDeviceByID(ctx, deviceID)
|
||||
if deviceResponse.HasErrors() {
|
||||
errorResp := deviceResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("UserDeviceHandler::GetDevice -> Failed to get device from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, deviceResponse, "UserDeviceHandler::GetDevice")
|
||||
}
|
||||
|
||||
func (h *UserDeviceHandler) GetMyDevices(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
deviceResponse := h.userDeviceService.GetDevicesByUserID(ctx, contextInfo.UserID)
|
||||
if deviceResponse.HasErrors() {
|
||||
errorResp := deviceResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("UserDeviceHandler::GetMyDevices -> Failed to get devices from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, deviceResponse, "UserDeviceHandler::GetMyDevices")
|
||||
}
|
||||
|
||||
func (h *UserDeviceHandler) GetDevicesByUser(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
userIDStr := c.Param("user_id")
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UserDeviceHandler::GetDevicesByUser -> Invalid user ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid user ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::GetDevicesByUser")
|
||||
return
|
||||
}
|
||||
|
||||
deviceResponse := h.userDeviceService.GetDevicesByUserID(ctx, userID)
|
||||
if deviceResponse.HasErrors() {
|
||||
errorResp := deviceResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("UserDeviceHandler::GetDevicesByUser -> Failed to get devices from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, deviceResponse, "UserDeviceHandler::GetDevicesByUser")
|
||||
}
|
||||
|
||||
func (h *UserDeviceHandler) ListDevices(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
req := &contract.ListUserDevicesRequest{
|
||||
Page: 1,
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
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 userID := c.Query("user_id"); userID != "" {
|
||||
req.UserID = userID
|
||||
}
|
||||
|
||||
if platform := c.Query("platform"); platform != "" {
|
||||
req.Platform = platform
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.userDeviceValidator.ValidateListDevicesRequest(req)
|
||||
if validationError != nil {
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::ListDevices")
|
||||
return
|
||||
}
|
||||
|
||||
deviceResponse := h.userDeviceService.ListDevices(ctx, req)
|
||||
if deviceResponse.HasErrors() {
|
||||
errorResp := deviceResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("UserDeviceHandler::ListDevices -> Failed to list devices from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, deviceResponse, "UserDeviceHandler::ListDevices")
|
||||
}
|
||||
@ -13,12 +13,11 @@ func CategoryEntityToModel(entity *entities.Category) *models.Category {
|
||||
return &models.Category{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
Name: entity.Name,
|
||||
Description: entity.Description,
|
||||
ImageURL: nil,
|
||||
Order: entity.Order,
|
||||
IsActive: true,
|
||||
ImageURL: nil, // Entity doesn't have ImageURL, model does
|
||||
Order: entity.Order, // Entity doesn't have SortOrder, model does
|
||||
IsActive: true, // Entity doesn't have IsActive, default to true
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
@ -33,14 +32,14 @@ func CategoryModelToEntity(model *models.Category) *entities.Category {
|
||||
if model.ImageURL != nil {
|
||||
metadata["image_url"] = *model.ImageURL
|
||||
}
|
||||
// metadata["sort_order"] = model.SortOrder
|
||||
|
||||
return &entities.Category{
|
||||
ID: model.ID,
|
||||
OrganizationID: model.OrganizationID,
|
||||
OutletID: model.OutletID,
|
||||
Name: model.Name,
|
||||
Description: model.Description,
|
||||
BusinessType: "restaurant",
|
||||
BusinessType: "restaurant", // Default business type
|
||||
Order: model.Order,
|
||||
Metadata: metadata,
|
||||
CreatedAt: model.CreatedAt,
|
||||
@ -57,14 +56,14 @@ func CreateCategoryRequestToEntity(req *models.CreateCategoryRequest) *entities.
|
||||
if req.ImageURL != nil {
|
||||
metadata["image_url"] = *req.ImageURL
|
||||
}
|
||||
// metadata["sort_order"] = req.SortOrder
|
||||
|
||||
return &entities.Category{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Order: req.Order,
|
||||
BusinessType: "restaurant",
|
||||
BusinessType: "restaurant", // Default business type
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
@ -88,12 +87,11 @@ func CategoryEntityToResponse(entity *entities.Category) *models.CategoryRespons
|
||||
return &models.CategoryResponse{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
Name: entity.Name,
|
||||
Description: entity.Description,
|
||||
ImageURL: imageURL,
|
||||
Order: entity.Order,
|
||||
IsActive: true,
|
||||
Order: entity.Order,
|
||||
IsActive: true, // Default to true since entity doesn't have this field
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
@ -123,10 +121,6 @@ func UpdateCategoryEntityFromRequest(entity *entities.Category, req *models.Upda
|
||||
if req.Order != nil {
|
||||
entity.Order = *req.Order
|
||||
}
|
||||
|
||||
if req.OutletID != nil {
|
||||
entity.OutletID = req.OutletID
|
||||
}
|
||||
}
|
||||
|
||||
func CategoryEntitiesToModels(entities []*entities.Category) []*models.Category {
|
||||
|
||||
@ -1,135 +0,0 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
func ExpenseEntityToModel(entity *entities.Expense) *models.Expense {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.Expense{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
Receiver: entity.Receiver,
|
||||
TransactionDate: entity.TransactionDate,
|
||||
CodeNumber: entity.CodeNumber,
|
||||
Status: entity.Status,
|
||||
Description: entity.Description,
|
||||
Tax: entity.Tax,
|
||||
Total: entity.Total,
|
||||
Reserved1: entity.Reserved1,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ExpenseModelToEntity(model *models.Expense) *entities.Expense {
|
||||
if model == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &entities.Expense{
|
||||
ID: model.ID,
|
||||
OrganizationID: model.OrganizationID,
|
||||
OutletID: model.OutletID,
|
||||
Receiver: model.Receiver,
|
||||
TransactionDate: model.TransactionDate,
|
||||
CodeNumber: model.CodeNumber,
|
||||
Status: model.Status,
|
||||
Description: model.Description,
|
||||
Tax: model.Tax,
|
||||
Total: model.Total,
|
||||
Reserved1: model.Reserved1,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ExpenseEntityToResponse(entity *entities.Expense) *models.ExpenseResponse {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
resp := &models.ExpenseResponse{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
Receiver: entity.Receiver,
|
||||
TransactionDate: entity.TransactionDate,
|
||||
CodeNumber: entity.CodeNumber,
|
||||
Status: entity.Status,
|
||||
Description: entity.Description,
|
||||
Tax: entity.Tax,
|
||||
Total: entity.Total,
|
||||
Reserved1: entity.Reserved1,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
|
||||
if entity.Items != nil {
|
||||
resp.Items = ExpenseItemEntitiesToResponses(entity.Items)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func ExpenseEntitiesToResponses(entities []*entities.Expense) []*models.ExpenseResponse {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
responses := make([]*models.ExpenseResponse, len(entities))
|
||||
for i, entity := range entities {
|
||||
responses[i] = ExpenseEntityToResponse(entity)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
func ExpenseItemEntityToResponse(entity *entities.ExpenseItem) *models.ExpenseItemResponse {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := &models.ExpenseItemResponse{
|
||||
ID: entity.ID,
|
||||
ExpenseID: entity.ExpenseID,
|
||||
ChartOfAccountID: entity.ChartOfAccountID,
|
||||
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||
Item: entity.Item,
|
||||
Description: entity.Description,
|
||||
Amount: entity.Amount,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
|
||||
if entity.ChartOfAccount != nil {
|
||||
response.ChartOfAccountName = entity.ChartOfAccount.Name
|
||||
}
|
||||
|
||||
if entity.PurchaseCategory != nil {
|
||||
response.PurchaseCategoryName = entity.PurchaseCategory.Name
|
||||
response.PurchaseCategoryType = string(entity.PurchaseCategory.Type)
|
||||
response.PurchaseCategory = PurchaseCategoryEntityToResponse(entity.PurchaseCategory)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func ExpenseItemEntitiesToResponses(entities []entities.ExpenseItem) []models.ExpenseItemResponse {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
responses := make([]models.ExpenseItemResponse, len(entities))
|
||||
for i, entity := range entities {
|
||||
response := ExpenseItemEntityToResponse(&entity)
|
||||
if response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
}
|
||||
return responses
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
func NotificationEntityToResponse(e *entities.Notification) *models.NotificationResponse {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return &models.NotificationResponse{
|
||||
ID: e.ID,
|
||||
Title: e.Title,
|
||||
Body: e.Body,
|
||||
Type: e.Type,
|
||||
Category: e.Category,
|
||||
Priority: e.Priority,
|
||||
ImageURL: e.ImageURL,
|
||||
ActionURL: e.ActionURL,
|
||||
NotifiableType: e.NotifiableType,
|
||||
NotifiableID: e.NotifiableID,
|
||||
Data: e.Data,
|
||||
ScheduledAt: e.ScheduledAt,
|
||||
SentAt: e.SentAt,
|
||||
ExpiredAt: e.ExpiredAt,
|
||||
CreatedBy: e.CreatedBy,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func NotificationReceiverEntityToResponse(e *entities.NotificationReceiver) *models.NotificationReceiverResponse {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
resp := &models.NotificationReceiverResponse{
|
||||
ID: e.ID,
|
||||
NotificationID: e.NotificationID,
|
||||
UserID: e.UserID,
|
||||
IsRead: e.IsRead,
|
||||
ReadAt: e.ReadAt,
|
||||
IsDeleted: e.IsDeleted,
|
||||
DeletedAt: e.DeletedAt,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
}
|
||||
if e.Notification != nil {
|
||||
resp.Notification = NotificationEntityToResponse(e.Notification)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func NotificationReceiverEntitiesToResponses(entities []*entities.NotificationReceiver) []*models.NotificationReceiverResponse {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
responses := make([]*models.NotificationReceiverResponse, len(entities))
|
||||
for i, e := range entities {
|
||||
responses[i] = NotificationReceiverEntityToResponse(e)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
func NotificationDeliveryEntityToResponse(e *entities.NotificationDelivery) *models.NotificationDeliveryResponse {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return &models.NotificationDeliveryResponse{
|
||||
ID: e.ID,
|
||||
NotificationReceiverID: e.NotificationReceiverID,
|
||||
UserDeviceID: e.UserDeviceID,
|
||||
Channel: e.Channel,
|
||||
DeliveryStatus: e.DeliveryStatus,
|
||||
Provider: e.Provider,
|
||||
ProviderMessageID: e.ProviderMessageID,
|
||||
SentAt: e.SentAt,
|
||||
DeliveredAt: e.DeliveredAt,
|
||||
FailedAt: e.FailedAt,
|
||||
FailureReason: e.FailureReason,
|
||||
RetryCount: e.RetryCount,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@ -82,7 +82,7 @@ func OrderEntityToResponse(order *entities.Order) *models.OrderResponse {
|
||||
}
|
||||
|
||||
for i, item := range order.OrderItems {
|
||||
resp := OrderItemEntityToResponse(&item, order.OutletID)
|
||||
resp := OrderItemEntityToResponse(&item)
|
||||
if resp != nil {
|
||||
resp.PaidQuantity = paidQtyByOrderItem[item.ID]
|
||||
response.OrderItems[i] = *resp
|
||||
@ -101,20 +101,11 @@ func OrderEntityToResponse(order *entities.Order) *models.OrderResponse {
|
||||
return response
|
||||
}
|
||||
|
||||
func OrderItemEntityToResponse(item *entities.OrderItem, outletID uuid.UUID) *models.OrderItemResponse {
|
||||
func OrderItemEntityToResponse(item *entities.OrderItem) *models.OrderItemResponse {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resolve print_to_checker from preloaded outlet prices
|
||||
printToChecker := true // default
|
||||
for _, op := range item.Product.ProductOutletPrices {
|
||||
if op.OutletID == outletID {
|
||||
printToChecker = op.PrintToChecker
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
response := &models.OrderItemResponse{
|
||||
ID: item.ID,
|
||||
OrderID: item.OrderID,
|
||||
@ -139,19 +130,10 @@ func OrderItemEntityToResponse(item *entities.OrderItem, outletID uuid.UUID) *mo
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
PrinterType: item.Product.PrinterType,
|
||||
PrintToChecker: printToChecker,
|
||||
}
|
||||
|
||||
if item.Product.ID != uuid.Nil {
|
||||
response.ProductName = item.Product.Name
|
||||
if item.Product.CategoryID != uuid.Nil {
|
||||
categoryID := item.Product.CategoryID
|
||||
response.CategoryID = &categoryID
|
||||
}
|
||||
if item.Product.Category.ID != uuid.Nil {
|
||||
categoryName := item.Product.Category.Name
|
||||
response.CategoryName = &categoryName
|
||||
}
|
||||
}
|
||||
|
||||
if item.ProductVariant != nil {
|
||||
@ -334,14 +316,14 @@ func OrderEntitiesToResponses(orders []*entities.Order) []models.OrderResponse {
|
||||
return responses
|
||||
}
|
||||
|
||||
func OrderItemEntitiesToResponses(items []*entities.OrderItem, outletID uuid.UUID) []models.OrderItemResponse {
|
||||
func OrderItemEntitiesToResponses(items []*entities.OrderItem) []models.OrderItemResponse {
|
||||
if items == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
responses := make([]models.OrderItemResponse, len(items))
|
||||
for i, item := range items {
|
||||
response := OrderItemEntityToResponse(item, outletID)
|
||||
response := OrderItemEntityToResponse(item)
|
||||
if response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ func TestOrderItemEntityToResponse_WithProductNames(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
|
||||
result := OrderItemEntityToResponse(orderItem)
|
||||
|
||||
// Assert
|
||||
assert.NotNil(t, result)
|
||||
@ -89,7 +89,7 @@ func TestOrderItemEntityToResponse_WithoutProductVariant(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
|
||||
result := OrderItemEntityToResponse(orderItem)
|
||||
|
||||
// Assert
|
||||
assert.NotNil(t, result)
|
||||
@ -129,7 +129,7 @@ func TestOrderItemEntityToResponse_WithoutProductPreload(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
|
||||
result := OrderItemEntityToResponse(orderItem)
|
||||
|
||||
// Assert
|
||||
assert.NotNil(t, result)
|
||||
|
||||
@ -11,17 +11,17 @@ func MapProductIngredientEntityToModel(entity *entities.ProductIngredient) *mode
|
||||
}
|
||||
|
||||
return &models.ProductIngredient{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
ProductID: entity.ProductID,
|
||||
IngredientID: entity.IngredientID,
|
||||
Quantity: entity.Quantity,
|
||||
WastePercentage: entity.WastePercentage,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
Product: ProductEntityToModel(entity.Product),
|
||||
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
ProductID: entity.ProductID,
|
||||
IngredientID: entity.IngredientID,
|
||||
Quantity: entity.Quantity,
|
||||
WastePercentage: entity.WastePercentage,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
Product: ProductEntityToModel(entity.Product),
|
||||
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,17 +31,17 @@ func MapProductIngredientModelToEntity(model *models.ProductIngredient) *entitie
|
||||
}
|
||||
|
||||
return &entities.ProductIngredient{
|
||||
ID: model.ID,
|
||||
OrganizationID: model.OrganizationID,
|
||||
OutletID: model.OutletID,
|
||||
ProductID: model.ProductID,
|
||||
IngredientID: model.IngredientID,
|
||||
Quantity: model.Quantity,
|
||||
WastePercentage: model.WastePercentage,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
Product: ProductModelToEntity(model.Product),
|
||||
Ingredient: MapIngredientModelToEntity(model.Ingredient),
|
||||
ID: model.ID,
|
||||
OrganizationID: model.OrganizationID,
|
||||
OutletID: model.OutletID,
|
||||
ProductID: model.ProductID,
|
||||
IngredientID: model.IngredientID,
|
||||
Quantity: model.Quantity,
|
||||
WastePercentage: model.WastePercentage,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
Product: ProductModelToEntity(model.Product),
|
||||
Ingredient: MapIngredientModelToEntity(model.Ingredient),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -135,7 +135,6 @@ func ProductEntityToResponse(entity *entities.Product) *models.ProductResponse {
|
||||
Name: entity.Name,
|
||||
Description: entity.Description,
|
||||
Price: entity.Price,
|
||||
OutletPrice: nil, // populated by processor when outletID is available
|
||||
Cost: entity.Cost,
|
||||
BusinessType: constants.BusinessType(entity.BusinessType),
|
||||
ImageURL: entity.ImageURL,
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
func ProductOutletPriceEntityToModel(entity *entities.ProductOutletPrice) *models.ProductOutletPrice {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.ProductOutletPrice{
|
||||
ID: entity.ID,
|
||||
ProductID: entity.ProductID,
|
||||
OutletID: entity.OutletID,
|
||||
Price: entity.Price,
|
||||
PrintToChecker: entity.PrintToChecker,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ProductOutletPriceModelToEntity(model *models.ProductOutletPrice) *entities.ProductOutletPrice {
|
||||
if model == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &entities.ProductOutletPrice{
|
||||
ID: model.ID,
|
||||
ProductID: model.ProductID,
|
||||
OutletID: model.OutletID,
|
||||
Price: model.Price,
|
||||
PrintToChecker: model.PrintToChecker,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ProductOutletPriceEntitiesToModels(entities []*entities.ProductOutletPrice) []*models.ProductOutletPrice {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
models := make([]*models.ProductOutletPrice, len(entities))
|
||||
for i, entity := range entities {
|
||||
models[i] = ProductOutletPriceEntityToModel(entity)
|
||||
}
|
||||
return models
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
func CreatePurchaseCategoryRequestToEntity(req *models.CreatePurchaseCategoryRequest) *entities.PurchaseCategory {
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &entities.PurchaseCategory{
|
||||
OrganizationID: req.OrganizationID,
|
||||
ParentID: req.ParentID,
|
||||
Name: req.Name,
|
||||
Type: entities.PurchaseCategoryType(req.Type),
|
||||
SortOrder: req.SortOrder,
|
||||
IsActive: req.IsActive,
|
||||
}
|
||||
}
|
||||
|
||||
func PurchaseCategoryEntityToResponse(entity *entities.PurchaseCategory) *models.PurchaseCategoryResponse {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.PurchaseCategoryResponse{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
PresetID: entity.PresetID,
|
||||
ParentID: entity.ParentID,
|
||||
Code: entity.Code,
|
||||
Name: entity.Name,
|
||||
Type: string(entity.Type),
|
||||
SortOrder: entity.SortOrder,
|
||||
IsSystem: entity.IsSystem,
|
||||
IsActive: entity.IsActive,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func PurchaseCategoryEntitiesToResponses(categoryEntities []*entities.PurchaseCategory) []models.PurchaseCategoryResponse {
|
||||
responses := make([]models.PurchaseCategoryResponse, len(categoryEntities))
|
||||
for i, entity := range categoryEntities {
|
||||
response := PurchaseCategoryEntityToResponse(entity)
|
||||
if response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
}
|
||||
return responses
|
||||
}
|
||||
@ -13,7 +13,6 @@ func PurchaseOrderEntityToModel(entity *entities.PurchaseOrder) *models.Purchase
|
||||
return &models.PurchaseOrder{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
VendorID: entity.VendorID,
|
||||
PONumber: entity.PONumber,
|
||||
TransactionDate: entity.TransactionDate,
|
||||
@ -35,7 +34,6 @@ func PurchaseOrderModelToEntity(model *models.PurchaseOrder) *entities.PurchaseO
|
||||
return &entities.PurchaseOrder{
|
||||
ID: model.ID,
|
||||
OrganizationID: model.OrganizationID,
|
||||
OutletID: model.OutletID,
|
||||
VendorID: model.VendorID,
|
||||
PONumber: model.PONumber,
|
||||
TransactionDate: model.TransactionDate,
|
||||
@ -57,7 +55,6 @@ func PurchaseOrderEntityToResponse(entity *entities.PurchaseOrder) *models.Purch
|
||||
response := &models.PurchaseOrderResponse{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
VendorID: entity.VendorID,
|
||||
PONumber: entity.PONumber,
|
||||
TransactionDate: entity.TransactionDate,
|
||||
@ -94,16 +91,15 @@ func PurchaseOrderItemEntityToModel(entity *entities.PurchaseOrderItem) *models.
|
||||
}
|
||||
|
||||
return &models.PurchaseOrderItem{
|
||||
ID: entity.ID,
|
||||
PurchaseOrderID: entity.PurchaseOrderID,
|
||||
IngredientID: entity.IngredientID,
|
||||
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||
Description: entity.Description,
|
||||
Quantity: entity.Quantity,
|
||||
UnitID: entity.UnitID,
|
||||
Amount: entity.Amount,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,16 +109,15 @@ func PurchaseOrderItemModelToEntity(model *models.PurchaseOrderItem) *entities.P
|
||||
}
|
||||
|
||||
return &entities.PurchaseOrderItem{
|
||||
ID: model.ID,
|
||||
PurchaseOrderID: model.PurchaseOrderID,
|
||||
IngredientID: model.IngredientID,
|
||||
PurchaseCategoryID: model.PurchaseCategoryID,
|
||||
Description: model.Description,
|
||||
Quantity: model.Quantity,
|
||||
UnitID: model.UnitID,
|
||||
Amount: model.Amount,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,16 +127,15 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
|
||||
}
|
||||
|
||||
response := &models.PurchaseOrderItemResponse{
|
||||
ID: entity.ID,
|
||||
PurchaseOrderID: entity.PurchaseOrderID,
|
||||
IngredientID: entity.IngredientID,
|
||||
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||
Description: entity.Description,
|
||||
Quantity: entity.Quantity,
|
||||
UnitID: entity.UnitID,
|
||||
Amount: entity.Amount,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
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
|
||||
@ -152,10 +146,6 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
|
||||
}
|
||||
}
|
||||
|
||||
if entity.PurchaseCategory != nil {
|
||||
response.PurchaseCategory = PurchaseCategoryEntityToResponse(entity.PurchaseCategory)
|
||||
}
|
||||
|
||||
// Map unit if present
|
||||
if entity.Unit != nil {
|
||||
response.Unit = &models.UnitResponse{
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
func UserDeviceEntityToModel(entity *entities.UserDevice) *models.UserDevice {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.UserDevice{
|
||||
ID: entity.ID,
|
||||
UserID: entity.UserID,
|
||||
DeviceID: entity.DeviceID,
|
||||
DeviceName: entity.DeviceName,
|
||||
DeviceType: entity.DeviceType,
|
||||
Platform: entity.Platform,
|
||||
FCMToken: entity.FCMToken,
|
||||
AppVersion: entity.AppVersion,
|
||||
OsVersion: entity.OsVersion,
|
||||
IPAddress: entity.IPAddress,
|
||||
LastActiveAt: entity.LastActiveAt,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func UserDeviceEntityToResponse(entity *entities.UserDevice) *models.UserDeviceResponse {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.UserDeviceResponse{
|
||||
ID: entity.ID,
|
||||
UserID: entity.UserID,
|
||||
DeviceID: entity.DeviceID,
|
||||
DeviceName: entity.DeviceName,
|
||||
DeviceType: entity.DeviceType,
|
||||
Platform: entity.Platform,
|
||||
FCMToken: entity.FCMToken,
|
||||
AppVersion: entity.AppVersion,
|
||||
OsVersion: entity.OsVersion,
|
||||
IPAddress: entity.IPAddress,
|
||||
LastActiveAt: entity.LastActiveAt,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func UserDeviceEntitiesToResponses(entities []*entities.UserDevice) []*models.UserDeviceResponse {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
responses := make([]*models.UserDeviceResponse, len(entities))
|
||||
for i, entity := range entities {
|
||||
responses[i] = UserDeviceEntityToResponse(entity)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
@ -11,7 +11,6 @@ import (
|
||||
"apskel-pos-be/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AuthMiddleware struct {
|
||||
@ -46,13 +45,9 @@ func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
|
||||
setKeyInContext(c, appcontext.OrganizationIDKey, userResponse.OrganizationID.String())
|
||||
setKeyInContext(c, appcontext.UserIDKey, userResponse.ID.String())
|
||||
|
||||
// Always override OutletID from token to prevent header injection.
|
||||
// Set empty string if user has no outlet, so PopulateContext header value is ignored.
|
||||
outletIDStr := ""
|
||||
if userResponse.OutletID != nil && *userResponse.OutletID != uuid.Nil {
|
||||
outletIDStr = userResponse.OutletID.String()
|
||||
if userResponse.Role != "superadmin" {
|
||||
setKeyInContext(c, appcontext.OutletIDKey, userResponse.OutletID.String())
|
||||
}
|
||||
setKeyInContext(c, appcontext.OutletIDKey, outletIDStr)
|
||||
|
||||
logger.FromContext(c.Request.Context()).Infof("AuthMiddleware::RequireAuth -> User authenticated: %s", userResponse.Email)
|
||||
c.Next()
|
||||
@ -82,11 +77,7 @@ func (m *AuthMiddleware) RequireRole(allowedRoles ...string) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) RequireAdminOrManager() gin.HandlerFunc {
|
||||
return m.RequireRole("superadmin", "admin", "manager", "owner", "purchasing")
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) RequireAdminOrManagerOrPurchasing() gin.HandlerFunc {
|
||||
return m.RequireRole("superadmin", "admin", "manager", "owner", "purchasing")
|
||||
return m.RequireRole("superadmin", "admin", "manager")
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) RequireAdmin() gin.HandlerFunc {
|
||||
|
||||
@ -1,137 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const (
|
||||
IdempotencyKeyHeader = "X-Idempotency-Key"
|
||||
idempotencyTTL = 24 * time.Hour
|
||||
idempotencyPrefix = "idempotency:"
|
||||
)
|
||||
|
||||
type cachedResponse struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
// IdempotencyMiddleware returns a Gin middleware that ensures idempotent processing
|
||||
// for mutating operations. Client must send X-Idempotency-Key header.
|
||||
func IdempotencyMiddleware(redisClient *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
key := c.GetHeader(IdempotencyKeyHeader)
|
||||
if key == "" {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"errors": []gin.H{
|
||||
{
|
||||
"code": "missing_idempotency_key",
|
||||
"entity": "IdempotencyMiddleware",
|
||||
"cause": "X-Idempotency-Key header is required",
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
redisKey := fmt.Sprintf("%s%s", idempotencyPrefix, key)
|
||||
ctx := context.Background()
|
||||
|
||||
fmt.Printf("[DEBUG] IdempotencyMiddleware: key=%s redisKey=%s\n", key, redisKey)
|
||||
|
||||
// Check if key already exists (request was already processed)
|
||||
cached, err := redisClient.Get(ctx, redisKey).Result()
|
||||
if err == nil {
|
||||
// Key exists — return cached response
|
||||
fmt.Printf("[DEBUG] IdempotencyMiddleware: cache HIT for key=%s\n", key)
|
||||
var resp cachedResponse
|
||||
if err := json.Unmarshal([]byte(cached), &resp); err == nil {
|
||||
for k, v := range resp.Headers {
|
||||
c.Writer.Header().Set(k, v)
|
||||
}
|
||||
c.Writer.Header().Set("X-Idempotent-Replay", "true")
|
||||
c.Data(resp.StatusCode, "application/json", []byte(resp.Body))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] IdempotencyMiddleware: cache MISS for key=%s err=%v\n", key, err)
|
||||
}
|
||||
|
||||
// Mark key as in-progress to prevent concurrent duplicates
|
||||
set, err := redisClient.SetNX(ctx, redisKey, "processing", idempotencyTTL).Result()
|
||||
if err != nil {
|
||||
// Redis error — proceed without idempotency (fail open)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
if !set {
|
||||
// Another request with the same key is being processed
|
||||
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
|
||||
"success": false,
|
||||
"errors": []gin.H{
|
||||
{
|
||||
"code": "request_in_progress",
|
||||
"entity": "IdempotencyMiddleware",
|
||||
"cause": "A request with this idempotency key is already being processed",
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Capture response using a custom writer
|
||||
writer := &responseCapture{
|
||||
ResponseWriter: c.Writer,
|
||||
body: &bytes.Buffer{},
|
||||
}
|
||||
c.Writer = writer
|
||||
|
||||
c.Next()
|
||||
|
||||
// After handler completes, cache the response only if successful (2xx)
|
||||
statusCode := writer.Status()
|
||||
if statusCode >= 200 && statusCode < 300 {
|
||||
resp := cachedResponse{
|
||||
StatusCode: statusCode,
|
||||
Headers: map[string]string{
|
||||
"Content-Type": writer.Header().Get("Content-Type"),
|
||||
},
|
||||
Body: writer.body.String(),
|
||||
}
|
||||
|
||||
respJSON, err := json.Marshal(resp)
|
||||
if err == nil {
|
||||
redisClient.Set(ctx, redisKey, string(respJSON), idempotencyTTL)
|
||||
}
|
||||
} else {
|
||||
// Remove the in-progress key so the client can retry with the same key
|
||||
redisClient.Del(ctx, redisKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// responseCapture wraps gin.ResponseWriter to capture the response body
|
||||
type responseCapture struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *responseCapture) Write(b []byte) (int, error) {
|
||||
w.body.Write(b)
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func (w *responseCapture) WriteString(s string) (int, error) {
|
||||
w.body.WriteString(s)
|
||||
return w.ResponseWriter.WriteString(s)
|
||||
}
|
||||
@ -25,12 +25,12 @@ type AccountResponse struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
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 {
|
||||
|
||||
@ -19,7 +19,6 @@ type PaymentMethodAnalyticsRequest struct {
|
||||
type PaymentMethodAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
@ -59,7 +58,6 @@ type SalesAnalyticsRequest struct {
|
||||
type SalesAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
@ -89,77 +87,6 @@ type SalesAnalyticsData struct {
|
||||
NetSales float64 `json:"net_sales"`
|
||||
}
|
||||
|
||||
// PurchasingAnalyticsRequest represents the request for purchasing analytics
|
||||
type PurchasingAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID `validate:"required"`
|
||||
OutletID *uuid.UUID `validate:"omitempty"`
|
||||
DateFrom time.Time `validate:"required"`
|
||||
DateTo time.Time `validate:"required"`
|
||||
GroupBy string `validate:"omitempty,oneof=day hour week month"`
|
||||
}
|
||||
|
||||
// PurchasingAnalyticsResponse represents the response for purchasing analytics
|
||||
type PurchasingAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
Summary PurchasingSummary `json:"summary"`
|
||||
Data []PurchasingAnalyticsData `json:"data"`
|
||||
IngredientData []PurchasingIngredientData `json:"ingredient_data"`
|
||||
VendorData []PurchasingVendorData `json:"vendor_data"`
|
||||
}
|
||||
|
||||
// PurchasingSummary represents the summary of purchasing analytics
|
||||
type PurchasingSummary struct {
|
||||
TotalPurchases float64 `json:"total_purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
TotalQuantity float64 `json:"total_quantity"`
|
||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||
TotalIngredients int64 `json:"total_ingredients"`
|
||||
TotalVendors int64 `json:"total_vendors"`
|
||||
}
|
||||
|
||||
// PurchasingAnalyticsData represents purchasing analytics by time period
|
||||
type PurchasingAnalyticsData struct {
|
||||
Date time.Time `json:"date"`
|
||||
Purchases float64 `json:"purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
PurchaseOrders int64 `json:"purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Ingredients int64 `json:"ingredients"`
|
||||
Vendors int64 `json:"vendors"`
|
||||
}
|
||||
|
||||
// PurchasingIngredientData represents purchasing analytics for an ingredient
|
||||
type PurchasingIngredientData struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
IngredientName string `json:"ingredient_name"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
AverageUnitCost float64 `json:"average_unit_cost"`
|
||||
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||
}
|
||||
|
||||
// PurchasingVendorData represents purchasing analytics for a vendor
|
||||
type PurchasingVendorData struct {
|
||||
VendorID *uuid.UUID `json:"vendor_id"`
|
||||
VendorName string `json:"vendor_name"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||
IngredientCount int64 `json:"ingredient_count"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
}
|
||||
|
||||
// ProductAnalyticsRequest represents the request for product analytics
|
||||
type ProductAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID `validate:"required"`
|
||||
@ -173,7 +100,6 @@ type ProductAnalyticsRequest struct {
|
||||
type ProductAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
Data []ProductAnalyticsData `json:"data"`
|
||||
@ -183,7 +109,6 @@ type ProductAnalyticsData struct {
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
ProductSku string `json:"product_sku"`
|
||||
ProductPrice float64 `json:"product_price"`
|
||||
CategoryID uuid.UUID `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
CategoryOrder int `json:"category_order"`
|
||||
@ -211,7 +136,6 @@ type ProductAnalyticsPerCategoryRequest struct {
|
||||
type ProductAnalyticsPerCategoryResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
Data []ProductAnalyticsPerCategoryData `json:"data"`
|
||||
@ -241,7 +165,6 @@ type DashboardAnalyticsRequest struct {
|
||||
type DashboardAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
Overview DashboardOverview `json:"overview"`
|
||||
@ -252,17 +175,15 @@ type DashboardAnalyticsResponse struct {
|
||||
|
||||
// DashboardOverview represents the overview data for dashboard
|
||||
type DashboardOverview struct {
|
||||
TotalSales float64 `json:"total_sales"`
|
||||
TotalOrders int64 `json:"total_orders"`
|
||||
AverageOrderValue float64 `json:"average_order_value"`
|
||||
TotalCustomers int64 `json:"total_customers"`
|
||||
VoidedOrders int64 `json:"voided_orders"`
|
||||
RefundedOrders int64 `json:"refunded_orders"`
|
||||
TotalItemSold int64 `json:"total_item_sold"`
|
||||
TotalLowStock int64 `json:"total_low_stock"`
|
||||
TotalProductActive int64 `json:"total_product_active"`
|
||||
TotalSales float64 `json:"total_sales"`
|
||||
TotalOrders int64 `json:"total_orders"`
|
||||
AverageOrderValue float64 `json:"average_order_value"`
|
||||
TotalCustomers int64 `json:"total_customers"`
|
||||
VoidedOrders int64 `json:"voided_orders"`
|
||||
RefundedOrders int64 `json:"refunded_orders"`
|
||||
}
|
||||
|
||||
// ProfitLossAnalyticsRequest represents the request for profit and loss analytics
|
||||
type ProfitLossAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID `validate:"required"`
|
||||
OutletID *uuid.UUID `validate:"omitempty"`
|
||||
@ -271,39 +192,19 @@ type ProfitLossAnalyticsRequest struct {
|
||||
GroupBy string `validate:"omitempty,oneof=day hour week month"`
|
||||
}
|
||||
|
||||
// ProfitLossAnalyticsResponse represents the response for profit and loss analytics
|
||||
type ProfitLossAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
Summary ProfitLossSummary `json:"summary"`
|
||||
Data []ProfitLossData `json:"data"`
|
||||
ProductData []ProductProfitData `json:"product_data"`
|
||||
MainSummary []ProfitLossSummaryRow `json:"main_summary"`
|
||||
Purchasing ProfitLossPurchasing `json:"purchasing"`
|
||||
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
|
||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||
}
|
||||
|
||||
type ProfitLossPurchasing struct {
|
||||
TodayTotal float64 `json:"today_total"`
|
||||
MtdTotal float64 `json:"mtd_total"`
|
||||
TodayRawMaterial float64 `json:"today_raw_material"`
|
||||
MtdRawMaterial float64 `json:"mtd_raw_material"`
|
||||
TodayExpense float64 `json:"today_expense"`
|
||||
MtdExpense float64 `json:"mtd_expense"`
|
||||
Items []ProfitLossPurchasingItem `json:"items"`
|
||||
}
|
||||
|
||||
type ProfitLossPurchasingItem struct {
|
||||
Date time.Time `json:"date"`
|
||||
Item string `json:"item"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Nominal float64 `json:"nominal"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
Summary ProfitLossSummary `json:"summary"`
|
||||
Data []ProfitLossData `json:"data"`
|
||||
ProductData []ProductProfitData `json:"product_data"`
|
||||
}
|
||||
|
||||
// ProfitLossSummary represents the summary of profit and loss analytics
|
||||
type ProfitLossSummary struct {
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
@ -318,6 +219,7 @@ type ProfitLossSummary struct {
|
||||
ProfitabilityRatio float64 `json:"profitability_ratio"`
|
||||
}
|
||||
|
||||
// ProfitLossData represents individual profit and loss data point by time period
|
||||
type ProfitLossData struct {
|
||||
Date time.Time `json:"date"`
|
||||
Revenue float64 `json:"revenue"`
|
||||
@ -331,6 +233,7 @@ type ProfitLossData struct {
|
||||
Orders int64 `json:"orders"`
|
||||
}
|
||||
|
||||
// ProductProfitData represents profit data for individual products
|
||||
type ProductProfitData struct {
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
@ -345,139 +248,3 @@ type ProductProfitData struct {
|
||||
AverageCost float64 `json:"average_cost"`
|
||||
ProfitPerUnit float64 `json:"profit_per_unit"`
|
||||
}
|
||||
|
||||
type ProfitLossSummaryRow struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
IsBold bool `json:"is_bold"`
|
||||
TodayNominal float64 `json:"today_nominal"`
|
||||
TodayPct float64 `json:"today_pct"`
|
||||
MtdNominal float64 `json:"mtd_nominal"`
|
||||
MtdPct float64 `json:"mtd_pct"`
|
||||
SubItems []ProfitLossSummaryRow `json:"sub_items,omitempty"`
|
||||
}
|
||||
|
||||
type OperationalExpenseItem struct {
|
||||
Item string `json:"item"`
|
||||
Nominal float64 `json:"nominal"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryPeriodRequest struct {
|
||||
OrganizationID uuid.UUID `validate:"required"`
|
||||
OutletID *uuid.UUID `validate:"omitempty"`
|
||||
DateFrom time.Time `validate:"required"`
|
||||
DateTo time.Time `validate:"required"`
|
||||
ExcludeGajiStaffFromReimburse bool `validate:"omitempty"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryMonthlyRequest struct {
|
||||
OrganizationID uuid.UUID `validate:"required"`
|
||||
OutletID *uuid.UUID `validate:"omitempty"`
|
||||
Month time.Time `validate:"required"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryMTDRequest struct {
|
||||
OrganizationID uuid.UUID `validate:"required"`
|
||||
OutletID *uuid.UUID `validate:"omitempty"`
|
||||
DateTo time.Time `validate:"required"`
|
||||
ExcludeGajiStaffFromReimburse bool `validate:"omitempty"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryPeriodResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
Period ExclusiveSummaryPeriodRange `json:"period"`
|
||||
Summary ExclusiveSummaryPeriodSummary `json:"summary"`
|
||||
Reimburse ExclusiveSummaryReimburse `json:"reimburse"`
|
||||
HPPBreakdown []ExclusiveSummaryCategoryBreakdown `json:"hpp_breakdown"`
|
||||
OperationalExpenseBreakdown []ExclusiveSummaryCategoryBreakdown `json:"operational_expense_breakdown"`
|
||||
DailySummary []ExclusiveSummaryDailySummary `json:"daily_summary"`
|
||||
DailyTransactions []ExclusiveSummaryDailyTransaction `json:"daily_transactions"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryPeriodRange struct {
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryPeriodSummary struct {
|
||||
Sales float64 `json:"sales"`
|
||||
HPP float64 `json:"hpp"`
|
||||
GrossProfit float64 `json:"gross_profit"`
|
||||
SalaryTotal float64 `json:"salary_total"`
|
||||
SalaryDW float64 `json:"salary_dw"`
|
||||
SalaryStaff float64 `json:"salary_staff"`
|
||||
SalaryOther float64 `json:"salary_other"`
|
||||
OtherOperationalExpenses float64 `json:"other_operational_expenses"`
|
||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
NetProfit float64 `json:"net_profit"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryReimburse struct {
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
ExcludedSalaryStaff float64 `json:"excluded_salary_staff"`
|
||||
TotalReimburse float64 `json:"total_reimburse"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryCategoryBreakdown struct {
|
||||
CategoryCode string `json:"category_code"`
|
||||
CategoryName string `json:"category_name"`
|
||||
Amount float64 `json:"amount"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryDailySummary struct {
|
||||
Date time.Time `json:"date"`
|
||||
TransactionCount int64 `json:"transaction_count"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryDailyTransaction struct {
|
||||
Date time.Time `json:"date"`
|
||||
CategoryCode string `json:"category_code"`
|
||||
CategoryName string `json:"category_name"`
|
||||
Description string `json:"description"`
|
||||
Amount float64 `json:"amount"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryMonthlyResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
Month string `json:"month"`
|
||||
Summary ExclusiveSummaryMonthlySummary `json:"summary"`
|
||||
Periods []ExclusiveSummaryMonthlyPeriod `json:"periods"`
|
||||
BankBalance []ExclusiveSummaryBankBalance `json:"bank_balance"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryMonthlySummary struct {
|
||||
TotalSales float64 `json:"total_sales"`
|
||||
HPP float64 `json:"hpp"`
|
||||
GrossProfit float64 `json:"gross_profit"`
|
||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
NetProfit float64 `json:"net_profit"`
|
||||
NetProfitMargin float64 `json:"net_profit_margin"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryMonthlyPeriod struct {
|
||||
Label string `json:"label"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
Sales float64 `json:"sales"`
|
||||
HPP float64 `json:"hpp"`
|
||||
GrossProfit float64 `json:"gross_profit"`
|
||||
GrossMargin float64 `json:"gross_margin"`
|
||||
}
|
||||
|
||||
type ExclusiveSummaryBankBalance struct {
|
||||
Bank string `json:"bank"`
|
||||
OpeningBalance *float64 `json:"opening_balance"`
|
||||
IncomingMutation *float64 `json:"incoming_mutation"`
|
||||
OutgoingMutation *float64 `json:"outgoing_mutation"`
|
||||
ClosingBalance *float64 `json:"closing_balance"`
|
||||
Notes *string `json:"notes"`
|
||||
}
|
||||
|
||||
@ -9,11 +9,10 @@ import (
|
||||
type Category struct {
|
||||
ID uuid.UUID
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *uuid.UUID
|
||||
Name string
|
||||
Description *string
|
||||
ImageURL *string
|
||||
Order int
|
||||
Order int
|
||||
IsActive bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
@ -21,30 +20,27 @@ type Category struct {
|
||||
|
||||
type CreateCategoryRequest struct {
|
||||
OrganizationID uuid.UUID `validate:"required"`
|
||||
OutletID *uuid.UUID
|
||||
Name string `validate:"required,min=1,max=255"`
|
||||
Description *string `validate:"omitempty,max=1000"`
|
||||
ImageURL *string `validate:"omitempty,url"`
|
||||
Order int `validate:"min=0"`
|
||||
Name string `validate:"required,min=1,max=255"`
|
||||
Description *string `validate:"omitempty,max=1000"`
|
||||
ImageURL *string `validate:"omitempty,url"`
|
||||
Order int `validate:"min=0"`
|
||||
}
|
||||
|
||||
type UpdateCategoryRequest struct {
|
||||
Name *string `validate:"omitempty,min=1,max=255"`
|
||||
Description *string `validate:"omitempty,max=1000"`
|
||||
ImageURL *string `validate:"omitempty,url"`
|
||||
OutletID *uuid.UUID
|
||||
Order *int `validate:"omitempty,min=0"`
|
||||
Order *int `validate:"omitempty,min=0"`
|
||||
IsActive *bool
|
||||
}
|
||||
|
||||
type CategoryResponse struct {
|
||||
ID uuid.UUID
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *uuid.UUID
|
||||
Name string
|
||||
Description *string
|
||||
ImageURL *string
|
||||
Order int
|
||||
Order int
|
||||
IsActive bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
||||
@ -23,17 +23,17 @@ type UpdateCustomerRequest struct {
|
||||
}
|
||||
|
||||
type CustomerResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
Name string `json:"name"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
Address *string `json:"address,omitempty"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Metadata entities.Metadata `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
Name string `json:"name"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
Address *string `json:"address,omitempty"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Metadata entities.Metadata `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ListCustomersQuery represents query parameters for listing customers
|
||||
|
||||
@ -1,190 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Expense struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID uuid.UUID `json:"outlet_id"`
|
||||
Receiver string `json:"receiver"`
|
||||
TransactionDate time.Time `json:"transaction_date"`
|
||||
CodeNumber string `json:"code_number"`
|
||||
Status string `json:"status"`
|
||||
Description *string `json:"description"`
|
||||
Tax float64 `json:"tax"`
|
||||
Total float64 `json:"total"`
|
||||
Reserved1 *string `json:"reserved1"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ExpenseItem struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ExpenseID uuid.UUID `json:"expense_id"`
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
Item string `json:"item"`
|
||||
Description *string `json:"description"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ExpenseResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID uuid.UUID `json:"outlet_id"`
|
||||
Receiver string `json:"receiver"`
|
||||
TransactionDate time.Time `json:"transaction_date"`
|
||||
CodeNumber string `json:"code_number"`
|
||||
Status string `json:"status"`
|
||||
Description *string `json:"description"`
|
||||
Tax float64 `json:"tax"`
|
||||
Total float64 `json:"total"`
|
||||
Reserved1 *string `json:"reserved1"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Items []ExpenseItemResponse `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
type ExpenseItemResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ExpenseID uuid.UUID `json:"expense_id"`
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
PurchaseCategoryName string `json:"purchase_category_name,omitempty"`
|
||||
PurchaseCategoryType string `json:"purchase_category_type,omitempty"`
|
||||
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||
Item string `json:"item"`
|
||||
Description *string `json:"description"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CreateExpenseRequest struct {
|
||||
Receiver string `json:"receiver"`
|
||||
TransactionDate string `json:"transaction_date"`
|
||||
CodeNumber string `json:"code_number"`
|
||||
OutletID string `json:"outlet_id"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
Description *string `json:"description"`
|
||||
Tax float64 `json:"tax"`
|
||||
Total float64 `json:"total"`
|
||||
Items []CreateExpenseItemRequest `json:"items"`
|
||||
}
|
||||
|
||||
type CreateExpenseItemRequest struct {
|
||||
ChartOfAccountID string `json:"chart_of_account_id"`
|
||||
PurchaseCategoryID string `json:"purchase_category_id"`
|
||||
Item string `json:"item"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
type UpdateExpenseRequest struct {
|
||||
Receiver *string `json:"receiver,omitempty"`
|
||||
TransactionDate *string `json:"transaction_date,omitempty"`
|
||||
CodeNumber *string `json:"code_number,omitempty"`
|
||||
OutletID *string `json:"outlet_id,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Tax *float64 `json:"tax,omitempty"`
|
||||
Total *float64 `json:"total,omitempty"`
|
||||
Reserved1 *string `json:"reserved1,omitempty"`
|
||||
Items []UpdateExpenseItemRequest `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateExpenseItemRequest struct {
|
||||
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
||||
PurchaseCategoryID *string `json:"purchase_category_id,omitempty"`
|
||||
Item *string `json:"item,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Amount *float64 `json:"amount,omitempty"`
|
||||
}
|
||||
|
||||
type ListExpenseRequest struct {
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
Search string `json:"search,omitempty"`
|
||||
OutletID string `json:"outlet_id,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
StartDate string `json:"start_date,omitempty"`
|
||||
EndDate string `json:"end_date,omitempty"`
|
||||
}
|
||||
|
||||
type ListExpenseResponse struct {
|
||||
Expenses []*ExpenseResponse `json:"expenses"`
|
||||
TotalCount int `json:"total_count"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
Summary ExpenseAnalyticsSummary `json:"summary"`
|
||||
Data []ExpenseAnalyticsData `json:"data"`
|
||||
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
|
||||
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData `json:"chart_of_account_data"`
|
||||
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsSummary struct {
|
||||
TotalExpenses float64 `json:"total_expenses"`
|
||||
TotalExpenseCount int64 `json:"total_expense_count"`
|
||||
TotalTax float64 `json:"total_tax"`
|
||||
AverageExpenseValue float64 `json:"average_expense_value"`
|
||||
TotalCategories int64 `json:"total_categories"`
|
||||
TotalItems int64 `json:"total_items"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsData struct {
|
||||
Date time.Time `json:"date"`
|
||||
Expenses float64 `json:"expenses"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
Tax float64 `json:"tax"`
|
||||
Items int64 `json:"items"`
|
||||
Categories int64 `json:"categories"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsCategoryData struct {
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
PurchaseCategoryName string `json:"purchase_category_name"`
|
||||
PurchaseCategoryType string `json:"purchase_category_type"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
ItemCount int64 `json:"item_count"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsChartOfAccountData struct {
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
ChartOfAccountName string `json:"chart_of_account_name"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
ItemCount int64 `json:"item_count"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsItemData struct {
|
||||
Item string `json:"item"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
ItemCount int64 `json:"item_count"`
|
||||
}
|
||||
@ -101,3 +101,4 @@ type IngredientUnitsResponse struct {
|
||||
BaseUnitName string `json:"base_unit_name"`
|
||||
Units []*UnitResponse `json:"units"`
|
||||
}
|
||||
|
||||
|
||||
@ -1,118 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ---- Request models ----
|
||||
|
||||
type SendNotificationRequest struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Type string `json:"type"`
|
||||
Category string `json:"category"`
|
||||
Priority entities.NotificationPriority `json:"priority"`
|
||||
ImageURL string `json:"image_url"`
|
||||
ActionURL string `json:"action_url"`
|
||||
NotifiableType string `json:"notifiable_type"`
|
||||
NotifiableID *uuid.UUID `json:"notifiable_id"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
ReceiverIDs []uuid.UUID `json:"receiver_ids"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
CreatedBy *uuid.UUID `json:"created_by"`
|
||||
}
|
||||
|
||||
type BroadcastNotificationRequest struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Type string `json:"type"`
|
||||
Category string `json:"category"`
|
||||
Priority entities.NotificationPriority `json:"priority"`
|
||||
ImageURL string `json:"image_url"`
|
||||
ActionURL string `json:"action_url"`
|
||||
NotifiableType string `json:"notifiable_type"`
|
||||
NotifiableID *uuid.UUID `json:"notifiable_id"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
CreatedBy *uuid.UUID `json:"created_by"`
|
||||
}
|
||||
|
||||
type MarkNotificationReadRequest struct {
|
||||
NotificationReceiverID uuid.UUID `json:"notification_receiver_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
}
|
||||
|
||||
type ListNotificationsRequest struct {
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
IsRead *bool `json:"is_read"`
|
||||
}
|
||||
|
||||
// ---- Response models ----
|
||||
|
||||
type NotificationResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Type string `json:"type"`
|
||||
Category string `json:"category"`
|
||||
Priority entities.NotificationPriority `json:"priority"`
|
||||
ImageURL string `json:"image_url"`
|
||||
ActionURL string `json:"action_url"`
|
||||
NotifiableType string `json:"notifiable_type"`
|
||||
NotifiableID *uuid.UUID `json:"notifiable_id"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at"`
|
||||
SentAt *time.Time `json:"sent_at"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
CreatedBy *uuid.UUID `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type NotificationReceiverResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
NotificationID uuid.UUID `json:"notification_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
IsRead bool `json:"is_read"`
|
||||
ReadAt *time.Time `json:"read_at"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
DeletedAt *time.Time `json:"deleted_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Notification *NotificationResponse `json:"notification,omitempty"`
|
||||
}
|
||||
|
||||
type NotificationDeliveryResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
NotificationReceiverID uuid.UUID `json:"notification_receiver_id"`
|
||||
UserDeviceID uuid.UUID `json:"user_device_id"`
|
||||
Channel entities.NotificationChannel `json:"channel"`
|
||||
DeliveryStatus entities.NotificationDeliveryStatus `json:"delivery_status"`
|
||||
Provider entities.NotificationProvider `json:"provider"`
|
||||
ProviderMessageID string `json:"provider_message_id"`
|
||||
SentAt *time.Time `json:"sent_at"`
|
||||
DeliveredAt *time.Time `json:"delivered_at"`
|
||||
FailedAt *time.Time `json:"failed_at"`
|
||||
FailureReason string `json:"failure_reason"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ListNotificationsResponse struct {
|
||||
Notifications []*NotificationReceiverResponse `json:"notifications"`
|
||||
TotalCount int `json:"total_count"`
|
||||
UnreadCount int `json:"unread_count"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
@ -188,8 +188,6 @@ type OrderItemResponse struct {
|
||||
ProductName string
|
||||
ProductVariantID *uuid.UUID
|
||||
ProductVariantName *string
|
||||
CategoryID *uuid.UUID
|
||||
CategoryName *string
|
||||
Quantity int
|
||||
UnitPrice float64
|
||||
TotalPrice float64
|
||||
@ -209,7 +207,6 @@ type OrderItemResponse struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
PrinterType string
|
||||
PrintToChecker bool
|
||||
PaidQuantity int
|
||||
}
|
||||
|
||||
|
||||
@ -52,8 +52,8 @@ type UpdateOrderIngredientTransactionRequest struct {
|
||||
GrossQty *float64 `json:"gross_qty,omitempty" validate:"omitempty,gt=0"`
|
||||
NetQty *float64 `json:"net_qty,omitempty" validate:"omitempty,gt=0"`
|
||||
WasteQty *float64 `json:"waste_qty,omitempty" validate:"min=0"`
|
||||
Unit *string `json:"unit,omitempty" validate:"omitempty,max=50"`
|
||||
TransactionDate *time.Time `json:"transaction_date,omitempty"`
|
||||
Unit *string `json:"unit,omitempty" validate:"omitempty,max=50"`
|
||||
TransactionDate *time.Time `json:"transaction_date,omitempty"`
|
||||
}
|
||||
|
||||
type OrderIngredientTransactionResponse struct {
|
||||
@ -98,11 +98,11 @@ type ListOrderIngredientTransactionsRequest struct {
|
||||
}
|
||||
|
||||
type OrderIngredientTransactionSummary struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
IngredientName string `json:"ingredient_name"`
|
||||
TotalGrossQty float64 `json:"total_gross_qty"`
|
||||
TotalNetQty float64 `json:"total_net_qty"`
|
||||
TotalWasteQty float64 `json:"total_waste_qty"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
Unit string `json:"unit"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
IngredientName string `json:"ingredient_name"`
|
||||
TotalGrossQty float64 `json:"total_gross_qty"`
|
||||
TotalNetQty float64 `json:"total_net_qty"`
|
||||
TotalWasteQty float64 `json:"total_waste_qty"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
Unit string `json:"unit"`
|
||||
}
|
||||
|
||||
@ -40,7 +40,6 @@ type ProductVariant struct {
|
||||
|
||||
type CreateProductRequest struct {
|
||||
OrganizationID uuid.UUID `validate:"required"`
|
||||
OutletID uuid.UUID `validate:"omitempty"` // If set, upsert product_outlet_prices on create
|
||||
CategoryID uuid.UUID `validate:"required"`
|
||||
SKU *string `validate:"omitempty,max=100"`
|
||||
Name string `validate:"required,min=1,max=255"`
|
||||
@ -50,7 +49,6 @@ type CreateProductRequest struct {
|
||||
BusinessType constants.BusinessType `validate:"required"`
|
||||
ImageURL *string `validate:"omitempty,max=500"`
|
||||
PrinterType *string `validate:"omitempty,max=50"`
|
||||
PrintToChecker *bool `validate:"omitempty"`
|
||||
UnitID *uuid.UUID `validate:"omitempty"`
|
||||
HasIngredients bool `validate:"omitempty"`
|
||||
Metadata map[string]interface{}
|
||||
@ -62,7 +60,6 @@ type CreateProductRequest struct {
|
||||
}
|
||||
|
||||
type UpdateProductRequest struct {
|
||||
OutletID uuid.UUID `validate:"omitempty"` // If set, upsert product_outlet_prices on update
|
||||
CategoryID *uuid.UUID `validate:"omitempty"`
|
||||
SKU *string `validate:"omitempty,max=100"`
|
||||
Name *string `validate:"omitempty,min=1,max=255"`
|
||||
@ -71,7 +68,6 @@ type UpdateProductRequest struct {
|
||||
Cost *float64 `validate:"omitempty,min=0"`
|
||||
ImageURL *string `validate:"omitempty,max=500"`
|
||||
PrinterType *string `validate:"omitempty,max=50"`
|
||||
PrintToChecker *bool `validate:"omitempty"`
|
||||
UnitID *uuid.UUID `validate:"omitempty"`
|
||||
HasIngredients *bool `validate:"omitempty"`
|
||||
Metadata map[string]interface{}
|
||||
@ -104,13 +100,10 @@ type ProductResponse struct {
|
||||
Name string
|
||||
Description *string
|
||||
Price float64
|
||||
OutletPrice *float64 // outlet-specific price, nil if not set
|
||||
OutletPrices []OutletPrice // all outlet prices, populated when no outletID in context
|
||||
Cost float64
|
||||
BusinessType constants.BusinessType
|
||||
ImageURL *string
|
||||
PrinterType string
|
||||
PrintToChecker bool
|
||||
UnitID *uuid.UUID
|
||||
HasIngredients bool
|
||||
Metadata map[string]interface{}
|
||||
@ -120,13 +113,6 @@ type ProductResponse struct {
|
||||
Variants []ProductVariantResponse
|
||||
}
|
||||
|
||||
type OutletPrice struct {
|
||||
OutletID uuid.UUID
|
||||
OutletName string
|
||||
Price float64
|
||||
PrintToChecker bool
|
||||
}
|
||||
|
||||
type ProductVariantResponse struct {
|
||||
ID uuid.UUID
|
||||
ProductID uuid.UUID
|
||||
|
||||
@ -7,15 +7,15 @@ import (
|
||||
)
|
||||
|
||||
type ProductIngredient struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Product *Product `json:"product,omitempty"`
|
||||
@ -37,15 +37,15 @@ type UpdateProductIngredientRequest struct {
|
||||
}
|
||||
|
||||
type ProductIngredientResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Product *Product `json:"product,omitempty"`
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ProductOutletPrice struct {
|
||||
ID uuid.UUID
|
||||
ProductID uuid.UUID
|
||||
OutletID uuid.UUID
|
||||
Price float64
|
||||
PrintToChecker bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type CreateProductOutletPriceRequest struct {
|
||||
ProductID uuid.UUID `validate:"required"`
|
||||
OutletID uuid.UUID `validate:"required"`
|
||||
Price float64 `validate:"required,min=0"`
|
||||
PrintToChecker bool
|
||||
}
|
||||
|
||||
type UpdateProductOutletPriceRequest struct {
|
||||
Price *float64 `validate:"required,min=0"`
|
||||
PrintToChecker *bool
|
||||
}
|
||||
|
||||
type ProductOutletPriceResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
OutletID uuid.UUID `json:"outlet_id"`
|
||||
Price float64 `json:"price"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@ -56,4 +56,4 @@ type ProductRecipeResponse struct {
|
||||
Product *Product `json:"product,omitempty"`
|
||||
ProductVariant *ProductVariant `json:"product_variant,omitempty"`
|
||||
Ingredient *Ingredient `json:"ingredient,omitempty"`
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PurchaseCategoryResponse struct {
|
||||
ID uuid.UUID
|
||||
OrganizationID uuid.UUID
|
||||
PresetID *uuid.UUID
|
||||
ParentID *uuid.UUID
|
||||
Code string
|
||||
Name string
|
||||
Type string
|
||||
SortOrder int
|
||||
IsSystem bool
|
||||
IsActive bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type CreatePurchaseCategoryRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
ParentID *uuid.UUID
|
||||
Code *string
|
||||
Name string
|
||||
Type string
|
||||
SortOrder int
|
||||
IsActive bool
|
||||
}
|
||||
|
||||
type UpdatePurchaseCategoryRequest struct {
|
||||
ParentID *uuid.UUID
|
||||
Code *string
|
||||
Name *string
|
||||
Type *string
|
||||
SortOrder *int
|
||||
IsActive *bool
|
||||
}
|
||||
|
||||
type ListPurchaseCategoriesRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
ParentID *uuid.UUID
|
||||
Type string
|
||||
Search string
|
||||
IsActive *bool
|
||||
Page int
|
||||
Limit int
|
||||
}
|
||||
@ -7,32 +7,30 @@ import (
|
||||
)
|
||||
|
||||
type PurchaseOrder struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_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"`
|
||||
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"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_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"`
|
||||
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 {
|
||||
@ -45,11 +43,10 @@ type PurchaseOrderAttachment struct {
|
||||
type PurchaseOrderResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
VendorID *uuid.UUID `json:"vendor_id"`
|
||||
VendorID uuid.UUID `json:"vendor_id"`
|
||||
PONumber string `json:"po_number"`
|
||||
TransactionDate time.Time `json:"transaction_date"`
|
||||
DueDate *time.Time `json:"due_date"`
|
||||
DueDate time.Time `json:"due_date"`
|
||||
Reference *string `json:"reference"`
|
||||
Status string `json:"status"`
|
||||
Message *string `json:"message"`
|
||||
@ -62,19 +59,17 @@ type PurchaseOrderResponse struct {
|
||||
}
|
||||
|
||||
type PurchaseOrderItemResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||
IngredientID *uuid.UUID `json:"ingredient_id"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_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"`
|
||||
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||
Unit *UnitResponse `json:"unit,omitempty"`
|
||||
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 {
|
||||
@ -86,11 +81,10 @@ type PurchaseOrderAttachmentResponse struct {
|
||||
}
|
||||
|
||||
type CreatePurchaseOrderRequest struct {
|
||||
VendorID *uuid.UUID `json:"vendor_id,omitempty"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
VendorID uuid.UUID `json:"vendor_id"`
|
||||
PONumber string `json:"po_number"`
|
||||
TransactionDate time.Time `json:"transaction_date"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
DueDate time.Time `json:"due_date"`
|
||||
Reference *string `json:"reference,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
Message *string `json:"message,omitempty"`
|
||||
@ -99,12 +93,11 @@ type CreatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type CreatePurchaseOrderItemRequest struct {
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Quantity *float64 `json:"quantity,omitempty"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||
Amount float64 `json:"amount"`
|
||||
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 {
|
||||
@ -120,13 +113,12 @@ type UpdatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type UpdatePurchaseOrderItemRequest struct {
|
||||
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||
PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Quantity *float64 `json:"quantity,omitempty"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||
Amount *float64 `json:"amount,omitempty"`
|
||||
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 {
|
||||
|
||||
@ -63,12 +63,10 @@ type UserResponse struct {
|
||||
|
||||
func (u *User) HasPermission(requiredRole constants.UserRole) bool {
|
||||
roleHierarchy := map[constants.UserRole]int{
|
||||
constants.RoleWaiter: 1,
|
||||
constants.RoleCashier: 2,
|
||||
constants.RolePurchasing: 3,
|
||||
constants.RoleManager: 4,
|
||||
constants.RoleAdmin: 5,
|
||||
constants.RoleOwner: 6,
|
||||
constants.RoleWaiter: 1,
|
||||
constants.RoleCashier: 2,
|
||||
constants.RoleManager: 3,
|
||||
constants.RoleAdmin: 4,
|
||||
}
|
||||
|
||||
userLevel := roleHierarchy[u.Role]
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserDevice struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceType entities.DeviceType `json:"device_type"`
|
||||
Platform entities.DevicePlatform `json:"platform"`
|
||||
FCMToken string `json:"fcm_token"`
|
||||
AppVersion string `json:"app_version"`
|
||||
OsVersion string `json:"os_version"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
LastActiveAt *time.Time `json:"last_active_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type UserDeviceResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceType entities.DeviceType `json:"device_type"`
|
||||
Platform entities.DevicePlatform `json:"platform"`
|
||||
FCMToken string `json:"fcm_token"`
|
||||
AppVersion string `json:"app_version"`
|
||||
OsVersion string `json:"os_version"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
LastActiveAt *time.Time `json:"last_active_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type RegisterUserDeviceRequest struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceType entities.DeviceType `json:"device_type"`
|
||||
Platform entities.DevicePlatform `json:"platform"`
|
||||
FCMToken string `json:"fcm_token"`
|
||||
AppVersion string `json:"app_version"`
|
||||
OsVersion string `json:"os_version"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
}
|
||||
|
||||
type UpdateUserDeviceRequest struct {
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceType entities.DeviceType `json:"device_type"`
|
||||
Platform entities.DevicePlatform `json:"platform"`
|
||||
FCMToken string `json:"fcm_token"`
|
||||
AppVersion string `json:"app_version"`
|
||||
OsVersion string `json:"os_version"`
|
||||
}
|
||||
|
||||
type ListUserDevicesRequest struct {
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
}
|
||||
|
||||
type ListUserDevicesResponse struct {
|
||||
Devices []*UserDeviceResponse `json:"devices"`
|
||||
TotalCount int `json:"total_count"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
@ -3,53 +3,31 @@ package processor
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AnalyticsProcessor interface {
|
||||
GetPaymentMethodAnalytics(ctx context.Context, req *models.PaymentMethodAnalyticsRequest) (*models.PaymentMethodAnalyticsResponse, error)
|
||||
GetSalesAnalytics(ctx context.Context, req *models.SalesAnalyticsRequest) (*models.SalesAnalyticsResponse, error)
|
||||
GetPurchasingAnalytics(ctx context.Context, req *models.PurchasingAnalyticsRequest) (*models.PurchasingAnalyticsResponse, error)
|
||||
GetProductAnalytics(ctx context.Context, req *models.ProductAnalyticsRequest) (*models.ProductAnalyticsResponse, error)
|
||||
GetProductAnalyticsPerCategory(ctx context.Context, req *models.ProductAnalyticsPerCategoryRequest) (*models.ProductAnalyticsPerCategoryResponse, error)
|
||||
GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error)
|
||||
GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error)
|
||||
GetExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error)
|
||||
GetExclusiveSummaryMonthly(ctx context.Context, req *models.ExclusiveSummaryMonthlyRequest) (*models.ExclusiveSummaryMonthlyResponse, error)
|
||||
GetExclusiveSummaryMTD(ctx context.Context, req *models.ExclusiveSummaryMTDRequest) (*models.ExclusiveSummaryPeriodResponse, error)
|
||||
}
|
||||
|
||||
type AnalyticsProcessorImpl struct {
|
||||
analyticsRepo repository.AnalyticsRepository
|
||||
expenseRepo ExpenseRepository
|
||||
}
|
||||
|
||||
func NewAnalyticsProcessorImpl(analyticsRepo repository.AnalyticsRepository, expenseRepo ExpenseRepository) *AnalyticsProcessorImpl {
|
||||
func NewAnalyticsProcessorImpl(analyticsRepo repository.AnalyticsRepository) *AnalyticsProcessorImpl {
|
||||
return &AnalyticsProcessorImpl{
|
||||
analyticsRepo: analyticsRepo,
|
||||
expenseRepo: expenseRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// resolveOutletName fetches the outlet name from the database if outletID is provided
|
||||
func (p *AnalyticsProcessorImpl) resolveOutletName(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) *string {
|
||||
if outletID == nil {
|
||||
return nil
|
||||
}
|
||||
name, err := p.analyticsRepo.GetOutletName(ctx, organizationID, *outletID)
|
||||
if err != nil || name == "" {
|
||||
return nil
|
||||
}
|
||||
return &name
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) GetPaymentMethodAnalytics(ctx context.Context, req *models.PaymentMethodAnalyticsRequest) (*models.PaymentMethodAnalyticsResponse, error) {
|
||||
if req.DateFrom.After(req.DateTo) {
|
||||
return nil, fmt.Errorf("date_from cannot be after date_to")
|
||||
@ -104,7 +82,6 @@ func (p *AnalyticsProcessorImpl) GetPaymentMethodAnalytics(ctx context.Context,
|
||||
return &models.PaymentMethodAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
GroupBy: req.GroupBy,
|
||||
@ -179,7 +156,6 @@ func (p *AnalyticsProcessorImpl) GetSalesAnalytics(ctx context.Context, req *mod
|
||||
return &models.SalesAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
GroupBy: req.GroupBy,
|
||||
@ -188,85 +164,6 @@ func (p *AnalyticsProcessorImpl) GetSalesAnalytics(ctx context.Context, req *mod
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req *models.PurchasingAnalyticsRequest) (*models.PurchasingAnalyticsResponse, error) {
|
||||
if req.DateFrom.After(req.DateTo) {
|
||||
return nil, fmt.Errorf("date_from cannot be after date_to")
|
||||
}
|
||||
|
||||
if req.GroupBy == "" {
|
||||
req.GroupBy = "day"
|
||||
}
|
||||
|
||||
result, err := p.analyticsRepo.GetPurchasingAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.GroupBy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get purchasing analytics: %w", err)
|
||||
}
|
||||
|
||||
data := make([]models.PurchasingAnalyticsData, len(result.Data))
|
||||
for i, item := range result.Data {
|
||||
data[i] = models.PurchasingAnalyticsData{
|
||||
Date: item.Date,
|
||||
Purchases: item.Purchases,
|
||||
RawMaterialPurchases: item.RawMaterialPurchases,
|
||||
ExpensePurchases: item.ExpensePurchases,
|
||||
PurchaseOrders: item.PurchaseOrders,
|
||||
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
||||
ExpenseCount: item.ExpenseCount,
|
||||
Quantity: item.Quantity,
|
||||
Ingredients: item.Ingredients,
|
||||
Vendors: item.Vendors,
|
||||
}
|
||||
}
|
||||
|
||||
ingredientData := make([]models.PurchasingIngredientData, len(result.IngredientData))
|
||||
for i, item := range result.IngredientData {
|
||||
ingredientData[i] = models.PurchasingIngredientData{
|
||||
IngredientID: item.IngredientID,
|
||||
IngredientName: item.IngredientName,
|
||||
Quantity: item.Quantity,
|
||||
TotalCost: item.TotalCost,
|
||||
AverageUnitCost: item.AverageUnitCost,
|
||||
PurchaseOrderCount: item.PurchaseOrderCount,
|
||||
}
|
||||
}
|
||||
|
||||
vendorData := make([]models.PurchasingVendorData, len(result.VendorData))
|
||||
for i, item := range result.VendorData {
|
||||
vendorData[i] = models.PurchasingVendorData{
|
||||
VendorID: item.VendorID,
|
||||
VendorName: item.VendorName,
|
||||
TotalCost: item.TotalCost,
|
||||
PurchaseOrderCount: item.PurchaseOrderCount,
|
||||
IngredientCount: item.IngredientCount,
|
||||
Quantity: item.Quantity,
|
||||
}
|
||||
}
|
||||
|
||||
return &models.PurchasingAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: result.OutletName,
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
GroupBy: req.GroupBy,
|
||||
Summary: models.PurchasingSummary{
|
||||
TotalPurchases: result.Summary.TotalPurchases,
|
||||
RawMaterialPurchases: result.Summary.RawMaterialPurchases,
|
||||
ExpensePurchases: result.Summary.ExpensePurchases,
|
||||
TotalPurchaseOrders: result.Summary.TotalPurchaseOrders,
|
||||
RawMaterialPurchaseOrders: result.Summary.RawMaterialPurchaseOrders,
|
||||
ExpenseCount: result.Summary.ExpenseCount,
|
||||
TotalQuantity: result.Summary.TotalQuantity,
|
||||
AveragePurchaseOrderValue: result.Summary.AveragePurchaseOrderValue,
|
||||
TotalIngredients: result.Summary.TotalIngredients,
|
||||
TotalVendors: result.Summary.TotalVendors,
|
||||
},
|
||||
Data: data,
|
||||
IngredientData: ingredientData,
|
||||
VendorData: vendorData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *models.ProductAnalyticsRequest) (*models.ProductAnalyticsResponse, error) {
|
||||
// Validate date range
|
||||
if req.DateFrom.After(req.DateTo) {
|
||||
@ -291,7 +188,6 @@ func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *m
|
||||
ProductID: data.ProductID,
|
||||
ProductName: data.ProductName,
|
||||
ProductSku: data.ProductSku,
|
||||
ProductPrice: data.ProductPrice,
|
||||
CategoryID: data.CategoryID,
|
||||
CategoryName: data.CategoryName,
|
||||
CategoryOrder: data.CategoryOrder,
|
||||
@ -311,7 +207,6 @@ func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *m
|
||||
return &models.ProductAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
Data: resultData,
|
||||
@ -349,7 +244,6 @@ func (p *AnalyticsProcessorImpl) GetProductAnalyticsPerCategory(ctx context.Cont
|
||||
return &models.ProductAnalyticsPerCategoryResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
Data: resultData,
|
||||
@ -411,19 +305,15 @@ func (p *AnalyticsProcessorImpl) GetDashboardAnalytics(ctx context.Context, req
|
||||
return &models.DashboardAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
Overview: models.DashboardOverview{
|
||||
TotalSales: overview.TotalSales,
|
||||
TotalOrders: overview.TotalOrders,
|
||||
AverageOrderValue: overview.AverageOrderValue,
|
||||
TotalCustomers: overview.TotalCustomers,
|
||||
VoidedOrders: overview.VoidedOrders,
|
||||
RefundedOrders: overview.RefundedOrders,
|
||||
TotalItemSold: overview.TotalItemSold,
|
||||
TotalLowStock: overview.TotalLowStock,
|
||||
TotalProductActive: overview.TotalProductActive,
|
||||
TotalSales: overview.TotalSales,
|
||||
TotalOrders: overview.TotalOrders,
|
||||
AverageOrderValue: overview.AverageOrderValue,
|
||||
TotalCustomers: overview.TotalCustomers,
|
||||
VoidedOrders: overview.VoidedOrders,
|
||||
RefundedOrders: overview.RefundedOrders,
|
||||
},
|
||||
TopProducts: topProducts.Data,
|
||||
PaymentMethods: paymentMethods.Data,
|
||||
@ -432,27 +322,17 @@ func (p *AnalyticsProcessorImpl) GetDashboardAnalytics(ctx context.Context, req
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error) {
|
||||
if req.DateFrom.IsZero() {
|
||||
return nil, fmt.Errorf("date_from is required")
|
||||
}
|
||||
|
||||
if req.DateTo.IsZero() {
|
||||
return nil, fmt.Errorf("date_to is required")
|
||||
}
|
||||
|
||||
if req.DateFrom.After(req.DateTo) {
|
||||
return nil, fmt.Errorf("date_from cannot be after date_to")
|
||||
}
|
||||
|
||||
if req.GroupBy == "" {
|
||||
req.GroupBy = "day"
|
||||
}
|
||||
|
||||
// Get analytics data from repository
|
||||
result, err := p.analyticsRepo.GetProfitLossAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.GroupBy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get profit/loss analytics: %w", err)
|
||||
}
|
||||
|
||||
// Transform entities to models
|
||||
data := make([]models.ProfitLossData, len(result.Data))
|
||||
for i, item := range result.Data {
|
||||
data[i] = models.ProfitLossData{
|
||||
@ -487,159 +367,9 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
||||
}
|
||||
}
|
||||
|
||||
type categoryAmount struct {
|
||||
Name string
|
||||
TodayAmt float64
|
||||
MtdAmt float64
|
||||
}
|
||||
|
||||
categoryMap := make(map[string]*categoryAmount)
|
||||
var categoryOrder []string
|
||||
|
||||
for _, cat := range result.TodayExpenseByCategory {
|
||||
name := cat.CategoryName
|
||||
if _, exists := categoryMap[name]; !exists {
|
||||
categoryMap[name] = &categoryAmount{Name: name}
|
||||
categoryOrder = append(categoryOrder, name)
|
||||
}
|
||||
categoryMap[name].TodayAmt = cat.Amount
|
||||
}
|
||||
|
||||
for _, cat := range result.MtdExpenseByCategory {
|
||||
name := cat.CategoryName
|
||||
if _, exists := categoryMap[name]; !exists {
|
||||
categoryMap[name] = &categoryAmount{Name: name}
|
||||
categoryOrder = append(categoryOrder, name)
|
||||
}
|
||||
categoryMap[name].MtdAmt = cat.Amount
|
||||
}
|
||||
|
||||
var todayTotalOps float64
|
||||
var mtdTotalOps float64
|
||||
var todayGaji float64
|
||||
var mtdGaji float64
|
||||
for _, cat := range categoryMap {
|
||||
if isSalaryExpenseCategory(cat.Name) {
|
||||
todayGaji += cat.TodayAmt
|
||||
mtdGaji += cat.MtdAmt
|
||||
continue
|
||||
}
|
||||
todayTotalOps += cat.TodayAmt
|
||||
mtdTotalOps += cat.MtdAmt
|
||||
}
|
||||
|
||||
todayGrossProfit := result.TodayRevenue - result.TodayCost
|
||||
mtdGrossProfit := result.MtdRevenue - result.MtdCost
|
||||
|
||||
todayProfitBeforeGaji := todayGrossProfit - todayTotalOps
|
||||
mtdProfitBeforeGaji := mtdGrossProfit - mtdTotalOps
|
||||
|
||||
todayNetProfit := todayProfitBeforeGaji - todayGaji
|
||||
mtdNetProfit := mtdProfitBeforeGaji - mtdGaji
|
||||
|
||||
todayPct := func(nominal float64) float64 {
|
||||
if result.TodayRevenue == 0 {
|
||||
return 0
|
||||
}
|
||||
return (nominal / result.TodayRevenue) * 100
|
||||
}
|
||||
mtdPct := func(nominal float64) float64 {
|
||||
if result.MtdRevenue == 0 {
|
||||
return 0
|
||||
}
|
||||
return (nominal / result.MtdRevenue) * 100
|
||||
}
|
||||
|
||||
opsSubItems := make([]models.ProfitLossSummaryRow, 0, len(categoryOrder)+1)
|
||||
opsCategoryCount := 0
|
||||
for _, name := range categoryOrder {
|
||||
cat := categoryMap[name]
|
||||
if isSalaryExpenseCategory(cat.Name) {
|
||||
continue
|
||||
}
|
||||
opsCategoryCount++
|
||||
opsSubItems = append(opsSubItems, models.ProfitLossSummaryRow{
|
||||
ID: fmt.Sprintf("by_%s", slugify(name)),
|
||||
Label: fmt.Sprintf("%d. %s", opsCategoryCount, cat.Name),
|
||||
TodayNominal: cat.TodayAmt,
|
||||
TodayPct: todayPct(cat.TodayAmt),
|
||||
MtdNominal: cat.MtdAmt,
|
||||
MtdPct: mtdPct(cat.MtdAmt),
|
||||
})
|
||||
}
|
||||
opsSubItems = append(opsSubItems, models.ProfitLossSummaryRow{
|
||||
ID: "total_biaya_ops",
|
||||
Label: fmt.Sprintf("Total Biaya OPS (%d kategori)", opsCategoryCount),
|
||||
IsBold: true,
|
||||
TodayNominal: todayTotalOps,
|
||||
TodayPct: todayPct(todayTotalOps),
|
||||
MtdNominal: mtdTotalOps,
|
||||
MtdPct: mtdPct(mtdTotalOps),
|
||||
})
|
||||
|
||||
mainSummary := []models.ProfitLossSummaryRow{
|
||||
{
|
||||
ID: "total_omset", Label: "TOTAL OMSET",
|
||||
TodayNominal: result.TodayRevenue, TodayPct: todayPct(result.TodayRevenue),
|
||||
MtdNominal: result.MtdRevenue, MtdPct: mtdPct(result.MtdRevenue),
|
||||
},
|
||||
{
|
||||
ID: "hpp", Label: "HPP",
|
||||
TodayNominal: result.TodayCost, TodayPct: todayPct(result.TodayCost),
|
||||
MtdNominal: result.MtdCost, MtdPct: mtdPct(result.MtdCost),
|
||||
},
|
||||
{
|
||||
ID: "laba_kotor", Label: "Laba Kotor (1-2)",
|
||||
TodayNominal: todayGrossProfit, TodayPct: todayPct(todayGrossProfit),
|
||||
MtdNominal: mtdGrossProfit, MtdPct: mtdPct(mtdGrossProfit),
|
||||
},
|
||||
{
|
||||
ID: "biaya_ops", Label: "BIAYA OPS",
|
||||
TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps),
|
||||
MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps),
|
||||
SubItems: opsSubItems,
|
||||
},
|
||||
{
|
||||
ID: "laba_rugi_sblm_gaji", Label: "Laba/Rugi sblm Gaji (3-4)",
|
||||
TodayNominal: todayProfitBeforeGaji, TodayPct: todayPct(todayProfitBeforeGaji),
|
||||
MtdNominal: mtdProfitBeforeGaji, MtdPct: mtdPct(mtdProfitBeforeGaji),
|
||||
},
|
||||
{
|
||||
ID: "biaya_gaji", Label: "BIAYA GAJI",
|
||||
TodayNominal: todayGaji, TodayPct: todayPct(todayGaji),
|
||||
MtdNominal: mtdGaji, MtdPct: mtdPct(mtdGaji),
|
||||
},
|
||||
{
|
||||
ID: "laba_rugi", Label: "Laba/Rugi (5-6)", IsBold: true,
|
||||
TodayNominal: todayNetProfit, TodayPct: todayPct(todayNetProfit),
|
||||
MtdNominal: mtdNetProfit, MtdPct: mtdPct(mtdNetProfit),
|
||||
},
|
||||
}
|
||||
|
||||
opsItems := make([]models.OperationalExpenseItem, len(result.OperationalExpenseItems))
|
||||
var opsTotal float64
|
||||
for i, item := range result.OperationalExpenseItems {
|
||||
opsItems[i] = models.OperationalExpenseItem{
|
||||
Item: item.Item,
|
||||
Nominal: item.Amount,
|
||||
}
|
||||
opsTotal += item.Amount
|
||||
}
|
||||
|
||||
purchasingItems := make([]models.ProfitLossPurchasingItem, len(result.PurchasingItems))
|
||||
for i, item := range result.PurchasingItems {
|
||||
purchasingItems[i] = models.ProfitLossPurchasingItem{
|
||||
Date: item.Date,
|
||||
Item: item.Item,
|
||||
Quantity: item.Quantity,
|
||||
Nominal: item.Amount,
|
||||
}
|
||||
}
|
||||
|
||||
return &models.ProfitLossAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
GroupBy: req.GroupBy,
|
||||
@ -658,319 +388,5 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
||||
},
|
||||
Data: data,
|
||||
ProductData: productData,
|
||||
MainSummary: mainSummary,
|
||||
Purchasing: models.ProfitLossPurchasing{
|
||||
TodayTotal: result.TodayPurchasing,
|
||||
MtdTotal: result.MtdPurchasing,
|
||||
TodayRawMaterial: result.TodayPurchasingRawMaterial,
|
||||
MtdRawMaterial: result.MtdPurchasingRawMaterial,
|
||||
TodayExpense: result.TodayPurchasingExpense,
|
||||
MtdExpense: result.MtdPurchasingExpense,
|
||||
Items: purchasingItems,
|
||||
},
|
||||
OperationalExpenses: opsItems,
|
||||
OperationalExpensesTotal: opsTotal,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isSalaryExpenseCategory(name string) bool {
|
||||
name = strings.ToLower(name)
|
||||
return strings.Contains(name, "gaji") || strings.Contains(name, "salary")
|
||||
}
|
||||
|
||||
func slugify(s string) string {
|
||||
result := make([]byte, 0, len(s))
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
switch {
|
||||
case c >= 'a' && c <= 'z':
|
||||
result = append(result, c)
|
||||
case c >= 'A' && c <= 'Z':
|
||||
result = append(result, c+32)
|
||||
case c >= '0' && c <= '9':
|
||||
result = append(result, c)
|
||||
default:
|
||||
if len(result) == 0 || result[len(result)-1] != '_' {
|
||||
result = append(result, '_')
|
||||
}
|
||||
}
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) GetExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error) {
|
||||
if req.DateFrom.After(req.DateTo) {
|
||||
return nil, fmt.Errorf("date_from cannot be after date_to")
|
||||
}
|
||||
|
||||
return p.buildExclusiveSummaryPeriod(ctx, req)
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMonthly(ctx context.Context, req *models.ExclusiveSummaryMonthlyRequest) (*models.ExclusiveSummaryMonthlyResponse, error) {
|
||||
monthStart := time.Date(req.Month.Year(), req.Month.Month(), 1, 0, 0, 0, 0, req.Month.Location())
|
||||
monthEnd := monthStart.AddDate(0, 1, 0).Add(-time.Nanosecond)
|
||||
|
||||
fullPeriod, err := p.buildExclusiveSummaryPeriod(ctx, &models.ExclusiveSummaryPeriodRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
DateFrom: monthStart,
|
||||
DateTo: monthEnd,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
periods := make([]models.ExclusiveSummaryMonthlyPeriod, 0)
|
||||
for _, bucket := range buildExclusiveSummaryMonthlyBuckets(monthStart) {
|
||||
period, err := p.buildExclusiveSummaryPeriod(ctx, &models.ExclusiveSummaryPeriodRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
DateFrom: bucket.DateFrom,
|
||||
DateTo: bucket.DateTo,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
periods = append(periods, models.ExclusiveSummaryMonthlyPeriod{
|
||||
Label: bucket.Label,
|
||||
DateFrom: bucket.DateFrom,
|
||||
DateTo: bucket.DateTo,
|
||||
Sales: period.Summary.Sales,
|
||||
HPP: period.Summary.HPP,
|
||||
GrossProfit: period.Summary.GrossProfit,
|
||||
GrossMargin: percentage(period.Summary.GrossProfit, period.Summary.Sales),
|
||||
})
|
||||
}
|
||||
|
||||
bankBalances, err := p.analyticsRepo.GetExclusiveSummaryBankBalances(ctx, req.OrganizationID, req.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get exclusive summary bank balances: %w", err)
|
||||
}
|
||||
|
||||
bankBalance := make([]models.ExclusiveSummaryBankBalance, len(bankBalances))
|
||||
for i, item := range bankBalances {
|
||||
bankBalance[i] = models.ExclusiveSummaryBankBalance{
|
||||
Bank: item.Bank,
|
||||
OpeningBalance: item.OpeningBalance,
|
||||
IncomingMutation: item.IncomingMutation,
|
||||
OutgoingMutation: item.OutgoingMutation,
|
||||
ClosingBalance: item.ClosingBalance,
|
||||
Notes: item.Notes,
|
||||
}
|
||||
}
|
||||
|
||||
return &models.ExclusiveSummaryMonthlyResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
Month: monthStart.Format("2006-01"),
|
||||
Summary: models.ExclusiveSummaryMonthlySummary{
|
||||
TotalSales: fullPeriod.Summary.Sales,
|
||||
HPP: fullPeriod.Summary.HPP,
|
||||
GrossProfit: fullPeriod.Summary.GrossProfit,
|
||||
OperationalExpensesTotal: fullPeriod.Summary.OperationalExpensesTotal,
|
||||
TotalCost: fullPeriod.Summary.TotalCost,
|
||||
NetProfit: fullPeriod.Summary.NetProfit,
|
||||
NetProfitMargin: percentage(fullPeriod.Summary.NetProfit, fullPeriod.Summary.Sales),
|
||||
},
|
||||
Periods: periods,
|
||||
BankBalance: bankBalance,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMTD(ctx context.Context, req *models.ExclusiveSummaryMTDRequest) (*models.ExclusiveSummaryPeriodResponse, error) {
|
||||
mtdStart := time.Date(req.DateTo.Year(), req.DateTo.Month(), 1, 0, 0, 0, 0, req.DateTo.Location())
|
||||
|
||||
return p.buildExclusiveSummaryPeriod(ctx, &models.ExclusiveSummaryPeriodRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
DateFrom: mtdStart,
|
||||
DateTo: req.DateTo,
|
||||
ExcludeGajiStaffFromReimburse: req.ExcludeGajiStaffFromReimburse,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) buildExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error) {
|
||||
result, err := p.analyticsRepo.GetExclusiveSummaryAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get exclusive summary analytics: %w", err)
|
||||
}
|
||||
|
||||
hppBreakdown, hppTotal := exclusiveSummaryCategoryBreakdown(result.HPPBreakdown)
|
||||
operationalBreakdown, operationalTotal := exclusiveSummaryCategoryBreakdown(result.OperationalExpenseBreakdown)
|
||||
salaryDW, salaryStaff, salaryOther := exclusiveSummarySalaryBreakdown(result.DailyTransactions)
|
||||
salaryTotal := salaryDW + salaryStaff + salaryOther
|
||||
otherOperationalExpenses := operationalTotal - salaryTotal
|
||||
if otherOperationalExpenses < 0 {
|
||||
otherOperationalExpenses = 0
|
||||
}
|
||||
|
||||
grossProfit := result.SalesTotal - hppTotal
|
||||
totalCost := hppTotal + operationalTotal
|
||||
netProfit := result.SalesTotal - totalCost
|
||||
excludedSalaryStaff := 0.0
|
||||
if req.ExcludeGajiStaffFromReimburse {
|
||||
excludedSalaryStaff = salaryStaff
|
||||
}
|
||||
|
||||
dailySummary := make([]models.ExclusiveSummaryDailySummary, len(result.DailySummary))
|
||||
for i, item := range result.DailySummary {
|
||||
dailySummary[i] = models.ExclusiveSummaryDailySummary{
|
||||
Date: item.Date,
|
||||
TransactionCount: item.TransactionCount,
|
||||
TotalCost: item.TotalCost,
|
||||
}
|
||||
}
|
||||
|
||||
dailyTransactions := make([]models.ExclusiveSummaryDailyTransaction, len(result.DailyTransactions))
|
||||
for i, item := range result.DailyTransactions {
|
||||
dailyTransactions[i] = models.ExclusiveSummaryDailyTransaction{
|
||||
Date: item.Date,
|
||||
CategoryCode: item.CategoryCode,
|
||||
CategoryName: item.CategoryName,
|
||||
Description: item.Description,
|
||||
Amount: item.Amount,
|
||||
Source: item.Source,
|
||||
}
|
||||
}
|
||||
|
||||
return &models.ExclusiveSummaryPeriodResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
Period: models.ExclusiveSummaryPeriodRange{
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
},
|
||||
Summary: models.ExclusiveSummaryPeriodSummary{
|
||||
Sales: result.SalesTotal,
|
||||
HPP: hppTotal,
|
||||
GrossProfit: grossProfit,
|
||||
SalaryTotal: salaryTotal,
|
||||
SalaryDW: salaryDW,
|
||||
SalaryStaff: salaryStaff,
|
||||
SalaryOther: salaryOther,
|
||||
OtherOperationalExpenses: otherOperationalExpenses,
|
||||
OperationalExpensesTotal: operationalTotal,
|
||||
TotalCost: totalCost,
|
||||
NetProfit: netProfit,
|
||||
},
|
||||
Reimburse: models.ExclusiveSummaryReimburse{
|
||||
TotalCost: totalCost,
|
||||
ExcludedSalaryStaff: excludedSalaryStaff,
|
||||
TotalReimburse: totalCost - excludedSalaryStaff,
|
||||
},
|
||||
HPPBreakdown: hppBreakdown,
|
||||
OperationalExpenseBreakdown: operationalBreakdown,
|
||||
DailySummary: dailySummary,
|
||||
DailyTransactions: dailyTransactions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func exclusiveSummaryCategoryBreakdown(items []entities.ExclusiveSummaryCategoryTotal) ([]models.ExclusiveSummaryCategoryBreakdown, float64) {
|
||||
var total float64
|
||||
for _, item := range items {
|
||||
total += item.Amount
|
||||
}
|
||||
|
||||
breakdown := make([]models.ExclusiveSummaryCategoryBreakdown, len(items))
|
||||
for i, item := range items {
|
||||
breakdown[i] = models.ExclusiveSummaryCategoryBreakdown{
|
||||
CategoryCode: item.CategoryCode,
|
||||
CategoryName: item.CategoryName,
|
||||
Amount: item.Amount,
|
||||
Percentage: percentage(item.Amount, total),
|
||||
}
|
||||
}
|
||||
|
||||
return breakdown, total
|
||||
}
|
||||
|
||||
func exclusiveSummarySalaryBreakdown(transactions []entities.ExclusiveSummaryDailyTransaction) (float64, float64, float64) {
|
||||
var salaryDW float64
|
||||
var salaryStaff float64
|
||||
var salaryOther float64
|
||||
|
||||
for _, transaction := range transactions {
|
||||
if !isExclusiveSummarySalary(transaction.CategoryCode, transaction.CategoryName, transaction.Description) {
|
||||
continue
|
||||
}
|
||||
|
||||
classification := strings.ToLower(transaction.CategoryCode + " " + transaction.CategoryName + " " + transaction.Description)
|
||||
switch {
|
||||
case strings.Contains(classification, "staff") || strings.Contains(classification, "kary") || strings.Contains(classification, "karyawan"):
|
||||
salaryStaff += transaction.Amount
|
||||
case strings.Contains(classification, "dw"):
|
||||
salaryDW += transaction.Amount
|
||||
default:
|
||||
salaryOther += transaction.Amount
|
||||
}
|
||||
}
|
||||
|
||||
return salaryDW, salaryStaff, salaryOther
|
||||
}
|
||||
|
||||
func isExclusiveSummarySalary(parts ...string) bool {
|
||||
text := strings.ToLower(strings.Join(parts, " "))
|
||||
return strings.Contains(text, "gaji") || strings.Contains(text, "salary")
|
||||
}
|
||||
|
||||
func percentage(numerator, denominator float64) float64 {
|
||||
if denominator == 0 {
|
||||
return 0
|
||||
}
|
||||
return (numerator / denominator) * 100
|
||||
}
|
||||
|
||||
type exclusiveSummaryMonthlyBucket struct {
|
||||
Label string
|
||||
DateFrom time.Time
|
||||
DateTo time.Time
|
||||
}
|
||||
|
||||
func buildExclusiveSummaryMonthlyBuckets(monthStart time.Time) []exclusiveSummaryMonthlyBucket {
|
||||
monthEnd := monthStart.AddDate(0, 1, 0).Add(-time.Nanosecond)
|
||||
buckets := make([]exclusiveSummaryMonthlyBucket, 0, 6)
|
||||
currentStart := monthStart
|
||||
|
||||
for !currentStart.After(monthEnd) {
|
||||
currentEnd := currentStart
|
||||
for currentEnd.Weekday() != time.Sunday && currentEnd.Day() < monthEnd.Day() {
|
||||
currentEnd = currentEnd.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
bucketEnd := time.Date(currentEnd.Year(), currentEnd.Month(), currentEnd.Day(), 23, 59, 59, int(time.Second-time.Nanosecond), currentEnd.Location())
|
||||
if bucketEnd.After(monthEnd) {
|
||||
bucketEnd = monthEnd
|
||||
}
|
||||
|
||||
buckets = append(buckets, exclusiveSummaryMonthlyBucket{
|
||||
Label: fmt.Sprintf("%d - %d %s", currentStart.Day(), bucketEnd.Day(), indonesianMonthName(currentStart.Month())),
|
||||
DateFrom: currentStart,
|
||||
DateTo: bucketEnd,
|
||||
})
|
||||
|
||||
currentStart = time.Date(bucketEnd.Year(), bucketEnd.Month(), bucketEnd.Day(), 0, 0, 0, 0, bucketEnd.Location()).AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
return buckets
|
||||
}
|
||||
|
||||
func indonesianMonthName(month time.Month) string {
|
||||
names := map[time.Month]string{
|
||||
time.January: "Januari",
|
||||
time.February: "Februari",
|
||||
time.March: "Maret",
|
||||
time.April: "April",
|
||||
time.May: "Mei",
|
||||
time.June: "Juni",
|
||||
time.July: "Juli",
|
||||
time.August: "Agustus",
|
||||
time.September: "September",
|
||||
time.October: "Oktober",
|
||||
time.November: "November",
|
||||
time.December: "Desember",
|
||||
}
|
||||
return names[month]
|
||||
}
|
||||
|
||||
@ -1,452 +0,0 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type analyticsRepositoryStub struct {
|
||||
purchasingResult *entities.PurchasingAnalytics
|
||||
profitLossResult *entities.ProfitLossAnalytics
|
||||
exclusiveSummaryResults []*entities.ExclusiveSummaryAnalytics
|
||||
bankBalances []entities.ExclusiveSummaryBankBalance
|
||||
profitLossGroup string
|
||||
exclusiveSummaryCalls int
|
||||
exclusiveSummaryFrom []time.Time
|
||||
exclusiveSummaryTo []time.Time
|
||||
}
|
||||
|
||||
func (analyticsRepositoryStub) GetPaymentMethodAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) ([]*entities.PaymentMethodAnalytics, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (analyticsRepositoryStub) GetSalesAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time, string) ([]*entities.SalesAnalytics, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s analyticsRepositoryStub) GetPurchasingAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time, string) (*entities.PurchasingAnalytics, error) {
|
||||
return s.purchasingResult, nil
|
||||
}
|
||||
|
||||
func (analyticsRepositoryStub) GetProductAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time, int) ([]*entities.ProductAnalytics, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (analyticsRepositoryStub) GetProductAnalyticsPerCategory(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) ([]*entities.ProductAnalyticsPerCategory, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (analyticsRepositoryStub) GetDashboardOverview(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) (*entities.DashboardOverview, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s analyticsRepositoryStub) GetProfitLossAnalytics(_ context.Context, _ uuid.UUID, _ *uuid.UUID, _, _ time.Time, groupBy string) (*entities.ProfitLossAnalytics, error) {
|
||||
s.profitLossGroup = groupBy
|
||||
return s.profitLossResult, nil
|
||||
}
|
||||
|
||||
func (s *analyticsRepositoryStub) GetExclusiveSummaryAnalytics(_ context.Context, _ uuid.UUID, _ *uuid.UUID, dateFrom, dateTo time.Time) (*entities.ExclusiveSummaryAnalytics, error) {
|
||||
s.exclusiveSummaryFrom = append(s.exclusiveSummaryFrom, dateFrom)
|
||||
s.exclusiveSummaryTo = append(s.exclusiveSummaryTo, dateTo)
|
||||
if s.exclusiveSummaryCalls < len(s.exclusiveSummaryResults) {
|
||||
result := s.exclusiveSummaryResults[s.exclusiveSummaryCalls]
|
||||
s.exclusiveSummaryCalls++
|
||||
return result, nil
|
||||
}
|
||||
s.exclusiveSummaryCalls++
|
||||
return &entities.ExclusiveSummaryAnalytics{}, nil
|
||||
}
|
||||
|
||||
func (s *analyticsRepositoryStub) GetExclusiveSummaryBankBalances(context.Context, uuid.UUID, *uuid.UUID) ([]entities.ExclusiveSummaryBankBalance, error) {
|
||||
return s.bankBalances, nil
|
||||
}
|
||||
|
||||
func (analyticsRepositoryStub) GetOutletName(context.Context, uuid.UUID, uuid.UUID) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
type expenseRepositoryStub struct{}
|
||||
|
||||
func (expenseRepositoryStub) Create(context.Context, *entities.Expense) error { return nil }
|
||||
func (expenseRepositoryStub) GetByID(context.Context, uuid.UUID) (*entities.Expense, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (expenseRepositoryStub) GetByIDAndOrganizationID(context.Context, uuid.UUID, uuid.UUID) (*entities.Expense, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (expenseRepositoryStub) Update(context.Context, *entities.Expense) error { return nil }
|
||||
func (expenseRepositoryStub) Delete(context.Context, uuid.UUID) error { return nil }
|
||||
func (expenseRepositoryStub) List(context.Context, uuid.UUID, map[string]interface{}, int, int) ([]*entities.Expense, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
func (expenseRepositoryStub) GetAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time, string) (*entities.ExpenseAnalytics, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (expenseRepositoryStub) CreateItem(context.Context, *entities.ExpenseItem) error { return nil }
|
||||
func (expenseRepositoryStub) DeleteItemsByExpenseID(context.Context, uuid.UUID) error { return nil }
|
||||
|
||||
func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T) {
|
||||
outletID := uuid.New()
|
||||
outletName := "Main Outlet"
|
||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{
|
||||
purchasingResult: &entities.PurchasingAnalytics{
|
||||
OutletName: &outletName,
|
||||
Summary: entities.PurchasingSummary{
|
||||
TotalPurchases: 300,
|
||||
RawMaterialPurchases: 125,
|
||||
ExpensePurchases: 175,
|
||||
TotalPurchaseOrders: 3,
|
||||
RawMaterialPurchaseOrders: 1,
|
||||
ExpenseCount: 2,
|
||||
},
|
||||
Data: []entities.PurchasingAnalyticsData{
|
||||
{
|
||||
Date: now,
|
||||
Purchases: 300,
|
||||
RawMaterialPurchases: 125,
|
||||
ExpensePurchases: 175,
|
||||
PurchaseOrders: 3,
|
||||
RawMaterialPurchaseOrders: 1,
|
||||
ExpenseCount: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, expenseRepositoryStub{})
|
||||
|
||||
result, err := processor.GetPurchasingAnalytics(context.Background(), &models.PurchasingAnalyticsRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
OutletID: &outletID,
|
||||
DateFrom: now,
|
||||
DateTo: now,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, &outletID, result.OutletID)
|
||||
require.NotNil(t, result.OutletName)
|
||||
require.Equal(t, outletName, *result.OutletName)
|
||||
require.Equal(t, float64(300), result.Summary.TotalPurchases)
|
||||
require.Equal(t, float64(125), result.Summary.RawMaterialPurchases)
|
||||
require.Equal(t, float64(175), result.Summary.ExpensePurchases)
|
||||
require.Equal(t, int64(3), result.Summary.TotalPurchaseOrders)
|
||||
require.Equal(t, int64(1), result.Summary.RawMaterialPurchaseOrders)
|
||||
require.Equal(t, int64(2), result.Summary.ExpenseCount)
|
||||
require.Len(t, result.Data, 1)
|
||||
require.Equal(t, float64(300), result.Data[0].Purchases)
|
||||
require.Equal(t, float64(125), result.Data[0].RawMaterialPurchases)
|
||||
require.Equal(t, float64(175), result.Data[0].ExpensePurchases)
|
||||
}
|
||||
|
||||
func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) {
|
||||
productID := uuid.New()
|
||||
categoryID := uuid.New()
|
||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{
|
||||
profitLossResult: &entities.ProfitLossAnalytics{
|
||||
Summary: entities.ProfitLossSummary{
|
||||
TotalRevenue: 1000,
|
||||
TotalCost: 400,
|
||||
GrossProfit: 600,
|
||||
GrossProfitMargin: 60,
|
||||
TotalTax: 50,
|
||||
TotalDiscount: 25,
|
||||
NetProfit: 575,
|
||||
NetProfitMargin: 57.5,
|
||||
TotalOrders: 10,
|
||||
AverageProfit: 57.5,
|
||||
ProfitabilityRatio: 150,
|
||||
},
|
||||
Data: []entities.ProfitLossData{
|
||||
{
|
||||
Date: now,
|
||||
Revenue: 1000,
|
||||
Cost: 400,
|
||||
GrossProfit: 600,
|
||||
GrossProfitMargin: 60,
|
||||
Tax: 50,
|
||||
Discount: 25,
|
||||
NetProfit: 575,
|
||||
NetProfitMargin: 57.5,
|
||||
Orders: 10,
|
||||
},
|
||||
},
|
||||
ProductData: []entities.ProductProfitData{
|
||||
{
|
||||
ProductID: productID,
|
||||
ProductName: "Nasi",
|
||||
CategoryID: categoryID,
|
||||
CategoryName: "Food",
|
||||
QuantitySold: 5,
|
||||
Revenue: 500,
|
||||
Cost: 200,
|
||||
GrossProfit: 300,
|
||||
GrossProfitMargin: 60,
|
||||
AveragePrice: 100,
|
||||
AverageCost: 40,
|
||||
ProfitPerUnit: 60,
|
||||
},
|
||||
},
|
||||
TodayRevenue: 1000,
|
||||
TodayCost: 400,
|
||||
MtdRevenue: 2000,
|
||||
MtdCost: 800,
|
||||
},
|
||||
}, expenseRepositoryStub{})
|
||||
|
||||
result, err := processor.GetProfitLossAnalytics(context.Background(), &models.ProfitLossAnalyticsRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
DateFrom: now,
|
||||
DateTo: now,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, "day", result.GroupBy)
|
||||
require.Equal(t, float64(1000), result.Summary.TotalRevenue)
|
||||
require.Len(t, result.Data, 1)
|
||||
require.Equal(t, float64(575), result.Data[0].NetProfit)
|
||||
require.Len(t, result.ProductData, 1)
|
||||
require.Equal(t, productID, result.ProductData[0].ProductID)
|
||||
require.NotEmpty(t, result.MainSummary)
|
||||
require.Equal(t, "total_omset", result.MainSummary[0].ID)
|
||||
}
|
||||
|
||||
func TestAnalyticsProcessorGetProfitLossAnalyticsDynamicExpenseCategories(t *testing.T) {
|
||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{
|
||||
profitLossResult: &entities.ProfitLossAnalytics{
|
||||
Summary: entities.ProfitLossSummary{
|
||||
TotalRevenue: 10000,
|
||||
TotalCost: 4000,
|
||||
},
|
||||
TodayRevenue: 10000,
|
||||
TodayCost: 4000,
|
||||
MtdRevenue: 20000,
|
||||
MtdCost: 8000,
|
||||
TodayExpenseByCategory: []entities.ExpenseCategoryTotal{
|
||||
{CategoryName: "Gaji", Amount: 1500},
|
||||
{CategoryName: "Promosi", Amount: 300},
|
||||
{CategoryName: "Sewa", Amount: 500},
|
||||
},
|
||||
MtdExpenseByCategory: []entities.ExpenseCategoryTotal{
|
||||
{CategoryName: "Gaji", Amount: 3000},
|
||||
{CategoryName: "Promosi", Amount: 600},
|
||||
{CategoryName: "Sewa", Amount: 1000},
|
||||
},
|
||||
},
|
||||
}, expenseRepositoryStub{})
|
||||
|
||||
result, err := processor.GetProfitLossAnalytics(context.Background(), &models.ProfitLossAnalyticsRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
DateFrom: now,
|
||||
DateTo: now,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
|
||||
require.Len(t, result.MainSummary, 7)
|
||||
|
||||
require.Equal(t, "total_omset", result.MainSummary[0].ID)
|
||||
require.Equal(t, float64(10000), result.MainSummary[0].TodayNominal)
|
||||
require.Equal(t, float64(20000), result.MainSummary[0].MtdNominal)
|
||||
|
||||
require.Equal(t, "hpp", result.MainSummary[1].ID)
|
||||
require.Equal(t, float64(4000), result.MainSummary[1].TodayNominal)
|
||||
require.Equal(t, float64(8000), result.MainSummary[1].MtdNominal)
|
||||
|
||||
require.Equal(t, "laba_kotor", result.MainSummary[2].ID)
|
||||
require.Equal(t, float64(6000), result.MainSummary[2].TodayNominal)
|
||||
require.Equal(t, float64(12000), result.MainSummary[2].MtdNominal)
|
||||
|
||||
require.Equal(t, "biaya_ops", result.MainSummary[3].ID)
|
||||
require.Equal(t, float64(800), result.MainSummary[3].TodayNominal)
|
||||
require.Equal(t, float64(1600), result.MainSummary[3].MtdNominal)
|
||||
require.Len(t, result.MainSummary[3].SubItems, 3) // 2 operational categories + 1 total
|
||||
|
||||
require.Equal(t, "by_promosi", result.MainSummary[3].SubItems[0].ID)
|
||||
require.Equal(t, float64(300), result.MainSummary[3].SubItems[0].TodayNominal)
|
||||
require.Equal(t, float64(600), result.MainSummary[3].SubItems[0].MtdNominal)
|
||||
|
||||
require.Equal(t, "by_sewa", result.MainSummary[3].SubItems[1].ID)
|
||||
require.Equal(t, float64(500), result.MainSummary[3].SubItems[1].TodayNominal)
|
||||
require.Equal(t, float64(1000), result.MainSummary[3].SubItems[1].MtdNominal)
|
||||
|
||||
require.Equal(t, "total_biaya_ops", result.MainSummary[3].SubItems[2].ID)
|
||||
require.True(t, result.MainSummary[3].SubItems[2].IsBold)
|
||||
require.Equal(t, float64(800), result.MainSummary[3].SubItems[2].TodayNominal)
|
||||
require.Equal(t, float64(1600), result.MainSummary[3].SubItems[2].MtdNominal)
|
||||
|
||||
require.Equal(t, "laba_rugi_sblm_gaji", result.MainSummary[4].ID)
|
||||
require.Equal(t, float64(5200), result.MainSummary[4].TodayNominal)
|
||||
require.Equal(t, float64(10400), result.MainSummary[4].MtdNominal)
|
||||
|
||||
require.Equal(t, "biaya_gaji", result.MainSummary[5].ID)
|
||||
require.Equal(t, float64(1500), result.MainSummary[5].TodayNominal)
|
||||
require.Equal(t, float64(3000), result.MainSummary[5].MtdNominal)
|
||||
|
||||
require.Equal(t, "laba_rugi", result.MainSummary[6].ID)
|
||||
require.Equal(t, float64(3700), result.MainSummary[6].TodayNominal)
|
||||
require.Equal(t, float64(7400), result.MainSummary[6].MtdNominal)
|
||||
require.True(t, result.MainSummary[6].IsBold)
|
||||
}
|
||||
|
||||
func TestAnalyticsProcessorGetExclusiveSummaryPeriodCalculatesTotalsAndReimburse(t *testing.T) {
|
||||
now := time.Date(2026, 5, 26, 0, 0, 0, 0, time.UTC)
|
||||
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{
|
||||
exclusiveSummaryResults: []*entities.ExclusiveSummaryAnalytics{
|
||||
{
|
||||
SalesTotal: 1000,
|
||||
HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{
|
||||
{CategoryCode: "RAW", CategoryName: "Raw", Amount: 400},
|
||||
},
|
||||
OperationalExpenseBreakdown: []entities.ExclusiveSummaryCategoryTotal{
|
||||
{CategoryCode: "GAJI", CategoryName: "Gaji", Amount: 250},
|
||||
{CategoryCode: "OPS", CategoryName: "Operasional", Amount: 100},
|
||||
},
|
||||
DailySummary: []entities.ExclusiveSummaryDailySummary{
|
||||
{Date: now, TransactionCount: 3, TotalCost: 750},
|
||||
},
|
||||
DailyTransactions: []entities.ExclusiveSummaryDailyTransaction{
|
||||
{Date: now, CategoryCode: "RAW", CategoryName: "Raw", Description: "beras", Amount: 400, Source: "purchase_order"},
|
||||
{Date: now, CategoryCode: "GAJI", CategoryName: "Gaji", Description: "gaji karyawan", Amount: 200, Source: "purchase_order"},
|
||||
{Date: now, CategoryCode: "GAJI", CategoryName: "Gaji", Description: "DW", Amount: 50, Source: "purchase_order"},
|
||||
{Date: now, CategoryCode: "OPS", CategoryName: "Operasional", Description: "atk", Amount: 100, Source: "purchase_order"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, expenseRepositoryStub{})
|
||||
|
||||
result, err := processor.GetExclusiveSummaryPeriod(context.Background(), &models.ExclusiveSummaryPeriodRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
DateFrom: now,
|
||||
DateTo: now,
|
||||
ExcludeGajiStaffFromReimburse: true,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, float64(1000), result.Summary.Sales)
|
||||
require.Equal(t, float64(400), result.Summary.HPP)
|
||||
require.Equal(t, float64(600), result.Summary.GrossProfit)
|
||||
require.Equal(t, float64(350), result.Summary.OperationalExpensesTotal)
|
||||
require.Equal(t, float64(750), result.Summary.TotalCost)
|
||||
require.Equal(t, float64(250), result.Summary.NetProfit)
|
||||
require.Equal(t, float64(250), result.Summary.SalaryTotal)
|
||||
require.Equal(t, float64(50), result.Summary.SalaryDW)
|
||||
require.Equal(t, float64(200), result.Summary.SalaryStaff)
|
||||
require.Equal(t, float64(100), result.Summary.OtherOperationalExpenses)
|
||||
require.Equal(t, float64(200), result.Reimburse.ExcludedSalaryStaff)
|
||||
require.Equal(t, float64(550), result.Reimburse.TotalReimburse)
|
||||
require.Len(t, result.HPPBreakdown, 1)
|
||||
require.Equal(t, float64(100), result.HPPBreakdown[0].Percentage)
|
||||
require.Len(t, result.DailySummary, 1)
|
||||
require.Len(t, result.DailyTransactions, 4)
|
||||
}
|
||||
|
||||
func TestAnalyticsProcessorGetExclusiveSummaryMonthlyBuildsSummaryAndBuckets(t *testing.T) {
|
||||
location, err := time.LoadLocation("Asia/Jakarta")
|
||||
require.NoError(t, err)
|
||||
month := time.Date(2026, 5, 1, 0, 0, 0, 0, location)
|
||||
openingBalance := 5000000.0
|
||||
closingBalance := 5000000.0
|
||||
notes := "Main cash account for daily transactions"
|
||||
stub := &analyticsRepositoryStub{
|
||||
exclusiveSummaryResults: []*entities.ExclusiveSummaryAnalytics{
|
||||
{SalesTotal: 1000, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 400}}, OperationalExpenseBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 100}}},
|
||||
{SalesTotal: 100, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 40}}},
|
||||
{SalesTotal: 200, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 80}}},
|
||||
{SalesTotal: 300, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 120}}},
|
||||
{SalesTotal: 400, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 160}}},
|
||||
{SalesTotal: 500, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 200}}},
|
||||
},
|
||||
bankBalances: []entities.ExclusiveSummaryBankBalance{
|
||||
{Bank: "Cash and Bank", OpeningBalance: &openingBalance, ClosingBalance: &closingBalance, Notes: ¬es},
|
||||
},
|
||||
}
|
||||
processor := NewAnalyticsProcessorImpl(stub, expenseRepositoryStub{})
|
||||
|
||||
result, err := processor.GetExclusiveSummaryMonthly(context.Background(), &models.ExclusiveSummaryMonthlyRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
Month: month,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, "2026-05", result.Month)
|
||||
require.Equal(t, float64(1000), result.Summary.TotalSales)
|
||||
require.Equal(t, float64(400), result.Summary.HPP)
|
||||
require.Equal(t, float64(500), result.Summary.NetProfit)
|
||||
require.InDelta(t, float64(50), result.Summary.NetProfitMargin, 0.0001)
|
||||
require.Len(t, result.Periods, 5)
|
||||
require.Equal(t, "1 - 3 Mei", result.Periods[0].Label)
|
||||
require.Equal(t, "25 - 31 Mei", result.Periods[4].Label)
|
||||
require.Len(t, result.BankBalance, 1)
|
||||
require.Equal(t, "Cash and Bank", result.BankBalance[0].Bank)
|
||||
require.NotNil(t, result.BankBalance[0].OpeningBalance)
|
||||
require.Equal(t, openingBalance, *result.BankBalance[0].OpeningBalance)
|
||||
require.NotNil(t, result.BankBalance[0].ClosingBalance)
|
||||
require.Equal(t, closingBalance, *result.BankBalance[0].ClosingBalance)
|
||||
require.Nil(t, result.BankBalance[0].IncomingMutation)
|
||||
require.Nil(t, result.BankBalance[0].OutgoingMutation)
|
||||
require.NotNil(t, result.BankBalance[0].Notes)
|
||||
require.Equal(t, notes, *result.BankBalance[0].Notes)
|
||||
require.Equal(t, 6, stub.exclusiveSummaryCalls)
|
||||
}
|
||||
|
||||
func TestAnalyticsProcessorGetExclusiveSummaryMTDBuildsMonthToDateBreakdown(t *testing.T) {
|
||||
location, err := time.LoadLocation("Asia/Jakarta")
|
||||
require.NoError(t, err)
|
||||
dateTo := time.Date(2026, 6, 18, 23, 59, 59, int(time.Second-time.Nanosecond), location)
|
||||
stub := &analyticsRepositoryStub{
|
||||
exclusiveSummaryResults: []*entities.ExclusiveSummaryAnalytics{
|
||||
{
|
||||
SalesTotal: 1000,
|
||||
HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{
|
||||
{CategoryCode: "RAW", CategoryName: "Raw Material", Amount: 400},
|
||||
},
|
||||
OperationalExpenseBreakdown: []entities.ExclusiveSummaryCategoryTotal{
|
||||
{CategoryCode: "OPS", CategoryName: "Operational", Amount: 100},
|
||||
},
|
||||
DailySummary: []entities.ExclusiveSummaryDailySummary{
|
||||
{Date: dateTo, TransactionCount: 2, TotalCost: 500},
|
||||
},
|
||||
DailyTransactions: []entities.ExclusiveSummaryDailyTransaction{
|
||||
{Date: dateTo, CategoryCode: "RAW", CategoryName: "Raw Material", Description: "beras", Amount: 400, Source: "purchase_order"},
|
||||
{Date: dateTo, CategoryCode: "OPS", CategoryName: "Operational", Description: "atk", Amount: 100, Source: "expense"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
processor := NewAnalyticsProcessorImpl(stub, expenseRepositoryStub{})
|
||||
|
||||
result, err := processor.GetExclusiveSummaryMTD(context.Background(), &models.ExclusiveSummaryMTDRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
DateTo: dateTo,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Len(t, stub.exclusiveSummaryFrom, 1)
|
||||
require.Equal(t, time.Date(2026, 6, 1, 0, 0, 0, 0, location), stub.exclusiveSummaryFrom[0])
|
||||
require.Equal(t, dateTo, stub.exclusiveSummaryTo[0])
|
||||
require.Equal(t, stub.exclusiveSummaryFrom[0], result.Period.DateFrom)
|
||||
require.Equal(t, dateTo, result.Period.DateTo)
|
||||
require.Equal(t, float64(1000), result.Summary.Sales)
|
||||
require.Equal(t, float64(400), result.Summary.HPP)
|
||||
require.Equal(t, float64(500), result.Summary.TotalCost)
|
||||
require.Equal(t, float64(500), result.Summary.NetProfit)
|
||||
require.Len(t, result.HPPBreakdown, 1)
|
||||
require.Equal(t, float64(100), result.HPPBreakdown[0].Percentage)
|
||||
require.Len(t, result.OperationalExpenseBreakdown, 1)
|
||||
require.Len(t, result.DailySummary, 1)
|
||||
require.Len(t, result.DailyTransactions, 2)
|
||||
}
|
||||
@ -1,352 +0,0 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ExpenseProcessor interface {
|
||||
CreateExpense(ctx context.Context, organizationID uuid.UUID, req *models.CreateExpenseRequest) (*models.ExpenseResponse, error)
|
||||
UpdateExpense(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdateExpenseRequest) (*models.ExpenseResponse, error)
|
||||
DeleteExpense(ctx context.Context, id, organizationID uuid.UUID) error
|
||||
GetExpenseByID(ctx context.Context, id, organizationID uuid.UUID) (*models.ExpenseResponse, error)
|
||||
ListExpenses(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.ExpenseResponse, int, error)
|
||||
GetExpenseAnalytics(ctx context.Context, req *models.ExpenseAnalyticsRequest) (*models.ExpenseAnalyticsResponse, error)
|
||||
}
|
||||
|
||||
type ExpenseProcessorImpl struct {
|
||||
expenseRepo ExpenseRepository
|
||||
purchaseCategoryRepo PurchaseCategoryRepository
|
||||
}
|
||||
|
||||
func NewExpenseProcessorImpl(expenseRepo ExpenseRepository, purchaseCategoryRepo PurchaseCategoryRepository) *ExpenseProcessorImpl {
|
||||
return &ExpenseProcessorImpl{
|
||||
expenseRepo: expenseRepo,
|
||||
purchaseCategoryRepo: purchaseCategoryRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID uuid.UUID, req *models.CreateExpenseRequest) (*models.ExpenseResponse, error) {
|
||||
outletID, err := uuid.Parse(req.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid outlet_id: %w", err)
|
||||
}
|
||||
|
||||
transactionDate, err := time.Parse("2006-01-02", req.TransactionDate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid transaction_date format, expected YYYY-MM-DD: %w", err)
|
||||
}
|
||||
|
||||
status := string(constants.ExpenseStatusDraft)
|
||||
if req.Status != nil {
|
||||
status = *req.Status
|
||||
}
|
||||
|
||||
items := make([]entities.ExpenseItem, len(req.Items))
|
||||
for i, itemReq := range req.Items {
|
||||
chartOfAccountID, err := uuid.Parse(itemReq.ChartOfAccountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid chart_of_account_id for item: %w", err)
|
||||
}
|
||||
|
||||
purchaseCategoryID, err := uuid.Parse(itemReq.PurchaseCategoryID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid purchase_category_id for item: %w", err)
|
||||
}
|
||||
if err := p.validateExpensePurchaseCategory(ctx, purchaseCategoryID, organizationID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items[i] = entities.ExpenseItem{
|
||||
ChartOfAccountID: chartOfAccountID,
|
||||
PurchaseCategoryID: purchaseCategoryID,
|
||||
Item: itemReq.Item,
|
||||
Description: itemReq.Description,
|
||||
Amount: itemReq.Amount,
|
||||
}
|
||||
}
|
||||
|
||||
expenseEntity := &entities.Expense{
|
||||
OrganizationID: organizationID,
|
||||
OutletID: outletID,
|
||||
Receiver: req.Receiver,
|
||||
TransactionDate: transactionDate,
|
||||
CodeNumber: req.CodeNumber,
|
||||
Status: status,
|
||||
Description: req.Description,
|
||||
Tax: req.Tax,
|
||||
Total: req.Total,
|
||||
}
|
||||
|
||||
err = p.expenseRepo.Create(ctx, expenseEntity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create expense: %w", err)
|
||||
}
|
||||
|
||||
for i := range items {
|
||||
items[i].ExpenseID = expenseEntity.ID
|
||||
|
||||
err = p.expenseRepo.CreateItem(ctx, &items[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create expense item: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
created, err := p.expenseRepo.GetByID(ctx, expenseEntity.ID)
|
||||
if err != nil {
|
||||
return mappers.ExpenseEntityToResponse(expenseEntity), nil
|
||||
}
|
||||
|
||||
return mappers.ExpenseEntityToResponse(created), nil
|
||||
}
|
||||
|
||||
func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdateExpenseRequest) (*models.ExpenseResponse, error) {
|
||||
expenseEntity, err := p.expenseRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("expense not found: %w", err)
|
||||
}
|
||||
|
||||
if req.Receiver != nil {
|
||||
expenseEntity.Receiver = *req.Receiver
|
||||
}
|
||||
if req.TransactionDate != nil {
|
||||
parsedDate, err := time.Parse("2006-01-02", *req.TransactionDate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid transaction_date format, expected YYYY-MM-DD: %w", err)
|
||||
}
|
||||
expenseEntity.TransactionDate = parsedDate
|
||||
}
|
||||
if req.CodeNumber != nil {
|
||||
expenseEntity.CodeNumber = *req.CodeNumber
|
||||
}
|
||||
if req.Status != nil {
|
||||
expenseEntity.Status = *req.Status
|
||||
}
|
||||
if req.OutletID != nil {
|
||||
outletID, err := uuid.Parse(*req.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid outlet_id: %w", err)
|
||||
}
|
||||
expenseEntity.OutletID = outletID
|
||||
}
|
||||
if req.Description != nil {
|
||||
expenseEntity.Description = req.Description
|
||||
}
|
||||
if req.Tax != nil {
|
||||
expenseEntity.Tax = *req.Tax
|
||||
}
|
||||
if req.Total != nil {
|
||||
expenseEntity.Total = *req.Total
|
||||
}
|
||||
if req.Reserved1 != nil {
|
||||
expenseEntity.Reserved1 = req.Reserved1
|
||||
}
|
||||
|
||||
var items []entities.ExpenseItem
|
||||
if req.Items != nil {
|
||||
items = make([]entities.ExpenseItem, len(req.Items))
|
||||
for i, itemReq := range req.Items {
|
||||
chartOfAccountID := uuid.Nil
|
||||
if itemReq.ChartOfAccountID != nil {
|
||||
chartOfAccountID, err = uuid.Parse(*itemReq.ChartOfAccountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid chart_of_account_id for item: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if itemReq.PurchaseCategoryID == nil {
|
||||
return nil, fmt.Errorf("purchase_category_id is required for item")
|
||||
}
|
||||
purchaseCategoryID, err := uuid.Parse(*itemReq.PurchaseCategoryID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid purchase_category_id for item: %w", err)
|
||||
}
|
||||
if err := p.validateExpensePurchaseCategory(ctx, purchaseCategoryID, organizationID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
amount := 0.0
|
||||
if itemReq.Amount != nil {
|
||||
amount = *itemReq.Amount
|
||||
}
|
||||
item := ""
|
||||
if itemReq.Item != nil {
|
||||
item = *itemReq.Item
|
||||
}
|
||||
|
||||
items[i] = entities.ExpenseItem{
|
||||
ExpenseID: expenseEntity.ID,
|
||||
ChartOfAccountID: chartOfAccountID,
|
||||
PurchaseCategoryID: purchaseCategoryID,
|
||||
Item: item,
|
||||
Description: itemReq.Description,
|
||||
Amount: amount,
|
||||
}
|
||||
}
|
||||
|
||||
err = p.expenseRepo.DeleteItemsByExpenseID(ctx, expenseEntity.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete existing items: %w", err)
|
||||
}
|
||||
|
||||
for i := range items {
|
||||
err = p.expenseRepo.CreateItem(ctx, &items[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create expense item: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = p.expenseRepo.Update(ctx, expenseEntity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update expense: %w", err)
|
||||
}
|
||||
|
||||
updated, err := p.expenseRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return mappers.ExpenseEntityToResponse(expenseEntity), nil
|
||||
}
|
||||
|
||||
return mappers.ExpenseEntityToResponse(updated), nil
|
||||
}
|
||||
|
||||
func (p *ExpenseProcessorImpl) DeleteExpense(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||
_, err := p.expenseRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expense not found: %w", err)
|
||||
}
|
||||
|
||||
err = p.expenseRepo.Delete(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete expense: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ExpenseProcessorImpl) GetExpenseByID(ctx context.Context, id, organizationID uuid.UUID) (*models.ExpenseResponse, error) {
|
||||
expenseEntity, err := p.expenseRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("expense not found: %w", err)
|
||||
}
|
||||
|
||||
return mappers.ExpenseEntityToResponse(expenseEntity), nil
|
||||
}
|
||||
|
||||
func (p *ExpenseProcessorImpl) ListExpenses(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.ExpenseResponse, int, error) {
|
||||
offset := (page - 1) * limit
|
||||
expenseEntities, total, err := p.expenseRepo.List(ctx, organizationID, filters, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list expenses: %w", err)
|
||||
}
|
||||
|
||||
expenseResponses := mappers.ExpenseEntitiesToResponses(expenseEntities)
|
||||
totalPages := int((total + int64(limit) - 1) / int64(limit))
|
||||
|
||||
return expenseResponses, totalPages, nil
|
||||
}
|
||||
|
||||
func (p *ExpenseProcessorImpl) GetExpenseAnalytics(ctx context.Context, req *models.ExpenseAnalyticsRequest) (*models.ExpenseAnalyticsResponse, error) {
|
||||
if req.DateFrom.After(req.DateTo) {
|
||||
return nil, fmt.Errorf("date_from cannot be after date_to")
|
||||
}
|
||||
|
||||
if req.GroupBy == "" {
|
||||
req.GroupBy = "day"
|
||||
}
|
||||
|
||||
result, err := p.expenseRepo.GetAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.GroupBy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get expense analytics: %w", err)
|
||||
}
|
||||
|
||||
data := make([]models.ExpenseAnalyticsData, len(result.Data))
|
||||
for i, item := range result.Data {
|
||||
data[i] = models.ExpenseAnalyticsData{
|
||||
Date: item.Date,
|
||||
Expenses: item.Expenses,
|
||||
ExpenseCount: item.ExpenseCount,
|
||||
Tax: item.Tax,
|
||||
Items: item.Items,
|
||||
Categories: item.Categories,
|
||||
}
|
||||
}
|
||||
|
||||
categoryData := make([]models.ExpenseAnalyticsCategoryData, len(result.CategoryData))
|
||||
for i, item := range result.CategoryData {
|
||||
categoryData[i] = models.ExpenseAnalyticsCategoryData{
|
||||
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||
PurchaseCategoryName: item.PurchaseCategoryName,
|
||||
PurchaseCategoryType: item.PurchaseCategoryType,
|
||||
TotalAmount: item.TotalAmount,
|
||||
ExpenseCount: item.ExpenseCount,
|
||||
ItemCount: item.ItemCount,
|
||||
}
|
||||
}
|
||||
|
||||
chartOfAccountData := make([]models.ExpenseAnalyticsChartOfAccountData, len(result.ChartOfAccountData))
|
||||
for i, item := range result.ChartOfAccountData {
|
||||
chartOfAccountData[i] = models.ExpenseAnalyticsChartOfAccountData{
|
||||
ChartOfAccountID: item.ChartOfAccountID,
|
||||
ChartOfAccountName: item.ChartOfAccountName,
|
||||
TotalAmount: item.TotalAmount,
|
||||
ExpenseCount: item.ExpenseCount,
|
||||
ItemCount: item.ItemCount,
|
||||
}
|
||||
}
|
||||
|
||||
itemData := make([]models.ExpenseAnalyticsItemData, len(result.ItemData))
|
||||
for i, item := range result.ItemData {
|
||||
itemData[i] = models.ExpenseAnalyticsItemData{
|
||||
Item: item.Item,
|
||||
TotalAmount: item.TotalAmount,
|
||||
ExpenseCount: item.ExpenseCount,
|
||||
ItemCount: item.ItemCount,
|
||||
}
|
||||
}
|
||||
|
||||
return &models.ExpenseAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
GroupBy: req.GroupBy,
|
||||
Summary: models.ExpenseAnalyticsSummary{
|
||||
TotalExpenses: result.Summary.TotalExpenses,
|
||||
TotalExpenseCount: result.Summary.TotalExpenseCount,
|
||||
TotalTax: result.Summary.TotalTax,
|
||||
AverageExpenseValue: result.Summary.AverageExpenseValue,
|
||||
TotalCategories: result.Summary.TotalCategories,
|
||||
TotalItems: result.Summary.TotalItems,
|
||||
},
|
||||
Data: data,
|
||||
CategoryData: categoryData,
|
||||
ChartOfAccountData: chartOfAccountData,
|
||||
ItemData: itemData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *ExpenseProcessorImpl) validateExpensePurchaseCategory(ctx context.Context, categoryID, organizationID uuid.UUID) error {
|
||||
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, categoryID, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("purchase category not found: %w", err)
|
||||
}
|
||||
|
||||
if !category.IsActive {
|
||||
return fmt.Errorf("purchase category is inactive")
|
||||
}
|
||||
|
||||
if category.Type != entities.PurchaseCategoryTypeExpense {
|
||||
return fmt.Errorf("purchase category must be expense")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,291 +0,0 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type expenseRepositoryCaptureStub struct {
|
||||
createdExpense *entities.Expense
|
||||
createdItems []*entities.ExpenseItem
|
||||
analytics *entities.ExpenseAnalytics
|
||||
}
|
||||
|
||||
type expensePurchaseCategoryRepositoryStub struct {
|
||||
category *entities.PurchaseCategory
|
||||
}
|
||||
|
||||
func (*expensePurchaseCategoryRepositoryStub) Create(context.Context, *entities.PurchaseCategory) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *expensePurchaseCategoryRepositoryStub) GetByIDAndOrganizationID(context.Context, uuid.UUID, uuid.UUID) (*entities.PurchaseCategory, error) {
|
||||
return s.category, nil
|
||||
}
|
||||
|
||||
func (*expensePurchaseCategoryRepositoryStub) Update(context.Context, *entities.PurchaseCategory) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*expensePurchaseCategoryRepositoryStub) SoftDelete(context.Context, uuid.UUID, uuid.UUID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*expensePurchaseCategoryRepositoryStub) List(context.Context, uuid.UUID, map[string]interface{}, int, int) ([]*entities.PurchaseCategory, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (*expensePurchaseCategoryRepositoryStub) ExistsByCode(context.Context, uuid.UUID, string, *uuid.UUID) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func newExpensePurchaseCategoryRepo(categoryID uuid.UUID, categoryType entities.PurchaseCategoryType) *expensePurchaseCategoryRepositoryStub {
|
||||
return &expensePurchaseCategoryRepositoryStub{
|
||||
category: &entities.PurchaseCategory{
|
||||
ID: categoryID,
|
||||
Name: "Operational",
|
||||
Type: categoryType,
|
||||
IsActive: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *expenseRepositoryCaptureStub) Create(_ context.Context, expense *entities.Expense) error {
|
||||
if expense.ID == uuid.Nil {
|
||||
expense.ID = uuid.New()
|
||||
}
|
||||
s.createdExpense = expense
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *expenseRepositoryCaptureStub) GetByID(context.Context, uuid.UUID) (*entities.Expense, error) {
|
||||
if s.createdExpense == nil {
|
||||
return nil, nil
|
||||
}
|
||||
items := make([]entities.ExpenseItem, len(s.createdItems))
|
||||
for i, item := range s.createdItems {
|
||||
items[i] = *item
|
||||
}
|
||||
s.createdExpense.Items = items
|
||||
return s.createdExpense, nil
|
||||
}
|
||||
|
||||
func (*expenseRepositoryCaptureStub) GetByIDAndOrganizationID(context.Context, uuid.UUID, uuid.UUID) (*entities.Expense, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (*expenseRepositoryCaptureStub) Update(context.Context, *entities.Expense) error { return nil }
|
||||
func (*expenseRepositoryCaptureStub) Delete(context.Context, uuid.UUID) error { return nil }
|
||||
func (*expenseRepositoryCaptureStub) List(context.Context, uuid.UUID, map[string]interface{}, int, int) ([]*entities.Expense, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
func (s *expenseRepositoryCaptureStub) GetAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time, string) (*entities.ExpenseAnalytics, error) {
|
||||
return s.analytics, nil
|
||||
}
|
||||
func (s *expenseRepositoryCaptureStub) CreateItem(_ context.Context, item *entities.ExpenseItem) error {
|
||||
if item.ID == uuid.Nil {
|
||||
item.ID = uuid.New()
|
||||
}
|
||||
s.createdItems = append(s.createdItems, item)
|
||||
return nil
|
||||
}
|
||||
func (*expenseRepositoryCaptureStub) DeleteItemsByExpenseID(context.Context, uuid.UUID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
||||
repo := &expenseRepositoryCaptureStub{}
|
||||
purchaseCategoryID := uuid.New()
|
||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
||||
chartOfAccountID := uuid.New()
|
||||
|
||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||
Receiver: "Cashier",
|
||||
TransactionDate: "2026-05-29",
|
||||
CodeNumber: "EXP-001",
|
||||
OutletID: uuid.NewString(),
|
||||
Total: 10000,
|
||||
Items: []models.CreateExpenseItemRequest{
|
||||
{
|
||||
ChartOfAccountID: chartOfAccountID.String(),
|
||||
PurchaseCategoryID: purchaseCategoryID.String(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Len(t, repo.createdItems, 1)
|
||||
require.Equal(t, "Cleaning supplies", repo.createdItems[0].Item)
|
||||
require.Equal(t, purchaseCategoryID, repo.createdItems[0].PurchaseCategoryID)
|
||||
require.Len(t, resp.Items, 1)
|
||||
require.Equal(t, "Cleaning supplies", resp.Items[0].Item)
|
||||
}
|
||||
|
||||
func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
||||
repo := &expenseRepositoryCaptureStub{}
|
||||
purchaseCategoryID := uuid.New()
|
||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
||||
|
||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||
Receiver: "Cashier",
|
||||
TransactionDate: "2026-05-29",
|
||||
CodeNumber: "EXP-001",
|
||||
OutletID: uuid.NewString(),
|
||||
Total: 10000,
|
||||
Items: []models.CreateExpenseItemRequest{
|
||||
{
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
PurchaseCategoryID: purchaseCategoryID.String(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, "draft", repo.createdExpense.Status)
|
||||
require.Equal(t, "draft", resp.Status)
|
||||
}
|
||||
|
||||
func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
||||
repo := &expenseRepositoryCaptureStub{}
|
||||
purchaseCategoryID := uuid.New()
|
||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
||||
status := "approved"
|
||||
|
||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||
Receiver: "Cashier",
|
||||
TransactionDate: "2026-05-29",
|
||||
CodeNumber: "EXP-001",
|
||||
OutletID: uuid.NewString(),
|
||||
Status: &status,
|
||||
Total: 10000,
|
||||
Items: []models.CreateExpenseItemRequest{
|
||||
{
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
PurchaseCategoryID: purchaseCategoryID.String(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, "approved", repo.createdExpense.Status)
|
||||
require.Equal(t, "approved", resp.Status)
|
||||
}
|
||||
|
||||
func TestExpenseProcessorCreateRejectsRawMaterialPurchaseCategory(t *testing.T) {
|
||||
repo := &expenseRepositoryCaptureStub{}
|
||||
purchaseCategoryID := uuid.New()
|
||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeRawMaterial))
|
||||
|
||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||
Receiver: "Cashier",
|
||||
TransactionDate: "2026-05-29",
|
||||
CodeNumber: "EXP-001",
|
||||
OutletID: uuid.NewString(),
|
||||
Total: 10000,
|
||||
Items: []models.CreateExpenseItemRequest{
|
||||
{
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
PurchaseCategoryID: purchaseCategoryID.String(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Nil(t, resp)
|
||||
require.Contains(t, err.Error(), "expense")
|
||||
}
|
||||
|
||||
func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *testing.T) {
|
||||
coaID := uuid.New()
|
||||
purchaseCategoryID := uuid.New()
|
||||
outletID := uuid.New()
|
||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
repo := &expenseRepositoryCaptureStub{
|
||||
analytics: &entities.ExpenseAnalytics{
|
||||
Summary: entities.ExpenseAnalyticsSummary{
|
||||
TotalExpenses: 100000,
|
||||
TotalExpenseCount: 2,
|
||||
TotalTax: 10000,
|
||||
AverageExpenseValue: 50000,
|
||||
TotalCategories: 1,
|
||||
TotalItems: 2,
|
||||
},
|
||||
Data: []entities.ExpenseAnalyticsData{
|
||||
{
|
||||
Date: now,
|
||||
Expenses: 100000,
|
||||
ExpenseCount: 2,
|
||||
Tax: 10000,
|
||||
Items: 2,
|
||||
Categories: 1,
|
||||
},
|
||||
},
|
||||
CategoryData: []entities.ExpenseAnalyticsCategoryData{
|
||||
{
|
||||
PurchaseCategoryID: purchaseCategoryID,
|
||||
PurchaseCategoryName: "Operational Supplies",
|
||||
PurchaseCategoryType: "expense",
|
||||
TotalAmount: 100000,
|
||||
ExpenseCount: 2,
|
||||
ItemCount: 2,
|
||||
},
|
||||
},
|
||||
ChartOfAccountData: []entities.ExpenseAnalyticsChartOfAccountData{
|
||||
{
|
||||
ChartOfAccountID: coaID,
|
||||
ChartOfAccountName: "Operational",
|
||||
TotalAmount: 100000,
|
||||
ExpenseCount: 2,
|
||||
ItemCount: 2,
|
||||
},
|
||||
},
|
||||
ItemData: []entities.ExpenseAnalyticsItemData{
|
||||
{
|
||||
Item: "Cleaning supplies",
|
||||
TotalAmount: 100000,
|
||||
ExpenseCount: 2,
|
||||
ItemCount: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
||||
|
||||
resp, err := p.GetExpenseAnalytics(context.Background(), &models.ExpenseAnalyticsRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
OutletID: &outletID,
|
||||
DateFrom: now,
|
||||
DateTo: now,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, "day", resp.GroupBy)
|
||||
require.Equal(t, &outletID, resp.OutletID)
|
||||
require.Equal(t, float64(100000), resp.Summary.TotalExpenses)
|
||||
require.Len(t, resp.Data, 1)
|
||||
require.Equal(t, int64(2), resp.Data[0].ExpenseCount)
|
||||
require.Len(t, resp.CategoryData, 1)
|
||||
require.Equal(t, purchaseCategoryID, resp.CategoryData[0].PurchaseCategoryID)
|
||||
require.Len(t, resp.ChartOfAccountData, 1)
|
||||
require.Equal(t, coaID, resp.ChartOfAccountData[0].ChartOfAccountID)
|
||||
require.Len(t, resp.ItemData, 1)
|
||||
require.Equal(t, "Cleaning supplies", resp.ItemData[0].Item)
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ExpenseRepository interface {
|
||||
Create(ctx context.Context, expense *entities.Expense) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.Expense, error)
|
||||
GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Expense, error)
|
||||
Update(ctx context.Context, expense *entities.Expense) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.Expense, int64, error)
|
||||
GetAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.ExpenseAnalytics, error)
|
||||
CreateItem(ctx context.Context, item *entities.ExpenseItem) error
|
||||
DeleteItemsByExpenseID(ctx context.Context, expenseID uuid.UUID) error
|
||||
}
|
||||
@ -1,338 +0,0 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/client"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// NotificationRepository is the interface the processor depends on.
|
||||
type NotificationRepository interface {
|
||||
Create(ctx context.Context, notification *entities.Notification) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.Notification, error)
|
||||
Update(ctx context.Context, notification *entities.Notification) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Notification, int64, error)
|
||||
}
|
||||
|
||||
// NotificationReceiverRepository is the interface the processor depends on.
|
||||
type NotificationReceiverRepository interface {
|
||||
Create(ctx context.Context, receiver *entities.NotificationReceiver) error
|
||||
BulkCreate(ctx context.Context, receivers []*entities.NotificationReceiver) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.NotificationReceiver, error)
|
||||
GetByNotificationAndUser(ctx context.Context, notificationID, userID uuid.UUID) (*entities.NotificationReceiver, error)
|
||||
Update(ctx context.Context, receiver *entities.NotificationReceiver) error
|
||||
ListByUserID(ctx context.Context, userID uuid.UUID, isRead *bool, limit, offset int) ([]*entities.NotificationReceiver, int64, error)
|
||||
CountUnreadByUserID(ctx context.Context, userID uuid.UUID) (int64, error)
|
||||
SoftDeleteByID(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
|
||||
// NotificationDeliveryRepository is the interface the processor depends on.
|
||||
type NotificationDeliveryRepository interface {
|
||||
Create(ctx context.Context, delivery *entities.NotificationDelivery) error
|
||||
BulkCreate(ctx context.Context, deliveries []*entities.NotificationDelivery) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.NotificationDelivery, error)
|
||||
Update(ctx context.Context, delivery *entities.NotificationDelivery) error
|
||||
ListByReceiverID(ctx context.Context, receiverID uuid.UUID) ([]*entities.NotificationDelivery, error)
|
||||
}
|
||||
|
||||
// NotificationUserRepository is a minimal interface to fetch user devices.
|
||||
type NotificationUserDeviceRepository interface {
|
||||
GetByUserID(ctx context.Context, userID uuid.UUID) ([]*entities.UserDevice, error)
|
||||
}
|
||||
|
||||
// NotificationUserRepository is a minimal interface to fetch users by org.
|
||||
type NotificationUserRepository interface {
|
||||
GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error)
|
||||
}
|
||||
|
||||
// NotificationProcessor defines the business logic interface.
|
||||
type NotificationProcessor interface {
|
||||
Send(ctx context.Context, req *models.SendNotificationRequest) (*models.NotificationResponse, error)
|
||||
Broadcast(ctx context.Context, req *models.BroadcastNotificationRequest) (*models.NotificationResponse, error)
|
||||
MarkAsRead(ctx context.Context, receiverID, userID uuid.UUID) (*models.NotificationReceiverResponse, error)
|
||||
MarkAllAsRead(ctx context.Context, userID uuid.UUID) error
|
||||
DeleteForUser(ctx context.Context, receiverID, userID uuid.UUID) error
|
||||
ListForUser(ctx context.Context, req *models.ListNotificationsRequest) ([]*models.NotificationReceiverResponse, int64, int64, error)
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*models.NotificationResponse, error)
|
||||
}
|
||||
|
||||
type NotificationProcessorImpl struct {
|
||||
notificationRepo NotificationRepository
|
||||
receiverRepo NotificationReceiverRepository
|
||||
deliveryRepo NotificationDeliveryRepository
|
||||
userDeviceRepo NotificationUserDeviceRepository
|
||||
userRepo NotificationUserRepository
|
||||
fcmClient client.FCMClient
|
||||
}
|
||||
|
||||
func NewNotificationProcessor(
|
||||
notificationRepo NotificationRepository,
|
||||
receiverRepo NotificationReceiverRepository,
|
||||
deliveryRepo NotificationDeliveryRepository,
|
||||
userDeviceRepo NotificationUserDeviceRepository,
|
||||
userRepo NotificationUserRepository,
|
||||
fcmClient client.FCMClient,
|
||||
) *NotificationProcessorImpl {
|
||||
return &NotificationProcessorImpl{
|
||||
notificationRepo: notificationRepo,
|
||||
receiverRepo: receiverRepo,
|
||||
deliveryRepo: deliveryRepo,
|
||||
userDeviceRepo: userDeviceRepo,
|
||||
userRepo: userRepo,
|
||||
fcmClient: fcmClient,
|
||||
}
|
||||
}
|
||||
|
||||
// Send creates a notification and dispatches it to the given receiver user IDs via FCM.
|
||||
func (p *NotificationProcessorImpl) Send(ctx context.Context, req *models.SendNotificationRequest) (*models.NotificationResponse, error) {
|
||||
if len(req.ReceiverIDs) == 0 {
|
||||
return nil, fmt.Errorf("at least one receiver_id is required")
|
||||
}
|
||||
|
||||
notification := &entities.Notification{
|
||||
Title: req.Title,
|
||||
Body: req.Body,
|
||||
Type: req.Type,
|
||||
Category: req.Category,
|
||||
Priority: req.Priority,
|
||||
ImageURL: req.ImageURL,
|
||||
ActionURL: req.ActionURL,
|
||||
NotifiableType: req.NotifiableType,
|
||||
NotifiableID: req.NotifiableID,
|
||||
Data: req.Data,
|
||||
ScheduledAt: req.ScheduledAt,
|
||||
ExpiredAt: req.ExpiredAt,
|
||||
CreatedBy: req.CreatedBy,
|
||||
}
|
||||
|
||||
if err := p.notificationRepo.Create(ctx, notification); err != nil {
|
||||
return nil, fmt.Errorf("failed to create notification: %w", err)
|
||||
}
|
||||
|
||||
// Create receiver records and dispatch FCM per user.
|
||||
for _, userID := range req.ReceiverIDs {
|
||||
receiver := &entities.NotificationReceiver{
|
||||
NotificationID: notification.ID,
|
||||
UserID: userID,
|
||||
}
|
||||
if err := p.receiverRepo.Create(ctx, receiver); err != nil {
|
||||
// Log but continue for other receivers.
|
||||
continue
|
||||
}
|
||||
|
||||
p.dispatchFCMToUser(ctx, receiver, notification)
|
||||
}
|
||||
|
||||
// Mark notification as sent.
|
||||
now := time.Now()
|
||||
notification.SentAt = &now
|
||||
_ = p.notificationRepo.Update(ctx, notification)
|
||||
|
||||
return mappers.NotificationEntityToResponse(notification), nil
|
||||
}
|
||||
|
||||
// Broadcast sends a notification to all active users in an organization.
|
||||
func (p *NotificationProcessorImpl) Broadcast(ctx context.Context, req *models.BroadcastNotificationRequest) (*models.NotificationResponse, error) {
|
||||
users, err := p.userRepo.GetActiveUsers(ctx, req.OrganizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch organization users: %w", err)
|
||||
}
|
||||
|
||||
notification := &entities.Notification{
|
||||
Title: req.Title,
|
||||
Body: req.Body,
|
||||
Type: req.Type,
|
||||
Category: req.Category,
|
||||
Priority: req.Priority,
|
||||
ImageURL: req.ImageURL,
|
||||
ActionURL: req.ActionURL,
|
||||
NotifiableType: req.NotifiableType,
|
||||
NotifiableID: req.NotifiableID,
|
||||
Data: req.Data,
|
||||
ScheduledAt: req.ScheduledAt,
|
||||
ExpiredAt: req.ExpiredAt,
|
||||
CreatedBy: req.CreatedBy,
|
||||
}
|
||||
|
||||
if err := p.notificationRepo.Create(ctx, notification); err != nil {
|
||||
return nil, fmt.Errorf("failed to create notification: %w", err)
|
||||
}
|
||||
|
||||
// Build receiver records in bulk.
|
||||
receivers := make([]*entities.NotificationReceiver, 0, len(users))
|
||||
for _, u := range users {
|
||||
receivers = append(receivers, &entities.NotificationReceiver{
|
||||
NotificationID: notification.ID,
|
||||
UserID: u.ID,
|
||||
})
|
||||
}
|
||||
|
||||
if err := p.receiverRepo.BulkCreate(ctx, receivers); err != nil {
|
||||
return nil, fmt.Errorf("failed to create notification receivers: %w", err)
|
||||
}
|
||||
|
||||
// Dispatch FCM for each receiver.
|
||||
for _, receiver := range receivers {
|
||||
p.dispatchFCMToUser(ctx, receiver, notification)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
notification.SentAt = &now
|
||||
_ = p.notificationRepo.Update(ctx, notification)
|
||||
|
||||
return mappers.NotificationEntityToResponse(notification), nil
|
||||
}
|
||||
|
||||
// MarkAsRead marks a single notification receiver record as read.
|
||||
func (p *NotificationProcessorImpl) MarkAsRead(ctx context.Context, receiverID, userID uuid.UUID) (*models.NotificationReceiverResponse, error) {
|
||||
receiver, err := p.receiverRepo.GetByID(ctx, receiverID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("notification not found: %w", err)
|
||||
}
|
||||
|
||||
if receiver.UserID != userID {
|
||||
return nil, fmt.Errorf("unauthorized: notification does not belong to user")
|
||||
}
|
||||
|
||||
if !receiver.IsRead {
|
||||
now := time.Now()
|
||||
receiver.IsRead = true
|
||||
receiver.ReadAt = &now
|
||||
if err := p.receiverRepo.Update(ctx, receiver); err != nil {
|
||||
return nil, fmt.Errorf("failed to mark notification as read: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return mappers.NotificationReceiverEntityToResponse(receiver), nil
|
||||
}
|
||||
|
||||
// MarkAllAsRead marks all unread notifications for a user as read.
|
||||
func (p *NotificationProcessorImpl) MarkAllAsRead(ctx context.Context, userID uuid.UUID) error {
|
||||
isRead := false
|
||||
receivers, _, err := p.receiverRepo.ListByUserID(ctx, userID, &isRead, 1000, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch unread notifications: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, r := range receivers {
|
||||
r.IsRead = true
|
||||
r.ReadAt = &now
|
||||
_ = p.receiverRepo.Update(ctx, r)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteForUser soft-deletes a notification receiver record for a user.
|
||||
func (p *NotificationProcessorImpl) DeleteForUser(ctx context.Context, receiverID, userID uuid.UUID) error {
|
||||
receiver, err := p.receiverRepo.GetByID(ctx, receiverID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("notification not found: %w", err)
|
||||
}
|
||||
|
||||
if receiver.UserID != userID {
|
||||
return fmt.Errorf("unauthorized: notification does not belong to user")
|
||||
}
|
||||
|
||||
return p.receiverRepo.SoftDeleteByID(ctx, receiverID)
|
||||
}
|
||||
|
||||
// ListForUser returns paginated notifications for a user.
|
||||
// Returns: receivers, total, unreadCount, error
|
||||
func (p *NotificationProcessorImpl) ListForUser(ctx context.Context, req *models.ListNotificationsRequest) ([]*models.NotificationReceiverResponse, int64, int64, error) {
|
||||
offset := (req.Page - 1) * req.Limit
|
||||
|
||||
receivers, total, err := p.receiverRepo.ListByUserID(ctx, req.UserID, req.IsRead, req.Limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, 0, fmt.Errorf("failed to list notifications: %w", err)
|
||||
}
|
||||
|
||||
unreadCount, err := p.receiverRepo.CountUnreadByUserID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
unreadCount = 0
|
||||
}
|
||||
|
||||
responses := mappers.NotificationReceiverEntitiesToResponses(receivers)
|
||||
return responses, total, unreadCount, nil
|
||||
}
|
||||
|
||||
// GetByID returns a single notification by its ID.
|
||||
func (p *NotificationProcessorImpl) GetByID(ctx context.Context, id uuid.UUID) (*models.NotificationResponse, error) {
|
||||
notification, err := p.notificationRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("notification not found: %w", err)
|
||||
}
|
||||
return mappers.NotificationEntityToResponse(notification), nil
|
||||
}
|
||||
|
||||
// dispatchFCMToUser fetches all FCM tokens for a user and sends the push notification.
|
||||
func (p *NotificationProcessorImpl) dispatchFCMToUser(ctx context.Context, receiver *entities.NotificationReceiver, notification *entities.Notification) {
|
||||
if p.fcmClient == nil {
|
||||
return
|
||||
}
|
||||
|
||||
devices, err := p.userDeviceRepo.GetByUserID(ctx, receiver.UserID)
|
||||
if err != nil || len(devices) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Build FCM data payload.
|
||||
data := map[string]string{
|
||||
"notification_id": notification.ID.String(),
|
||||
"notification_receiver_id": receiver.ID.String(),
|
||||
"type": notification.Type,
|
||||
"category": notification.Category,
|
||||
"action_url": notification.ActionURL,
|
||||
}
|
||||
|
||||
// Collect valid FCM tokens and create delivery records.
|
||||
tokens := make([]string, 0, len(devices))
|
||||
deliveries := make([]*entities.NotificationDelivery, 0, len(devices))
|
||||
|
||||
for _, device := range devices {
|
||||
if device.FCMToken == "" {
|
||||
continue
|
||||
}
|
||||
tokens = append(tokens, device.FCMToken)
|
||||
deliveries = append(deliveries, &entities.NotificationDelivery{
|
||||
NotificationReceiverID: receiver.ID,
|
||||
UserDeviceID: device.ID,
|
||||
Channel: entities.NotificationChannelPush,
|
||||
DeliveryStatus: entities.NotificationDeliveryStatusPending,
|
||||
Provider: entities.NotificationProviderFirebase,
|
||||
})
|
||||
}
|
||||
|
||||
if len(tokens) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Persist delivery records before sending.
|
||||
_ = p.deliveryRepo.BulkCreate(ctx, deliveries)
|
||||
|
||||
// Send via FCM multicast.
|
||||
now := time.Now()
|
||||
sendErr := p.fcmClient.SendMulticastNotification(ctx, tokens, notification.Title, notification.Body, data)
|
||||
|
||||
// Update delivery status.
|
||||
for _, delivery := range deliveries {
|
||||
if sendErr != nil {
|
||||
delivery.DeliveryStatus = entities.NotificationDeliveryStatusFailed
|
||||
delivery.FailedAt = &now
|
||||
delivery.FailureReason = sendErr.Error()
|
||||
} else {
|
||||
delivery.DeliveryStatus = entities.NotificationDeliveryStatusSent
|
||||
delivery.SentAt = &now
|
||||
}
|
||||
_ = p.deliveryRepo.Update(ctx, delivery)
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/constants"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -86,7 +87,7 @@ type CustomerRepository interface {
|
||||
}
|
||||
|
||||
type InventoryMovementService interface {
|
||||
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error
|
||||
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||
}
|
||||
|
||||
@ -107,7 +108,6 @@ type OrderProcessorImpl struct {
|
||||
productRecipeRepo *repository.ProductRecipeRepository
|
||||
ingredientRepo IngredientRepository
|
||||
inventoryMovementService InventoryMovementService
|
||||
productOutletPriceRepo repository.ProductOutletPriceRepository
|
||||
}
|
||||
|
||||
func NewOrderProcessorImpl(
|
||||
@ -126,7 +126,6 @@ func NewOrderProcessorImpl(
|
||||
productRecipeRepo *repository.ProductRecipeRepository,
|
||||
ingredientRepo IngredientRepository,
|
||||
inventoryMovementService InventoryMovementService,
|
||||
productOutletPriceRepo repository.ProductOutletPriceRepository,
|
||||
) *OrderProcessorImpl {
|
||||
return &OrderProcessorImpl{
|
||||
orderRepo: orderRepo,
|
||||
@ -145,7 +144,6 @@ func NewOrderProcessorImpl(
|
||||
productRecipeRepo: productRecipeRepo,
|
||||
ingredientRepo: ingredientRepo,
|
||||
inventoryMovementService: inventoryMovementService,
|
||||
productOutletPriceRepo: productOutletPriceRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,12 +170,6 @@ func (p *OrderProcessorImpl) CreateOrder(ctx context.Context, req *models.Create
|
||||
unitPrice := product.Price
|
||||
unitCost := product.Cost
|
||||
|
||||
if p.productOutletPriceRepo != nil {
|
||||
if outletPrice, err := p.productOutletPriceRepo.GetByProductAndOutlet(ctx, itemReq.ProductID, req.OutletID); err == nil {
|
||||
unitPrice = outletPrice.Price
|
||||
}
|
||||
}
|
||||
|
||||
if itemReq.ProductVariantID != nil {
|
||||
variant, err := p.productVariantRepo.GetByID(ctx, *itemReq.ProductVariantID)
|
||||
if err != nil {
|
||||
@ -301,12 +293,6 @@ func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID,
|
||||
unitPrice := product.Price
|
||||
unitCost := product.Cost
|
||||
|
||||
if p.productOutletPriceRepo != nil {
|
||||
if outletPrice, err := p.productOutletPriceRepo.GetByProductAndOutlet(ctx, itemReq.ProductID, order.OutletID); err == nil {
|
||||
unitPrice = outletPrice.Price
|
||||
}
|
||||
}
|
||||
|
||||
// Handle product variant if specified
|
||||
if itemReq.ProductVariantID != nil {
|
||||
variant, err := p.productVariantRepo.GetByID(ctx, *itemReq.ProductVariantID)
|
||||
@ -338,7 +324,7 @@ func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID,
|
||||
ProductID: itemReq.ProductID,
|
||||
ProductVariantID: itemReq.ProductVariantID,
|
||||
Quantity: itemReq.Quantity,
|
||||
UnitPrice: unitPrice,
|
||||
UnitPrice: unitPrice, // Use price from database
|
||||
TotalPrice: itemTotalPrice,
|
||||
UnitCost: unitCost,
|
||||
TotalCost: itemTotalCost,
|
||||
@ -387,10 +373,31 @@ func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID,
|
||||
return nil, fmt.Errorf("failed to create order item: %w", err)
|
||||
}
|
||||
|
||||
itemResponse := mappers.OrderItemEntityToResponse(orderItem, order.OutletID)
|
||||
if itemResponse != nil {
|
||||
addedItemResponses = append(addedItemResponses, *itemResponse)
|
||||
itemResponse := models.OrderItemResponse{
|
||||
ID: orderItem.ID,
|
||||
OrderID: orderItem.OrderID,
|
||||
ProductID: orderItem.ProductID,
|
||||
ProductVariantID: orderItem.ProductVariantID,
|
||||
Quantity: orderItem.Quantity,
|
||||
UnitPrice: orderItem.UnitPrice,
|
||||
TotalPrice: orderItem.TotalPrice,
|
||||
UnitCost: orderItem.UnitCost,
|
||||
TotalCost: orderItem.TotalCost,
|
||||
RefundAmount: orderItem.RefundAmount,
|
||||
RefundQuantity: orderItem.RefundQuantity,
|
||||
IsPartiallyRefunded: orderItem.IsPartiallyRefunded,
|
||||
IsFullyRefunded: orderItem.IsFullyRefunded,
|
||||
RefundReason: orderItem.RefundReason,
|
||||
RefundedAt: orderItem.RefundedAt,
|
||||
RefundedBy: orderItem.RefundedBy,
|
||||
Modifiers: []map[string]interface{}(orderItem.Modifiers),
|
||||
Notes: orderItem.Notes,
|
||||
Metadata: map[string]interface{}(orderItem.Metadata),
|
||||
Status: constants.OrderItemStatus(orderItem.Status),
|
||||
CreatedAt: orderItem.CreatedAt,
|
||||
UpdatedAt: orderItem.UpdatedAt,
|
||||
}
|
||||
addedItemResponses = append(addedItemResponses, itemResponse)
|
||||
}
|
||||
|
||||
orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, orderID)
|
||||
@ -594,10 +601,6 @@ func (p *OrderProcessorImpl) VoidOrder(ctx context.Context, req *models.VoidOrde
|
||||
return fmt.Errorf("order item does not belong to this order")
|
||||
}
|
||||
|
||||
if orderItem.Status == entities.OrderItemStatusCancelled {
|
||||
return fmt.Errorf("order item %s is already cancelled", orderItemID)
|
||||
}
|
||||
|
||||
if itemVoid.Quantity > orderItem.Quantity {
|
||||
return fmt.Errorf("void quantity cannot exceed original quantity for item %d", itemVoid.OrderItemID)
|
||||
}
|
||||
@ -618,15 +621,9 @@ func (p *OrderProcessorImpl) VoidOrder(ctx context.Context, req *models.VoidOrde
|
||||
return fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
// Reload order to get latest state
|
||||
order, err = p.orderRepo.GetByID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to reload order: %w", err)
|
||||
}
|
||||
|
||||
order.Subtotal -= totalVoidedAmount
|
||||
order.TotalCost -= totalVoidedCost
|
||||
order.TaxAmount = order.Subtotal * outlet.TaxRate
|
||||
order.TaxAmount = order.Subtotal * outlet.TaxRate // Recalculate tax using outlet's tax rate
|
||||
order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount
|
||||
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user