Compare commits
82
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a7ac25dcf | ||
|
|
d0a548f44e | ||
|
|
f4172fcea7 | ||
|
|
d5216e7994 | ||
|
|
1718c5adab | ||
|
|
d0c090a657 | ||
|
|
c3db919531 | ||
|
|
e09feff36d | ||
|
|
e7dd9660da | ||
|
|
c57620beeb | ||
|
|
29aeb58fc0 | ||
|
|
69d8c8ce5e | ||
|
|
6e3fc43d86 | ||
|
|
021ec152e9 | ||
|
|
ea9dceb333 | ||
|
|
afa1aa5b75 | ||
|
|
328336ea5a | ||
|
|
094e8b2a47 | ||
|
|
b90a3cde4a | ||
|
|
7c8c7fb7db | ||
|
|
343aa25230 | ||
|
|
47fa21d739 | ||
|
|
dc13bb5f93 | ||
|
|
d26f5c5354 | ||
|
|
1b7bec4f81 | ||
|
|
f7399fd0e7 | ||
|
|
cd61ad0eb9 | ||
|
|
84222fc7f4 | ||
|
|
23ac572e3f | ||
|
|
66a8126da0 | ||
|
|
a55a3f4ee2 | ||
|
|
024d9ee637 | ||
|
|
957c1ae53d | ||
|
|
b8be29e110 | ||
|
|
da87d659df | ||
|
|
d0378b5ac4 | ||
|
|
91960f0e57 | ||
|
|
72f67cb519 | ||
|
|
35c4cf2f2f | ||
|
|
c9ef90f5ea | ||
|
|
35e7152abb | ||
|
|
d9b51a7616 | ||
|
|
b27e40b531 | ||
|
|
44aca7641f | ||
|
|
a89ff00d94 | ||
|
|
227f11359c | ||
|
|
7a737d7f83 | ||
|
|
312ea94e62 | ||
|
|
6d735c20cb | ||
|
|
cb8a830345 | ||
|
|
9c143a43aa | ||
|
|
222cadd8df | ||
|
|
cad4e6c816 | ||
|
|
50d633ee3a | ||
|
|
21fa21d089 | ||
|
|
5f379faf17 | ||
|
|
3b62504798 | ||
|
|
4130cb66df | ||
|
|
30dff17272 | ||
|
|
fa037b4d2a | ||
|
|
d38a770ec5 | ||
|
|
015292e830 | ||
|
|
f8c732f0ff | ||
|
|
e92c487815 | ||
|
|
c573b23d76 | ||
|
|
f73a5d533c | ||
|
|
4ea8e32a8e | ||
|
|
06d79046d0 | ||
|
|
8eb19c57ba | ||
|
|
f123de7233 | ||
|
|
7ba776555e | ||
|
|
bccf02b5f7 | ||
|
|
c24a8a8c13 | ||
|
|
6064ef8fde | ||
|
|
1834dd0b19 | ||
|
|
9f653eef37 | ||
|
|
ddaf6df436 | ||
|
|
0708ce816e | ||
|
|
2c34578a98 | ||
|
|
9d71b339b5 | ||
|
|
bbd6666299 | ||
|
|
23b6293502 |
+2
-1
@@ -7,4 +7,5 @@ config/env/*
|
||||
|
||||
vendor
|
||||
|
||||
*firebase-adminsdk*.json
|
||||
# Firebase service account credentials
|
||||
infra/firebase-service-account.json
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# 1) Build stage
|
||||
FROM golang:1.21-alpine AS build
|
||||
FROM golang:1.24-alpine AS build
|
||||
RUN apk --no-cache add ca-certificates tzdata git curl
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
@@ -83,6 +83,12 @@ 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)
|
||||
|
||||
+3
-3
@@ -31,7 +31,7 @@ type Config struct {
|
||||
Log Log `mapstructure:"log"`
|
||||
S3Config S3Config `mapstructure:"s3"`
|
||||
Fonnte Fonnte `mapstructure:"fonnte"`
|
||||
Firebase Firebase `mapstructure:"firebase"`
|
||||
FCM FCM `mapstructure:"fcm"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -97,6 +97,6 @@ func (c *Config) GetFonnte() *Fonnte {
|
||||
return &c.Fonnte
|
||||
}
|
||||
|
||||
func (c *Config) GetFirebase() *Firebase {
|
||||
return &c.Firebase
|
||||
func (c *Config) GetFCM() *FCM {
|
||||
return &c.FCM
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package config
|
||||
|
||||
type Firebase struct {
|
||||
CredentialsFile string `mapstructure:"credentials_file"`
|
||||
}
|
||||
|
||||
func (f *Firebase) GetCredentialsFile() string {
|
||||
return f.CredentialsFile
|
||||
}
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
package config
|
||||
|
||||
type Server struct {
|
||||
Port string `mapstructure:"port"`
|
||||
BaseUrl string `mapstructure:"common-url"`
|
||||
LocalUrl string `mapstructure:"local-url"`
|
||||
Port string `mapstructure:"port"`
|
||||
BaseUrl string `mapstructure:"common-url"`
|
||||
LocalUrl string `mapstructure:"local-url"`
|
||||
SelfOrderUrl string `mapstructure:"self-order-url"`
|
||||
}
|
||||
|
||||
@@ -351,8 +351,6 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
|
||||
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
@@ -382,6 +380,7 @@ go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
server:
|
||||
base-url:
|
||||
local-url:
|
||||
self-order-url: http://localhost:5173
|
||||
port: 4000
|
||||
|
||||
jwt:
|
||||
@@ -28,7 +29,7 @@ postgresql:
|
||||
debug: false
|
||||
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
host: 194.233.78.1
|
||||
port: 6379
|
||||
password: "CmICdmnX1EZPhVBYzQPEGw==U"
|
||||
db: 0
|
||||
@@ -55,5 +56,6 @@ fonnte:
|
||||
token: "bADQrf9NTXfLZQCK2wGg"
|
||||
timeout: 30
|
||||
|
||||
firebase:
|
||||
credentials_file: "apskel-pos-v2-firebase-adminsdk-fbsvc-ae00499526.json"
|
||||
fcm:
|
||||
credentials_file: "infra/firebase-service-account.json"
|
||||
project_id: "apskel-pos-v2"
|
||||
+112
-16
@@ -25,11 +25,12 @@ import (
|
||||
)
|
||||
|
||||
type App struct {
|
||||
server *http.Server
|
||||
db *gorm.DB
|
||||
redisClient *redis.Client
|
||||
router *router.Router
|
||||
shutdown chan os.Signal
|
||||
server *http.Server
|
||||
db *gorm.DB
|
||||
redisClient *redis.Client
|
||||
router *router.Router
|
||||
shutdown chan os.Signal
|
||||
omsetScheduler *service.OmsetMilestoneScheduler
|
||||
}
|
||||
|
||||
func NewApp(db *gorm.DB, redisClient *redis.Client) *App {
|
||||
@@ -43,6 +44,15 @@ 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)
|
||||
@@ -56,7 +66,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
repos.userRepo,
|
||||
repos.sessionRepo,
|
||||
repos.orderRepo,
|
||||
processors.fcmClient,
|
||||
services.productOutletPriceService,
|
||||
)
|
||||
|
||||
a.router = router.NewRouter(
|
||||
@@ -98,6 +108,8 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
validators.vendorValidator,
|
||||
services.purchaseOrderService,
|
||||
validators.purchaseOrderValidator,
|
||||
services.purchaseCategoryService,
|
||||
validators.purchaseCategoryValidator,
|
||||
services.unitConverterService,
|
||||
validators.unitConverterValidator,
|
||||
services.chartOfAccountTypeService,
|
||||
@@ -119,13 +131,27 @@ 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{
|
||||
@@ -161,6 +187,9 @@ func (a *App) Start(port string) error {
|
||||
}
|
||||
|
||||
func (a *App) Shutdown() {
|
||||
if a.omsetScheduler != nil {
|
||||
a.omsetScheduler.Stop()
|
||||
}
|
||||
close(a.shutdown)
|
||||
}
|
||||
|
||||
@@ -189,6 +218,7 @@ type repositories struct {
|
||||
productRecipeRepo *repository.ProductRecipeRepository
|
||||
vendorRepo *repository.VendorRepositoryImpl
|
||||
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
|
||||
purchaseCategoryRepo *repository.PurchaseCategoryRepositoryImpl
|
||||
unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl
|
||||
chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl
|
||||
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
||||
@@ -208,6 +238,12 @@ 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 {
|
||||
@@ -236,6 +272,7 @@ 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),
|
||||
@@ -255,6 +292,12 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +321,7 @@ type processors struct {
|
||||
productRecipeProcessor *processor.ProductRecipeProcessorImpl
|
||||
vendorProcessor *processor.VendorProcessorImpl
|
||||
purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl
|
||||
purchaseCategoryProcessor *processor.PurchaseCategoryProcessorImpl
|
||||
unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl
|
||||
chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl
|
||||
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
|
||||
@@ -296,14 +340,16 @@ 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)
|
||||
|
||||
@@ -313,20 +359,21 @@ 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),
|
||||
productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo, repos.productOutletPriceRepo),
|
||||
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),
|
||||
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),
|
||||
paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo),
|
||||
fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient),
|
||||
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
|
||||
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo),
|
||||
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo, repos.expenseRepo),
|
||||
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.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
||||
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.purchaseCategoryRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
||||
purchaseCategoryProcessor: processor.NewPurchaseCategoryProcessorImpl(repos.purchaseCategoryRepo),
|
||||
unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo),
|
||||
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
|
||||
chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo),
|
||||
@@ -345,8 +392,11 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,6 +422,7 @@ type services struct {
|
||||
productRecipeService *service.ProductRecipeServiceImpl
|
||||
vendorService *service.VendorServiceImpl
|
||||
purchaseOrderService *service.PurchaseOrderServiceImpl
|
||||
purchaseCategoryService service.PurchaseCategoryService
|
||||
unitConverterService *service.IngredientUnitConverterServiceImpl
|
||||
chartOfAccountTypeService service.ChartOfAccountTypeService
|
||||
chartOfAccountService service.ChartOfAccountService
|
||||
@@ -383,11 +434,15 @@ 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, authConfig)
|
||||
authService := service.NewAuthService(processors.userProcessor, processors.userDeviceProcessor, authConfig)
|
||||
organizationService := service.NewOrganizationService(processors.organizationProcessor)
|
||||
outletService := service.NewOutletService(processors.outletProcessor)
|
||||
outletSettingService := service.NewOutletSettingService(processors.outletSettingProcessor)
|
||||
@@ -395,7 +450,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) // Will be updated after orderIngredientTransactionService is created
|
||||
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
|
||||
paymentMethodService := service.NewPaymentMethodService(processors.paymentMethodProcessor)
|
||||
fileService := service.NewFileServiceImpl(processors.fileProcessor)
|
||||
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor)
|
||||
@@ -407,6 +462,7 @@ 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)
|
||||
@@ -418,9 +474,11 @@ 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)
|
||||
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo, processors.notificationProcessor, repos.userRepo)
|
||||
|
||||
return &services{
|
||||
userService: service.NewUserService(processors.userProcessor),
|
||||
@@ -444,6 +502,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
productRecipeService: productRecipeService,
|
||||
vendorService: vendorService,
|
||||
purchaseOrderService: purchaseOrderService,
|
||||
purchaseCategoryService: purchaseCategoryService,
|
||||
unitConverterService: unitConverterService,
|
||||
chartOfAccountTypeService: chartOfAccountTypeService,
|
||||
chartOfAccountService: chartOfAccountService,
|
||||
@@ -455,6 +514,10 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,6 +548,7 @@ type validators struct {
|
||||
tableValidator *validator.TableValidator
|
||||
vendorValidator *validator.VendorValidatorImpl
|
||||
purchaseOrderValidator *validator.PurchaseOrderValidatorImpl
|
||||
purchaseCategoryValidator *validator.PurchaseCategoryValidatorImpl
|
||||
unitConverterValidator *validator.IngredientUnitConverterValidatorImpl
|
||||
chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl
|
||||
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
|
||||
@@ -494,6 +558,10 @@ 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 {
|
||||
@@ -512,6 +580,7 @@ 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),
|
||||
@@ -521,5 +590,32 @@ 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,81 +3,140 @@ 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 FcmClient interface {
|
||||
SendMulticastNotification(ctx context.Context, tokens []string, title string, body string) error
|
||||
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 struct {
|
||||
messagingClient *messaging.Client
|
||||
messaging *messaging.Client
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
func NewFCMClient(cfg FCMConfig) (FCMClient, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
opt := option.WithCredentialsFile(cfg.GetCredentialsFile())
|
||||
app, err := firebase.NewApp(context.Background(), nil, opt)
|
||||
|
||||
app, err := firebase.NewApp(ctx, &firebase.Config{
|
||||
ProjectID: cfg.GetProjectID(),
|
||||
}, opt)
|
||||
if err != nil {
|
||||
log.Printf("FCM: failed to initialize Firebase app: %v", err)
|
||||
return &fcmClient{messagingClient: nil}
|
||||
return nil, fmt.Errorf("failed to initialize firebase app: %w", err)
|
||||
}
|
||||
|
||||
client, err := app.Messaging(context.Background())
|
||||
msgClient, err := app.Messaging(ctx)
|
||||
if err != nil {
|
||||
log.Printf("FCM: failed to create messaging client: %v", err)
|
||||
return &fcmClient{messagingClient: nil}
|
||||
return nil, fmt.Errorf("failed to initialize firebase messaging client: %w", err)
|
||||
}
|
||||
|
||||
log.Println("FCM: client initialized successfully")
|
||||
return &fcmClient{messagingClient: client}
|
||||
return &fcmClient{
|
||||
messaging: msgClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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
|
||||
// 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, 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{
|
||||
Tokens: tokens,
|
||||
Notification: &messaging.Notification{
|
||||
Title: title,
|
||||
Body: body,
|
||||
},
|
||||
Tokens: tokens,
|
||||
Data: data,
|
||||
Android: &messaging.AndroidConfig{
|
||||
Priority: "high",
|
||||
},
|
||||
APNS: &messaging.APNSConfig{
|
||||
Payload: &messaging.APNSPayload{
|
||||
Aps: &messaging.Aps{
|
||||
Sound: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
response, err := c.messagingClient.SendMulticast(ctx, message)
|
||||
batchResp, err := f.messaging.SendEachForMulticast(ctx, message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("FCM: failed to send multicast notification: %w", err)
|
||||
return fmt.Errorf("failed to send FCM multicast 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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
log.Printf("FCM: sent %d/%d notifications successfully", response.SuccessCount, len(tokens))
|
||||
return nil
|
||||
}
|
||||
|
||||
+18
-12
@@ -40,22 +40,28 @@ 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"
|
||||
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"
|
||||
)
|
||||
|
||||
var HttpErrorMap = map[string]int{
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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
|
||||
}
|
||||
@@ -7,6 +7,7 @@ const (
|
||||
RoleManager UserRole = "manager"
|
||||
RoleCashier UserRole = "cashier"
|
||||
RoleWaiter UserRole = "waiter"
|
||||
RoleOwner UserRole = "owner"
|
||||
)
|
||||
|
||||
func GetAllUserRoles() []UserRole {
|
||||
@@ -15,6 +16,7 @@ func GetAllUserRoles() []UserRole {
|
||||
RoleManager,
|
||||
RoleCashier,
|
||||
RoleWaiter,
|
||||
RoleOwner,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,11 +7,11 @@ import (
|
||||
)
|
||||
|
||||
type PaymentMethodAnalyticsRequest struct {
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// PaymentMethodAnalyticsResponse represents the response for payment method analytics
|
||||
@@ -45,10 +45,10 @@ type PaymentMethodAnalyticsData struct {
|
||||
|
||||
type SalesAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
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"`
|
||||
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 SalesAnalyticsResponse struct {
|
||||
@@ -83,13 +83,78 @@ 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 *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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// ProductAnalyticsResponse represents the response for product analytics
|
||||
@@ -105,6 +170,7 @@ 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"`
|
||||
@@ -123,9 +189,9 @@ type ProductAnalyticsData struct {
|
||||
// ProductAnalyticsPerCategoryRequest represents the request for product analytics per category
|
||||
type ProductAnalyticsPerCategoryRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
OutletID *string `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
|
||||
@@ -152,9 +218,9 @@ type ProductAnalyticsPerCategoryData struct {
|
||||
// DashboardAnalyticsRequest represents the request for dashboard analytics
|
||||
type DashboardAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
OutletID *string `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
|
||||
@@ -179,28 +245,28 @@ type DashboardOverview struct {
|
||||
RefundedOrders int64 `json:"refunded_orders"`
|
||||
}
|
||||
|
||||
// ProfitLossAnalyticsRequest represents the request for profit and loss analytics
|
||||
type ProfitLossAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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"`
|
||||
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"`
|
||||
MainSummary []ProfitLossSummaryRow `json:"main_summary"`
|
||||
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
|
||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||
}
|
||||
|
||||
// ProfitLossSummary represents the summary of profit and loss analytics
|
||||
type ProfitLossSummary struct {
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
@@ -215,7 +281,6 @@ 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"`
|
||||
@@ -229,7 +294,6 @@ 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"`
|
||||
@@ -244,3 +308,19 @@ 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"`
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ type CreateCategoryRequest struct {
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
BusinessType *string `json:"business_type,omitempty"`
|
||||
Order *int `json:"order,omitempty"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
Order *int `json:"order,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
@@ -18,12 +19,14 @@ 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"`
|
||||
Order *int `json:"order,omitempty"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,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"`
|
||||
@@ -34,10 +37,11 @@ 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"`
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
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,4 +81,3 @@ 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 {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
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,6 +98,8 @@ 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"`
|
||||
@@ -108,6 +110,7 @@ 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,6 +8,7 @@ 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"`
|
||||
@@ -16,28 +17,30 @@ 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"` // 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
|
||||
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"`
|
||||
}
|
||||
|
||||
type UpdateProductRequest struct {
|
||||
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
|
||||
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"`
|
||||
}
|
||||
|
||||
type CreateProductVariantRequest struct {
|
||||
@@ -56,24 +59,27 @@ 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"`
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type ProductVariantResponse struct {
|
||||
@@ -89,6 +95,7 @@ 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"`
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
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"`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
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"`
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
type CreatePurchaseOrderRequest struct {
|
||||
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" validate:"required"` // Format: YYYY-MM-DD
|
||||
TransactionDate string `json:"transaction_date" validate:"required"` // 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"`
|
||||
@@ -19,18 +19,19 @@ type CreatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type CreatePurchaseOrderItemRequest struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
||||
Amount float64 `json:"amount" validate:"required,gte=0"`
|
||||
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"`
|
||||
}
|
||||
|
||||
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"`
|
||||
@@ -39,12 +40,13 @@ type UpdatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type UpdatePurchaseOrderItemRequest struct {
|
||||
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
||||
Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type PurchaseOrderResponse struct {
|
||||
@@ -53,7 +55,7 @@ type PurchaseOrderResponse struct {
|
||||
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"`
|
||||
@@ -66,17 +68,19 @@ type PurchaseOrderResponse struct {
|
||||
}
|
||||
|
||||
type PurchaseOrderItemResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Description *string `json:"description"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
||||
Unit *UnitResponse `json:"unit,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type PurchaseOrderAttachmentResponse struct {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -50,8 +48,10 @@ type SelfOrderMenuVariant struct {
|
||||
}
|
||||
|
||||
type SelfOrderCreateOrderRequest struct {
|
||||
SessionID string `json:"session_id" validate:"required"`
|
||||
OrderItems []SelfOrderCreateOrderItem `json:"order_items" validate:"required,min=1,dive"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type SelfOrderCreateOrderItem struct {
|
||||
@@ -62,7 +62,7 @@ type SelfOrderCreateOrderItem struct {
|
||||
}
|
||||
|
||||
type SelfOrderListCategoriesRequest struct {
|
||||
OrganizationID string `form:"organisasi_id" validate:"required"`
|
||||
OrganizationID string `form:"organization_id" validate:"required"`
|
||||
OutletID string `form:"outlet_id" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -78,35 +78,5 @@ type SelfOrderListCategoriesResponse struct {
|
||||
}
|
||||
|
||||
type SelfOrderListOrdersResponse struct {
|
||||
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"`
|
||||
Orders []OrderResponse `json:"orders"`
|
||||
}
|
||||
|
||||
@@ -35,9 +35,15 @@ type UpdateUserOutletRequest struct {
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
FcmToken *string `json:"fcm_token,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
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"`
|
||||
}
|
||||
+107
-40
@@ -27,10 +27,64 @@ 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"`
|
||||
@@ -68,54 +122,67 @@ type DashboardOverview struct {
|
||||
RefundedOrders int64 `json:"refunded_orders"`
|
||||
}
|
||||
|
||||
// ProfitLossAnalytics represents profit and loss analytics data
|
||||
type ProfitLossAnalytics struct {
|
||||
Summary ProfitLossSummary `json:"summary"`
|
||||
Data []ProfitLossData `json:"data"`
|
||||
ProductData []ProductProfitData `json:"product_data"`
|
||||
Summary ProfitLossSummary
|
||||
Data []ProfitLossData
|
||||
ProductData []ProductProfitData
|
||||
TodayRevenue float64
|
||||
TodayCost float64
|
||||
MtdRevenue float64
|
||||
MtdCost float64
|
||||
TodayExpenseByCategory []ExpenseCategoryTotal
|
||||
MtdExpenseByCategory []ExpenseCategoryTotal
|
||||
OperationalExpenseItems []OperationalExpenseItem
|
||||
}
|
||||
|
||||
// ProfitLossSummary represents profit and loss summary data
|
||||
type ProfitLossSummary struct {
|
||||
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"`
|
||||
TotalRevenue float64
|
||||
TotalCost float64
|
||||
GrossProfit float64
|
||||
GrossProfitMargin float64
|
||||
TotalTax float64
|
||||
TotalDiscount float64
|
||||
NetProfit float64
|
||||
NetProfitMargin float64
|
||||
TotalOrders int64
|
||||
AverageProfit float64
|
||||
ProfitabilityRatio float64
|
||||
}
|
||||
|
||||
// ProfitLossData represents profit and loss data by time period
|
||||
type ProfitLossData struct {
|
||||
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"`
|
||||
Date time.Time
|
||||
Revenue float64
|
||||
Cost float64
|
||||
GrossProfit float64
|
||||
GrossProfitMargin float64
|
||||
Tax float64
|
||||
Discount float64
|
||||
NetProfit float64
|
||||
NetProfitMargin float64
|
||||
Orders int64
|
||||
}
|
||||
|
||||
// ProductProfitData represents profit data for individual products
|
||||
type ProductProfitData struct {
|
||||
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"`
|
||||
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
|
||||
}
|
||||
|
||||
@@ -31,15 +31,16 @@ 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"`
|
||||
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"`
|
||||
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"`
|
||||
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Products []Product `gorm:"foreignKey:CategoryID" json:"products,omitempty"`
|
||||
|
||||
@@ -36,6 +36,13 @@ func GetAllEntities() []interface{} {
|
||||
&CampaignRule{},
|
||||
&OtpSession{},
|
||||
// Analytics entities are not database tables, they are query results
|
||||
&UserDevice{},
|
||||
// Notification entities
|
||||
&Notification{},
|
||||
&NotificationReceiver{},
|
||||
&NotificationDelivery{},
|
||||
&ProductOutletPrice{},
|
||||
&Expense{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
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"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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,4 +39,3 @@ func (iuc *IngredientUnitConverter) BeforeCreate() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -36,34 +36,36 @@ 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"`
|
||||
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"`
|
||||
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"`
|
||||
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (im *InventoryMovement) BeforeCreate(tx *gorm.DB) error {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
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,13 +26,14 @@ 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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
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"`
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
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,18 +9,18 @@ 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"`
|
||||
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"`
|
||||
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" 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"`
|
||||
|
||||
Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Vendor *Vendor `gorm:"foreignKey:VendorID" json:"vendor,omitempty"`
|
||||
@@ -41,19 +41,21 @@ 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;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"`
|
||||
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"`
|
||||
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (poi *PurchaseOrderItem) BeforeCreate(tx *gorm.DB) error {
|
||||
|
||||
@@ -49,7 +49,6 @@ type User struct {
|
||||
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"`
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
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,6 +8,7 @@ import (
|
||||
"apskel-pos-be/internal/util"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AnalyticsHandler struct {
|
||||
@@ -25,6 +26,17 @@ 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)
|
||||
@@ -36,7 +48,7 @@ func (h *AnalyticsHandler) GetPaymentMethodAnalytics(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq := transformer.PaymentMethodAnalyticsContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetPaymentMethodAnalytics(ctx, modelReq)
|
||||
@@ -60,7 +72,7 @@ func (h *AnalyticsHandler) GetSalesAnalytics(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq := transformer.SalesAnalyticsContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetSalesAnalytics(ctx, modelReq)
|
||||
@@ -73,6 +85,30 @@ 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)
|
||||
@@ -84,7 +120,7 @@ func (h *AnalyticsHandler) GetProductAnalytics(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq := transformer.ProductAnalyticsContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetProductAnalytics(ctx, modelReq)
|
||||
@@ -108,7 +144,7 @@ func (h *AnalyticsHandler) GetProductAnalyticsPerCategory(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq := transformer.ProductAnalyticsPerCategoryContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetProductAnalyticsPerCategory(ctx, modelReq)
|
||||
@@ -132,7 +168,7 @@ func (h *AnalyticsHandler) GetDashboardAnalytics(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq := transformer.DashboardAnalyticsContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetDashboardAnalytics(ctx, modelReq)
|
||||
@@ -156,6 +192,7 @@ 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")
|
||||
|
||||
@@ -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,6 +44,11 @@ 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())
|
||||
@@ -149,6 +154,11 @@ 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 {
|
||||
@@ -176,6 +186,11 @@ 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,6 +1,8 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/logger"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
@@ -47,7 +49,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)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
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,4 +275,3 @@ func (h *IngredientUnitConverterHandler) GetUnitsByIngredientID(c *gin.Context)
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, unitsResponse, "IngredientUnitConverterHandler::GetUnitsByIngredientID")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
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,6 +137,10 @@ 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,6 +60,7 @@ 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)
|
||||
@@ -85,7 +86,7 @@ func (h *ProductHandler) UpdateProduct(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
productResponse := h.productService.UpdateProduct(ctx, productID, &req)
|
||||
productResponse := h.productService.UpdateProduct(ctx, contextInfo, productID, &req)
|
||||
if productResponse.HasErrors() {
|
||||
errorResp := productResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::UpdateProduct -> Failed to update product from service")
|
||||
@@ -117,6 +118,7 @@ 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)
|
||||
@@ -127,7 +129,7 @@ func (h *ProductHandler) GetProduct(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
productResponse := h.productService.GetProductByID(ctx, productID)
|
||||
productResponse := h.productService.GetProductByID(ctx, productID, contextInfo.OutletID)
|
||||
if productResponse.HasErrors() {
|
||||
errorResp := productResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::GetProduct -> Failed to get product from service")
|
||||
@@ -184,6 +186,97 @@ 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
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
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,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ReportHandler struct {
|
||||
@@ -19,11 +20,26 @@ 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 := c.Param("outlet_id")
|
||||
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 {
|
||||
|
||||
@@ -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,22 +15,21 @@ 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
|
||||
fcmClient client.FcmClient
|
||||
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
|
||||
}
|
||||
|
||||
func NewSelfOrderHandler(
|
||||
@@ -42,18 +41,18 @@ func NewSelfOrderHandler(
|
||||
userRepo processor.UserRepository,
|
||||
sessionRepo repository.SessionRepository,
|
||||
orderRepo repository.OrderRepository,
|
||||
fcmClient client.FcmClient,
|
||||
productOutletPriceService service.ProductOutletPriceService,
|
||||
) *SelfOrderHandler {
|
||||
return &SelfOrderHandler{
|
||||
orderService: orderService,
|
||||
categoryService: categoryService,
|
||||
productService: productService,
|
||||
tableRepo: tableRepo,
|
||||
outletRepo: outletRepo,
|
||||
userRepo: userRepo,
|
||||
sessionRepo: sessionRepo,
|
||||
orderRepo: orderRepo,
|
||||
fcmClient: fcmClient,
|
||||
orderService: orderService,
|
||||
categoryService: categoryService,
|
||||
productService: productService,
|
||||
tableRepo: tableRepo,
|
||||
outletRepo: outletRepo,
|
||||
userRepo: userRepo,
|
||||
sessionRepo: sessionRepo,
|
||||
orderRepo: orderRepo,
|
||||
productOutletPriceService: productOutletPriceService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,16 +219,29 @@ func (h *SelfOrderHandler) GetMenu(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
menu := h.buildMenuResponse(outlet, table, catList.Categories, prodList.Products)
|
||||
menu := h.buildMenuResponse(ctx, 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)
|
||||
@@ -240,11 +252,15 @@ 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: p.Price,
|
||||
Price: price,
|
||||
ImageURL: p.ImageURL,
|
||||
}
|
||||
for _, v := range p.Variants {
|
||||
@@ -335,6 +351,7 @@ 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{
|
||||
@@ -342,7 +359,7 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
|
||||
UserID: userID,
|
||||
TableID: &tableID,
|
||||
TableNumber: &table.TableName,
|
||||
OrderType: constants.OrderTypeDineIn,
|
||||
OrderType: constants.OrderType(req.OrderType),
|
||||
OrderItems: orderItems,
|
||||
Metadata: metadata,
|
||||
}
|
||||
@@ -356,41 +373,13 @@ 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("sessionId")
|
||||
sessionID := c.Param("session_id")
|
||||
|
||||
if sessionID == "" {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
@@ -423,47 +412,15 @@ func (h *SelfOrderHandler) GetOrdersBySession(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
modelOrders := mappers.OrderEntitiesToResponses(orders)
|
||||
contractOrders := make([]contract.OrderResponse, len(modelOrders))
|
||||
for i := range modelOrders {
|
||||
contractOrders[i] = *transformer.OrderModelToContract(&modelOrders[i])
|
||||
}
|
||||
|
||||
resp := &contract.SelfOrderListOrdersResponse{
|
||||
Orders: contractOrders,
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "SelfOrderHandler::GetOrdersBySession")
|
||||
}
|
||||
|
||||
@@ -472,6 +429,7 @@ 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 {
|
||||
@@ -499,7 +457,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, "organisasi_id is required"),
|
||||
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "organization_id is required"),
|
||||
}), "SelfOrderHandler::ListCategories")
|
||||
return
|
||||
}
|
||||
@@ -514,7 +472,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 organisasi_id format"),
|
||||
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "invalid organization_id format"),
|
||||
}), "SelfOrderHandler::ListCategories")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -19,14 +19,14 @@ import (
|
||||
type TableHandler struct {
|
||||
tableService TableService
|
||||
tableValidator *validator.TableValidator
|
||||
baseURL string
|
||||
selfOrderURL string
|
||||
}
|
||||
|
||||
func NewTableHandler(tableService TableService, tableValidator *validator.TableValidator, baseURL string) *TableHandler {
|
||||
func NewTableHandler(tableService TableService, tableValidator *validator.TableValidator, selfOrderURL string) *TableHandler {
|
||||
return &TableHandler{
|
||||
tableService: tableService,
|
||||
tableValidator: tableValidator,
|
||||
baseURL: baseURL,
|
||||
selfOrderURL: selfOrderURL,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,11 @@ 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
|
||||
@@ -312,7 +317,7 @@ func (h *TableHandler) GenerateQRCode(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
selfOrderURL := fmt.Sprintf("%s/api/v1/self-order/table/%s", h.baseURL, token)
|
||||
selfOrderURLResult := fmt.Sprintf("%s/menu?token=%s", h.selfOrderURL, token)
|
||||
|
||||
size := 256
|
||||
if sizeStr := c.Query("size"); sizeStr != "" {
|
||||
@@ -321,7 +326,7 @@ func (h *TableHandler) GenerateQRCode(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
pngBytes, err := qrcode.GeneratePNG(selfOrderURL, size)
|
||||
pngBytes, err := qrcode.GeneratePNG(selfOrderURLResult, 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")
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
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,11 +13,12 @@ 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, // 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
|
||||
ImageURL: nil,
|
||||
Order: entity.Order,
|
||||
IsActive: true,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
@@ -32,14 +33,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", // Default business type
|
||||
BusinessType: "restaurant",
|
||||
Order: model.Order,
|
||||
Metadata: metadata,
|
||||
CreatedAt: model.CreatedAt,
|
||||
@@ -56,14 +57,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", // Default business type
|
||||
BusinessType: "restaurant",
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
@@ -87,11 +88,12 @@ 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, // Default to true since entity doesn't have this field
|
||||
Order: entity.Order,
|
||||
IsActive: true,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
@@ -121,6 +123,10 @@ 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 {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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)
|
||||
resp := OrderItemEntityToResponse(&item, order.OutletID)
|
||||
if resp != nil {
|
||||
resp.PaidQuantity = paidQtyByOrderItem[item.ID]
|
||||
response.OrderItems[i] = *resp
|
||||
@@ -101,11 +101,20 @@ func OrderEntityToResponse(order *entities.Order) *models.OrderResponse {
|
||||
return response
|
||||
}
|
||||
|
||||
func OrderItemEntityToResponse(item *entities.OrderItem) *models.OrderItemResponse {
|
||||
func OrderItemEntityToResponse(item *entities.OrderItem, outletID uuid.UUID) *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,
|
||||
@@ -130,10 +139,19 @@ func OrderItemEntityToResponse(item *entities.OrderItem) *models.OrderItemRespon
|
||||
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 {
|
||||
@@ -316,14 +334,14 @@ func OrderEntitiesToResponses(orders []*entities.Order) []models.OrderResponse {
|
||||
return responses
|
||||
}
|
||||
|
||||
func OrderItemEntitiesToResponses(items []*entities.OrderItem) []models.OrderItemResponse {
|
||||
func OrderItemEntitiesToResponses(items []*entities.OrderItem, outletID uuid.UUID) []models.OrderItemResponse {
|
||||
if items == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
responses := make([]models.OrderItemResponse, len(items))
|
||||
for i, item := range items {
|
||||
response := OrderItemEntityToResponse(item)
|
||||
response := OrderItemEntityToResponse(item, outletID)
|
||||
if response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestOrderItemEntityToResponse_WithProductNames(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
result := OrderItemEntityToResponse(orderItem)
|
||||
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
|
||||
|
||||
// Assert
|
||||
assert.NotNil(t, result)
|
||||
@@ -89,7 +89,7 @@ func TestOrderItemEntityToResponse_WithoutProductVariant(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
result := OrderItemEntityToResponse(orderItem)
|
||||
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
|
||||
|
||||
// Assert
|
||||
assert.NotNil(t, result)
|
||||
@@ -129,7 +129,7 @@ func TestOrderItemEntityToResponse_WithoutProductPreload(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
result := OrderItemEntityToResponse(orderItem)
|
||||
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
|
||||
|
||||
// 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,6 +135,7 @@ 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,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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
|
||||
}
|
||||
@@ -91,15 +91,16 @@ func PurchaseOrderItemEntityToModel(entity *entities.PurchaseOrderItem) *models.
|
||||
}
|
||||
|
||||
return &models.PurchaseOrderItem{
|
||||
ID: entity.ID,
|
||||
PurchaseOrderID: entity.PurchaseOrderID,
|
||||
IngredientID: entity.IngredientID,
|
||||
Description: entity.Description,
|
||||
Quantity: entity.Quantity,
|
||||
UnitID: entity.UnitID,
|
||||
Amount: entity.Amount,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,15 +110,16 @@ func PurchaseOrderItemModelToEntity(model *models.PurchaseOrderItem) *entities.P
|
||||
}
|
||||
|
||||
return &entities.PurchaseOrderItem{
|
||||
ID: model.ID,
|
||||
PurchaseOrderID: model.PurchaseOrderID,
|
||||
IngredientID: model.IngredientID,
|
||||
Description: model.Description,
|
||||
Quantity: model.Quantity,
|
||||
UnitID: model.UnitID,
|
||||
Amount: model.Amount,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,15 +129,16 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
|
||||
}
|
||||
|
||||
response := &models.PurchaseOrderItemResponse{
|
||||
ID: entity.ID,
|
||||
PurchaseOrderID: entity.PurchaseOrderID,
|
||||
IngredientID: entity.IngredientID,
|
||||
Description: entity.Description,
|
||||
Quantity: entity.Quantity,
|
||||
UnitID: entity.UnitID,
|
||||
Amount: entity.Amount,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
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,
|
||||
}
|
||||
|
||||
// Map ingredient if present
|
||||
@@ -146,6 +149,10 @@ 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{
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
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,6 +11,7 @@ import (
|
||||
"apskel-pos-be/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AuthMiddleware struct {
|
||||
@@ -45,9 +46,13 @@ func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
|
||||
setKeyInContext(c, appcontext.OrganizationIDKey, userResponse.OrganizationID.String())
|
||||
setKeyInContext(c, appcontext.UserIDKey, userResponse.ID.String())
|
||||
|
||||
if userResponse.Role != "superadmin" {
|
||||
setKeyInContext(c, appcontext.OutletIDKey, userResponse.OutletID.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()
|
||||
}
|
||||
setKeyInContext(c, appcontext.OutletIDKey, outletIDStr)
|
||||
|
||||
logger.FromContext(c.Request.Context()).Infof("AuthMiddleware::RequireAuth -> User authenticated: %s", userResponse.Email)
|
||||
c.Next()
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
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 {
|
||||
|
||||
@@ -87,6 +87,77 @@ 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"`
|
||||
@@ -109,6 +180,7 @@ 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"`
|
||||
@@ -183,7 +255,6 @@ type DashboardOverview struct {
|
||||
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"`
|
||||
@@ -192,19 +263,20 @@ 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"`
|
||||
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"`
|
||||
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"`
|
||||
MainSummary []ProfitLossSummaryRow `json:"main_summary"`
|
||||
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
|
||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||
}
|
||||
|
||||
// ProfitLossSummary represents the summary of profit and loss analytics
|
||||
type ProfitLossSummary struct {
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
@@ -219,7 +291,6 @@ 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"`
|
||||
@@ -233,7 +304,6 @@ 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"`
|
||||
@@ -248,3 +318,19 @@ 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"`
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@ 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
|
||||
@@ -20,27 +21,30 @@ type Category struct {
|
||||
|
||||
type CreateCategoryRequest struct {
|
||||
OrganizationID uuid.UUID `validate:"required"`
|
||||
Name string `validate:"required,min=1,max=255"`
|
||||
Description *string `validate:"omitempty,max=1000"`
|
||||
ImageURL *string `validate:"omitempty,url"`
|
||||
Order int `validate:"min=0"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type UpdateCategoryRequest struct {
|
||||
Name *string `validate:"omitempty,min=1,max=255"`
|
||||
Description *string `validate:"omitempty,max=1000"`
|
||||
ImageURL *string `validate:"omitempty,url"`
|
||||
Order *int `validate:"omitempty,min=0"`
|
||||
OutletID *uuid.UUID
|
||||
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
|
||||
|
||||
+11
-11
@@ -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
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
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,4 +101,3 @@ type IngredientUnitsResponse struct {
|
||||
BaseUnitName string `json:"base_unit_name"`
|
||||
Units []*UnitResponse `json:"units"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
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,6 +188,8 @@ type OrderItemResponse struct {
|
||||
ProductName string
|
||||
ProductVariantID *uuid.UUID
|
||||
ProductVariantName *string
|
||||
CategoryID *uuid.UUID
|
||||
CategoryName *string
|
||||
Quantity int
|
||||
UnitPrice float64
|
||||
TotalPrice float64
|
||||
@@ -207,6 +209,7 @@ 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,6 +40,7 @@ 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"`
|
||||
@@ -49,6 +50,7 @@ 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{}
|
||||
@@ -60,6 +62,7 @@ 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"`
|
||||
@@ -68,6 +71,7 @@ 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{}
|
||||
@@ -100,10 +104,13 @@ 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{}
|
||||
@@ -113,6 +120,13 @@ 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"`
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
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"`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
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,30 +7,31 @@ import (
|
||||
)
|
||||
|
||||
type PurchaseOrder struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
VendorID uuid.UUID `json:"vendor_id"`
|
||||
PONumber string `json:"po_number"`
|
||||
TransactionDate time.Time `json:"transaction_date"`
|
||||
DueDate time.Time `json:"due_date"`
|
||||
Reference *string `json:"reference"`
|
||||
Status string `json:"status"`
|
||||
Message *string `json:"message"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
VendorID uuid.UUID `json:"vendor_id"`
|
||||
PONumber string `json:"po_number"`
|
||||
TransactionDate time.Time `json:"transaction_date"`
|
||||
DueDate *time.Time `json:"due_date"`
|
||||
Reference *string `json:"reference"`
|
||||
Status string `json:"status"`
|
||||
Message *string `json:"message"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type PurchaseOrderItem struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Description *string `json:"description"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type PurchaseOrderAttachment struct {
|
||||
@@ -46,7 +47,7 @@ type PurchaseOrderResponse struct {
|
||||
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"`
|
||||
@@ -59,17 +60,19 @@ type PurchaseOrderResponse struct {
|
||||
}
|
||||
|
||||
type PurchaseOrderItemResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Description *string `json:"description"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
||||
Unit *UnitResponse `json:"unit,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type PurchaseOrderAttachmentResponse struct {
|
||||
@@ -84,7 +87,7 @@ type CreatePurchaseOrderRequest struct {
|
||||
VendorID uuid.UUID `json:"vendor_id"`
|
||||
PONumber string `json:"po_number"`
|
||||
TransactionDate time.Time `json:"transaction_date"`
|
||||
DueDate time.Time `json:"due_date"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
Reference *string `json:"reference,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
Message *string `json:"message,omitempty"`
|
||||
@@ -93,11 +96,12 @@ type CreatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type CreatePurchaseOrderItemRequest struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type UpdatePurchaseOrderRequest struct {
|
||||
@@ -113,12 +117,13 @@ type UpdatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type UpdatePurchaseOrderItemRequest struct {
|
||||
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Quantity *float64 `json:"quantity,omitempty"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||
Amount *float64 `json:"amount,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type ListPurchaseOrdersRequest struct {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
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,6 +3,7 @@ package processor
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/models"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
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)
|
||||
@@ -20,11 +22,13 @@ type AnalyticsProcessor interface {
|
||||
|
||||
type AnalyticsProcessorImpl struct {
|
||||
analyticsRepo repository.AnalyticsRepository
|
||||
expenseRepo ExpenseRepository
|
||||
}
|
||||
|
||||
func NewAnalyticsProcessorImpl(analyticsRepo repository.AnalyticsRepository) *AnalyticsProcessorImpl {
|
||||
func NewAnalyticsProcessorImpl(analyticsRepo repository.AnalyticsRepository, expenseRepo ExpenseRepository) *AnalyticsProcessorImpl {
|
||||
return &AnalyticsProcessorImpl{
|
||||
analyticsRepo: analyticsRepo,
|
||||
expenseRepo: expenseRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +168,85 @@ 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) {
|
||||
@@ -188,6 +271,7 @@ 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,
|
||||
@@ -322,17 +406,27 @@ 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")
|
||||
}
|
||||
|
||||
// Get analytics data from repository
|
||||
if req.GroupBy == "" {
|
||||
req.GroupBy = "day"
|
||||
}
|
||||
|
||||
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{
|
||||
@@ -367,6 +461,145 @@ 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
|
||||
}
|
||||
|
||||
return &models.ProfitLossAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
@@ -386,7 +619,35 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
||||
AverageProfit: result.Summary.AverageProfit,
|
||||
ProfitabilityRatio: result.Summary.ProfitabilityRatio,
|
||||
},
|
||||
Data: data,
|
||||
ProductData: productData,
|
||||
Data: data,
|
||||
ProductData: productData,
|
||||
MainSummary: mainSummary,
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
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
|
||||
profitLossGroup string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
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,7 +1,6 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/constants"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -87,7 +86,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) 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, purchaseOrderItemID *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
|
||||
}
|
||||
|
||||
@@ -108,6 +107,7 @@ type OrderProcessorImpl struct {
|
||||
productRecipeRepo *repository.ProductRecipeRepository
|
||||
ingredientRepo IngredientRepository
|
||||
inventoryMovementService InventoryMovementService
|
||||
productOutletPriceRepo repository.ProductOutletPriceRepository
|
||||
}
|
||||
|
||||
func NewOrderProcessorImpl(
|
||||
@@ -126,6 +126,7 @@ func NewOrderProcessorImpl(
|
||||
productRecipeRepo *repository.ProductRecipeRepository,
|
||||
ingredientRepo IngredientRepository,
|
||||
inventoryMovementService InventoryMovementService,
|
||||
productOutletPriceRepo repository.ProductOutletPriceRepository,
|
||||
) *OrderProcessorImpl {
|
||||
return &OrderProcessorImpl{
|
||||
orderRepo: orderRepo,
|
||||
@@ -144,6 +145,7 @@ func NewOrderProcessorImpl(
|
||||
productRecipeRepo: productRecipeRepo,
|
||||
ingredientRepo: ingredientRepo,
|
||||
inventoryMovementService: inventoryMovementService,
|
||||
productOutletPriceRepo: productOutletPriceRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +172,12 @@ 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 {
|
||||
@@ -293,6 +301,12 @@ 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)
|
||||
@@ -324,7 +338,7 @@ func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID,
|
||||
ProductID: itemReq.ProductID,
|
||||
ProductVariantID: itemReq.ProductVariantID,
|
||||
Quantity: itemReq.Quantity,
|
||||
UnitPrice: unitPrice, // Use price from database
|
||||
UnitPrice: unitPrice,
|
||||
TotalPrice: itemTotalPrice,
|
||||
UnitCost: unitCost,
|
||||
TotalCost: itemTotalCost,
|
||||
@@ -373,31 +387,10 @@ func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID,
|
||||
return nil, fmt.Errorf("failed to create order item: %w", err)
|
||||
}
|
||||
|
||||
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,
|
||||
itemResponse := mappers.OrderItemEntityToResponse(orderItem, order.OutletID)
|
||||
if itemResponse != nil {
|
||||
addedItemResponses = append(addedItemResponses, *itemResponse)
|
||||
}
|
||||
addedItemResponses = append(addedItemResponses, itemResponse)
|
||||
}
|
||||
|
||||
orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, orderID)
|
||||
@@ -601,6 +594,10 @@ 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)
|
||||
}
|
||||
@@ -621,9 +618,15 @@ 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 // Recalculate tax using outlet's tax rate
|
||||
order.TaxAmount = order.Subtotal * outlet.TaxRate
|
||||
order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount
|
||||
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ProductOutletPriceProcessor interface {
|
||||
Upsert(ctx context.Context, req *models.CreateProductOutletPriceRequest) (*models.ProductOutletPrice, error)
|
||||
GetByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID) (*models.ProductOutletPrice, error)
|
||||
GetByProduct(ctx context.Context, productID uuid.UUID) ([]*models.ProductOutletPrice, error)
|
||||
GetByOutlet(ctx context.Context, outletID uuid.UUID) ([]*models.ProductOutletPrice, error)
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
ResolvePrice(ctx context.Context, productID, outletID uuid.UUID, fallbackPrice float64) float64
|
||||
BulkUpsert(ctx context.Context, productID uuid.UUID, prices []models.CreateProductOutletPriceRequest) ([]*models.ProductOutletPrice, error)
|
||||
}
|
||||
|
||||
type ProductOutletPriceProcessorImpl struct {
|
||||
repo repository.ProductOutletPriceRepository
|
||||
productRepo ProductRepository
|
||||
outletRepo OutletRepository
|
||||
}
|
||||
|
||||
func NewProductOutletPriceProcessorImpl(repo repository.ProductOutletPriceRepository, productRepo ProductRepository, outletRepo OutletRepository) *ProductOutletPriceProcessorImpl {
|
||||
return &ProductOutletPriceProcessorImpl{
|
||||
repo: repo,
|
||||
productRepo: productRepo,
|
||||
outletRepo: outletRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProductOutletPriceProcessorImpl) Upsert(ctx context.Context, req *models.CreateProductOutletPriceRequest) (*models.ProductOutletPrice, error) {
|
||||
if _, err := p.productRepo.GetByID(ctx, req.ProductID); err != nil {
|
||||
return nil, fmt.Errorf("product not found: %w", err)
|
||||
}
|
||||
|
||||
if _, err := p.outletRepo.GetByID(ctx, req.OutletID); err != nil {
|
||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
entity := &entities.ProductOutletPrice{
|
||||
ProductID: req.ProductID,
|
||||
OutletID: req.OutletID,
|
||||
Price: req.Price,
|
||||
PrintToChecker: req.PrintToChecker,
|
||||
}
|
||||
|
||||
if err := p.repo.Upsert(ctx, entity); err != nil {
|
||||
return nil, fmt.Errorf("failed to upsert product outlet price: %w", err)
|
||||
}
|
||||
|
||||
actual, err := p.repo.GetByProductAndOutlet(ctx, req.ProductID, req.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve upserted product outlet price: %w", err)
|
||||
}
|
||||
|
||||
return mappers.ProductOutletPriceEntityToModel(actual), nil
|
||||
}
|
||||
|
||||
func (p *ProductOutletPriceProcessorImpl) GetByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID) (*models.ProductOutletPrice, error) {
|
||||
entity, err := p.repo.GetByProductAndOutlet(ctx, productID, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("product outlet price not found: %w", err)
|
||||
}
|
||||
|
||||
return mappers.ProductOutletPriceEntityToModel(entity), nil
|
||||
}
|
||||
|
||||
func (p *ProductOutletPriceProcessorImpl) GetByProduct(ctx context.Context, productID uuid.UUID) ([]*models.ProductOutletPrice, error) {
|
||||
entities, err := p.repo.GetByProduct(ctx, productID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get product outlet prices: %w", err)
|
||||
}
|
||||
|
||||
return mappers.ProductOutletPriceEntitiesToModels(entities), nil
|
||||
}
|
||||
|
||||
func (p *ProductOutletPriceProcessorImpl) GetByOutlet(ctx context.Context, outletID uuid.UUID) ([]*models.ProductOutletPrice, error) {
|
||||
entities, err := p.repo.GetByOutlet(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get outlet prices: %w", err)
|
||||
}
|
||||
|
||||
return mappers.ProductOutletPriceEntitiesToModels(entities), nil
|
||||
}
|
||||
|
||||
func (p *ProductOutletPriceProcessorImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
if err := p.repo.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("failed to delete product outlet price: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProductOutletPriceProcessorImpl) ResolvePrice(ctx context.Context, productID, outletID uuid.UUID, fallbackPrice float64) float64 {
|
||||
outletPrice, err := p.repo.GetByProductAndOutlet(ctx, productID, outletID)
|
||||
if err != nil {
|
||||
return fallbackPrice
|
||||
}
|
||||
return outletPrice.Price
|
||||
}
|
||||
|
||||
func (p *ProductOutletPriceProcessorImpl) BulkUpsert(ctx context.Context, productID uuid.UUID, prices []models.CreateProductOutletPriceRequest) ([]*models.ProductOutletPrice, error) {
|
||||
var results []*models.ProductOutletPrice
|
||||
|
||||
for _, req := range prices {
|
||||
req.ProductID = productID
|
||||
result, err := p.Upsert(ctx, &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to upsert price for outlet %s: %w", req.OutletID, err)
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user