Adds the expiry job (docs/prd-point-coin.md F12, PC-503).
Every 15 minutes it lists the lots whose expiry has passed and that still
hold something, the longest overdue first, 500 at a time, and expires each
in its own transaction through WalletProcessor.ExpireLot: lock the wallet,
read the lot again, and take what is left with an EXPIRE row pointing at
the lot, keyed expire:{lot_id}. The description is frozen as
"Kedaluwarsa: 130 EnakPoint dari Belanja #ORD-0098", using the amount read
under the lock. Lots expire at the end of their day, so none stays past it
for more than about a quarter of an hour.
It is safe on several instances and across restarts, keeping no state in
memory as OmsetMilestoneScheduler does. Selecting the lots FOR UPDATE SKIP
LOCKED, as PC-503 suggested, would lock a lot before its wallet and
deadlock against payments, which lock the wallet first; instead the
listing takes no lock, and the wallet lock plus the idempotency key make a
second instance find the lot empty or the key used and take nothing.
A lot that fails is logged and retried on the next run without stopping
the others. Each customer gets one FCM push per currency with the total
that expired ("180 EnakPoint kamu sudah kedaluwarsa.", type
WALLET_EXPIRED).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
740 lines
43 KiB
Go
740 lines
43 KiB
Go
package app
|
|
|
|
import (
|
|
"apskel-pos-be/internal/client"
|
|
"apskel-pos-be/internal/transformer"
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"apskel-pos-be/config"
|
|
"apskel-pos-be/internal/handler"
|
|
"apskel-pos-be/internal/middleware"
|
|
"apskel-pos-be/internal/processor"
|
|
"apskel-pos-be/internal/repository"
|
|
"apskel-pos-be/internal/router"
|
|
"apskel-pos-be/internal/service"
|
|
"apskel-pos-be/internal/validator"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type App struct {
|
|
server *http.Server
|
|
db *gorm.DB
|
|
redisClient *redis.Client
|
|
router *router.Router
|
|
shutdown chan os.Signal
|
|
omsetScheduler *service.OmsetMilestoneScheduler
|
|
walletRecon *service.WalletReconciliationJob
|
|
earningRetry *service.EarningBackfillJob
|
|
walletExpiry *service.WalletExpiryJob
|
|
}
|
|
|
|
func NewApp(db *gorm.DB, redisClient *redis.Client) *App {
|
|
return &App{
|
|
db: db,
|
|
redisClient: redisClient,
|
|
shutdown: make(chan os.Signal, 1),
|
|
}
|
|
}
|
|
|
|
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,
|
|
)
|
|
|
|
// Checks that wallet balances, ledger and lots agree (docs/prd-point-coin.md ยง7.5)
|
|
a.walletRecon = service.NewWalletReconciliationJob(
|
|
repository.NewWalletReconciliationRepository(a.db),
|
|
repos.userRepo,
|
|
processors.notificationProcessor,
|
|
)
|
|
// Earns for paid orders whose earning failed at payment time (docs/prd-point-coin.md F3)
|
|
a.earningRetry = service.NewEarningBackfillJob(processors.earningProcessor)
|
|
// Expires balances whose time is up (docs/prd-point-coin.md F12)
|
|
a.walletExpiry = service.NewWalletExpiryJob(processor.NewWalletExpiryProcessor(
|
|
repository.NewWalletExpiryRepository(a.db), processor.NewWalletProcessor(repos.walletRepo), repos.txManager, processors.customerDeviceProcessor))
|
|
|
|
services := a.initServices(processors, repos, cfg)
|
|
validators := a.initValidators()
|
|
middleware := a.initMiddleware(services, cfg)
|
|
healthHandler := handler.NewHealthHandler()
|
|
selfOrderHandler := handler.NewSelfOrderHandler(
|
|
services.orderService,
|
|
services.categoryService,
|
|
services.productService,
|
|
repos.tableRepo,
|
|
repos.outletRepo,
|
|
repos.userRepo,
|
|
repos.sessionRepo,
|
|
repos.orderRepo,
|
|
services.productOutletPriceService,
|
|
)
|
|
|
|
a.router = router.NewRouter(
|
|
cfg,
|
|
healthHandler,
|
|
services.authService,
|
|
middleware.authMiddleware,
|
|
services.userService,
|
|
validators.userValidator,
|
|
services.organizationService,
|
|
validators.organizationValidator,
|
|
services.outletService,
|
|
validators.outletValidator,
|
|
services.outletSettingService,
|
|
services.categoryService,
|
|
validators.categoryValidator,
|
|
services.productService,
|
|
validators.productValidator,
|
|
services.productVariantService,
|
|
validators.productVariantValidator,
|
|
services.inventoryService,
|
|
validators.inventoryValidator,
|
|
services.orderService,
|
|
validators.orderValidator,
|
|
services.fileService,
|
|
validators.fileValidator,
|
|
services.customerService,
|
|
validators.customerValidator,
|
|
services.paymentMethodService,
|
|
validators.paymentMethodValidator,
|
|
services.analyticsService,
|
|
services.reportService,
|
|
services.tableService,
|
|
validators.tableValidator,
|
|
services.unitService,
|
|
services.ingredientService,
|
|
services.productRecipeService,
|
|
services.vendorService,
|
|
validators.vendorValidator,
|
|
services.purchaseOrderService,
|
|
validators.purchaseOrderValidator,
|
|
services.purchaseCategoryService,
|
|
validators.purchaseCategoryValidator,
|
|
services.unitConverterService,
|
|
validators.unitConverterValidator,
|
|
services.chartOfAccountTypeService,
|
|
validators.chartOfAccountTypeValidator,
|
|
services.chartOfAccountService,
|
|
validators.chartOfAccountValidator,
|
|
services.accountService,
|
|
validators.accountValidator,
|
|
*services.orderIngredientTransactionService,
|
|
validators.orderIngredientTransactionValidator,
|
|
services.gamificationService,
|
|
validators.gamificationValidator,
|
|
services.rewardService,
|
|
validators.rewardValidator,
|
|
services.campaignService,
|
|
validators.campaignValidator,
|
|
services.customerAuthService,
|
|
validators.customerAuthValidator,
|
|
services.customerPointsService,
|
|
services.spinGameService,
|
|
middleware.customerAuthMiddleware,
|
|
services.userDeviceService,
|
|
validators.userDeviceValidator,
|
|
services.notificationService,
|
|
validators.notificationValidator,
|
|
services.productOutletPriceService,
|
|
validators.productOutletPriceValidator,
|
|
selfOrderHandler,
|
|
services.expenseService,
|
|
validators.expenseValidator,
|
|
services.cashAdvanceService,
|
|
validators.cashAdvanceValidator,
|
|
services.walletAdminService,
|
|
validators.walletValidator,
|
|
services.loyaltySettingsService,
|
|
services.customerPinService,
|
|
services.pointPaymentService,
|
|
services.customerOrderPaymentService,
|
|
services.customerWalletService,
|
|
services.customerDeviceService,
|
|
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)
|
|
}
|
|
if a.walletRecon != nil {
|
|
a.walletRecon.Start(6 * time.Hour)
|
|
}
|
|
if a.earningRetry != nil {
|
|
a.earningRetry.Start(30 * time.Minute)
|
|
}
|
|
if a.walletExpiry != nil {
|
|
a.walletExpiry.Start(15 * time.Minute)
|
|
}
|
|
|
|
engine := a.router.Init()
|
|
|
|
a.server = &http.Server{
|
|
Addr: ":" + port,
|
|
Handler: engine,
|
|
ReadTimeout: 15 * time.Second,
|
|
WriteTimeout: 15 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
|
|
signal.Notify(a.shutdown, os.Interrupt, syscall.SIGTERM)
|
|
|
|
go func() {
|
|
log.Printf("Server starting on port %s", port)
|
|
if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("Failed to start server: %v", err)
|
|
}
|
|
}()
|
|
|
|
<-a.shutdown
|
|
log.Println("Shutting down server...")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
if err := a.server.Shutdown(ctx); err != nil {
|
|
log.Printf("Server forced to shutdown: %v", err)
|
|
return err
|
|
}
|
|
|
|
log.Println("Server exited gracefully")
|
|
return nil
|
|
}
|
|
|
|
func (a *App) Shutdown() {
|
|
if a.omsetScheduler != nil {
|
|
a.omsetScheduler.Stop()
|
|
}
|
|
if a.walletRecon != nil {
|
|
a.walletRecon.Stop()
|
|
}
|
|
if a.earningRetry != nil {
|
|
a.earningRetry.Stop()
|
|
}
|
|
if a.walletExpiry != nil {
|
|
a.walletExpiry.Stop()
|
|
}
|
|
close(a.shutdown)
|
|
}
|
|
|
|
type repositories struct {
|
|
userRepo *repository.UserRepositoryImpl
|
|
organizationRepo *repository.OrganizationRepositoryImpl
|
|
outletRepo *repository.OutletRepositoryImpl
|
|
outletSettingRepo *repository.OutletSettingRepositoryImpl
|
|
categoryRepo *repository.CategoryRepositoryImpl
|
|
productRepo *repository.ProductRepositoryImpl
|
|
productVariantRepo *repository.ProductVariantRepositoryImpl
|
|
inventoryRepo *repository.InventoryRepositoryImpl
|
|
inventoryMovementRepo *repository.InventoryMovementRepositoryImpl
|
|
orderRepo *repository.OrderRepositoryImpl
|
|
orderItemRepo *repository.OrderItemRepositoryImpl
|
|
paymentRepo *repository.PaymentRepositoryImpl
|
|
paymentOrderItemRepo *repository.PaymentOrderItemRepositoryImpl
|
|
paymentMethodRepo *repository.PaymentMethodRepositoryImpl
|
|
fileRepo *repository.FileRepositoryImpl
|
|
customerRepo *repository.CustomerRepository
|
|
analyticsRepo *repository.AnalyticsRepositoryImpl
|
|
tableRepo *repository.TableRepository
|
|
unitRepo *repository.UnitRepository
|
|
ingredientRepo *repository.IngredientRepository
|
|
ingredientCompositionRepo *repository.IngredientCompositionRepository
|
|
productRecipeRepo *repository.ProductRecipeRepository
|
|
vendorRepo *repository.VendorRepositoryImpl
|
|
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
|
|
purchaseCategoryRepo *repository.PurchaseCategoryRepositoryImpl
|
|
unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl
|
|
chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl
|
|
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
|
accountRepo *repository.AccountRepositoryImpl
|
|
orderIngredientTransactionRepo *repository.OrderIngredientTransactionRepositoryImpl
|
|
customerTokensRepo *repository.CustomerTokensRepository
|
|
tierRepo *repository.TierRepository
|
|
gameRepo *repository.GameRepository
|
|
gamePrizeRepo *repository.GamePrizeRepository
|
|
gamePlayRepo repository.GamePlayRepository
|
|
omsetTrackerRepo *repository.OmsetTrackerRepository
|
|
rewardRepo repository.RewardRepository
|
|
campaignRepo repository.CampaignRepository
|
|
campaignRuleRepo repository.CampaignRuleRepository
|
|
customerAuthRepo repository.CustomerAuthRepository
|
|
customerPointsRepo repository.CustomerPointsRepository
|
|
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
|
|
cashAdvanceRepo *repository.CashAdvanceRepositoryImpl
|
|
walletRepo repository.WalletRepository
|
|
walletQueryRepo repository.WalletQueryRepository
|
|
loyaltySettingsRepo repository.LoyaltySettingsRepository
|
|
}
|
|
|
|
func (a *App) initRepositories() *repositories {
|
|
return &repositories{
|
|
userRepo: repository.NewUserRepository(a.db),
|
|
organizationRepo: repository.NewOrganizationRepositoryImpl(a.db),
|
|
outletRepo: repository.NewOutletRepositoryImpl(a.db),
|
|
outletSettingRepo: repository.NewOutletSettingRepositoryImpl(a.db),
|
|
categoryRepo: repository.NewCategoryRepositoryImpl(a.db),
|
|
productRepo: repository.NewProductRepositoryImpl(a.db),
|
|
productVariantRepo: repository.NewProductVariantRepositoryImpl(a.db),
|
|
inventoryRepo: repository.NewInventoryRepositoryImpl(a.db),
|
|
inventoryMovementRepo: repository.NewInventoryMovementRepositoryImpl(a.db),
|
|
orderRepo: repository.NewOrderRepositoryImpl(a.db),
|
|
orderItemRepo: repository.NewOrderItemRepositoryImpl(a.db),
|
|
paymentRepo: repository.NewPaymentRepositoryImpl(a.db),
|
|
paymentOrderItemRepo: repository.NewPaymentOrderItemRepositoryImpl(a.db),
|
|
paymentMethodRepo: repository.NewPaymentMethodRepositoryImpl(a.db),
|
|
fileRepo: repository.NewFileRepositoryImpl(a.db),
|
|
customerRepo: repository.NewCustomerRepository(a.db),
|
|
analyticsRepo: repository.NewAnalyticsRepositoryImpl(a.db),
|
|
tableRepo: repository.NewTableRepository(a.db),
|
|
unitRepo: repository.NewUnitRepository(a.db),
|
|
ingredientRepo: repository.NewIngredientRepository(a.db),
|
|
ingredientCompositionRepo: repository.NewIngredientCompositionRepository(a.db),
|
|
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),
|
|
accountRepo: repository.NewAccountRepositoryImpl(a.db),
|
|
orderIngredientTransactionRepo: repository.NewOrderIngredientTransactionRepositoryImpl(a.db).(*repository.OrderIngredientTransactionRepositoryImpl),
|
|
customerTokensRepo: repository.NewCustomerTokensRepository(a.db),
|
|
tierRepo: repository.NewTierRepository(a.db),
|
|
gameRepo: repository.NewGameRepository(a.db),
|
|
gamePrizeRepo: repository.NewGamePrizeRepository(a.db),
|
|
gamePlayRepo: repository.NewGamePlayRepository(a.db),
|
|
omsetTrackerRepo: repository.NewOmsetTrackerRepository(a.db),
|
|
rewardRepo: repository.NewRewardRepository(a.db),
|
|
campaignRepo: repository.NewCampaignRepository(a.db),
|
|
campaignRuleRepo: repository.NewCampaignRuleRepository(a.db),
|
|
customerAuthRepo: repository.NewCustomerAuthRepository(a.db),
|
|
customerPointsRepo: repository.NewCustomerPointsRepository(a.db),
|
|
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),
|
|
cashAdvanceRepo: repository.NewCashAdvanceRepositoryImpl(a.db),
|
|
walletRepo: repository.NewWalletRepository(a.db),
|
|
walletQueryRepo: repository.NewWalletQueryRepository(a.db),
|
|
loyaltySettingsRepo: repository.NewLoyaltySettingsRepository(a.db),
|
|
}
|
|
}
|
|
|
|
type processors struct {
|
|
userProcessor *processor.UserProcessorImpl
|
|
organizationProcessor processor.OrganizationProcessor
|
|
outletProcessor processor.OutletProcessor
|
|
outletSettingProcessor *processor.OutletSettingProcessorImpl
|
|
categoryProcessor processor.CategoryProcessor
|
|
productProcessor processor.ProductProcessor
|
|
productVariantProcessor processor.ProductVariantProcessor
|
|
inventoryProcessor processor.InventoryProcessor
|
|
orderProcessor processor.OrderProcessor
|
|
paymentMethodProcessor processor.PaymentMethodProcessor
|
|
fileProcessor processor.FileProcessor
|
|
customerProcessor *processor.CustomerProcessor
|
|
analyticsProcessor *processor.AnalyticsProcessorImpl
|
|
tableProcessor *processor.TableProcessor
|
|
unitProcessor *processor.UnitProcessorImpl
|
|
ingredientProcessor *processor.IngredientProcessorImpl
|
|
productRecipeProcessor *processor.ProductRecipeProcessorImpl
|
|
vendorProcessor *processor.VendorProcessorImpl
|
|
purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl
|
|
purchaseCategoryProcessor *processor.PurchaseCategoryProcessorImpl
|
|
unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl
|
|
chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl
|
|
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
|
|
accountProcessor *processor.AccountProcessorImpl
|
|
orderIngredientTransactionProcessor *processor.OrderIngredientTransactionProcessorImpl
|
|
customerTokensProcessor *processor.CustomerTokensProcessor
|
|
tierProcessor *processor.TierProcessor
|
|
gameProcessor *processor.GameProcessor
|
|
gamePrizeProcessor *processor.GamePrizeProcessor
|
|
gamePlayProcessor *processor.GamePlayProcessor
|
|
omsetTrackerProcessor *processor.OmsetTrackerProcessor
|
|
rewardProcessor processor.RewardProcessor
|
|
campaignProcessor processor.CampaignProcessor
|
|
campaignRuleProcessor processor.CampaignRuleProcessor
|
|
customerAuthProcessor processor.CustomerAuthProcessor
|
|
customerPointsProcessor *processor.CustomerPointsProcessor
|
|
otpProcessor processor.OtpProcessor
|
|
fileClient processor.FileClient
|
|
inventoryMovementService service.InventoryMovementService
|
|
userDeviceProcessor *processor.UserDeviceProcessorImpl
|
|
notificationProcessor *processor.NotificationProcessorImpl
|
|
productOutletPriceProcessor processor.ProductOutletPriceProcessor
|
|
expenseProcessor *processor.ExpenseProcessorImpl
|
|
cashAdvanceProcessor *processor.CashAdvanceProcessorImpl
|
|
walletProcessor *processor.WalletProcessor
|
|
walletAdminProcessor *processor.WalletAdminProcessor
|
|
loyaltySettingsProcessor *processor.LoyaltySettingsProcessor
|
|
earningProcessor *processor.EarningProcessor
|
|
customerPinProcessor *processor.CustomerPinProcessor
|
|
paymentCodeProcessor *processor.PaymentCodeProcessor
|
|
pointPaymentProcessor *processor.PointPaymentProcessor
|
|
walletExchangeProcessor *processor.WalletExchangeProcessor
|
|
walletTransferProcessor *processor.WalletTransferProcessor
|
|
walletTraceProcessor *processor.WalletTraceProcessor
|
|
customerDeviceProcessor *processor.CustomerDeviceProcessor
|
|
}
|
|
|
|
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
|
fileClient := client.NewFileClient(cfg.S3Config)
|
|
fcmClient := buildFCMClient(cfg)
|
|
// Devices of the customer app, for pushes through FCM
|
|
customerDeviceProcessor := processor.NewCustomerDeviceProcessor(repository.NewCustomerDeviceRepository(a.db), fcmClient)
|
|
fonnteClient := client.NewFonnteClient(cfg.GetFonnte())
|
|
otpProcessor := processor.NewOtpProcessor(fonnteClient, repos.otpRepo)
|
|
// Customer PIN (docs/prd-point-coin.md F11)
|
|
customerPinProcessor := processor.NewCustomerPinProcessor(repository.NewCustomerPinRepository(a.db), otpProcessor, customerDeviceProcessor)
|
|
paymentCodeProcessor := processor.NewPaymentCodeProcessor(repository.NewPaymentCodeRepository(a.redisClient), customerPinProcessor)
|
|
inventoryMovementService := service.NewInventoryMovementService(repos.inventoryMovementRepo, repos.ingredientRepo)
|
|
|
|
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)
|
|
loyaltySettingsProcessor := processor.NewLoyaltySettingsProcessor(repos.loyaltySettingsRepo, repos.txManager)
|
|
// Earn EnakPoint and EnakCoin when an order becomes fully paid (docs/prd-point-coin.md F3)
|
|
earningProcessor := processor.NewEarningProcessor(repository.NewEarningRepository(a.db), loyaltySettingsProcessor, processor.NewWalletProcessor(repos.walletRepo), repos.txManager)
|
|
orderProcessor.SetLoyalty(earningProcessor)
|
|
// Pay orders with EnakPoint, approved by the customer's one-time code (docs/prd-point-coin.md F9)
|
|
pointPaymentProcessor := processor.NewPointPaymentProcessor(repository.NewPointPaymentRepository(a.db), loyaltySettingsProcessor, repos.walletQueryRepo, processor.NewWalletProcessor(repos.walletRepo), repos.txManager)
|
|
orderProcessor.SetPointPayments(pointPaymentProcessor, paymentCodeProcessor, customerPinProcessor)
|
|
// Exchange EnakCoin into EnakPoint, approved by the customer's PIN (docs/prd-point-coin.md F4)
|
|
walletExchangeProcessor := processor.NewWalletExchangeProcessor(repository.NewWalletMoveRepository(a.db), loyaltySettingsProcessor, repos.walletQueryRepo, customerPinProcessor, processor.NewWalletProcessor(repos.walletRepo), repos.txManager)
|
|
// Send EnakPoint or EnakCoin to another customer; the recipient gets a push through FCM (docs/prd-point-coin.md F5)
|
|
walletTransferProcessor := processor.NewWalletTransferProcessor(repository.NewWalletMoveRepository(a.db), loyaltySettingsProcessor, repos.walletQueryRepo, customerPinProcessor, processor.NewWalletProcessor(repos.walletRepo), repos.txManager, customerDeviceProcessor)
|
|
|
|
return &processors{
|
|
userProcessor: processor.NewUserProcessor(repos.userRepo, repos.organizationRepo, repos.outletRepo),
|
|
organizationProcessor: processor.NewOrganizationProcessorImpl(repos.organizationRepo, repos.outletRepo, repos.userRepo),
|
|
outletProcessor: processor.NewOutletProcessorImpl(repos.outletRepo),
|
|
outletSettingProcessor: processor.NewOutletSettingProcessorImpl(repos.outletSettingRepo, repos.outletRepo),
|
|
categoryProcessor: processor.NewCategoryProcessorImpl(repos.categoryRepo),
|
|
productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo, repos.productOutletPriceRepo),
|
|
productVariantProcessor: processor.NewProductVariantProcessorImpl(repos.productVariantRepo, repos.productRepo),
|
|
inventoryProcessor: processor.NewInventoryProcessorImpl(repos.inventoryRepo, repos.productRepo, repos.outletRepo, repos.ingredientRepo, repos.inventoryMovementRepo),
|
|
orderProcessor: orderProcessor,
|
|
paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo, loyaltySettingsProcessor),
|
|
fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient),
|
|
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
|
|
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.purchaseCategoryRepo, repos.categoryRepo, repos.cashAdvanceRepo, 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),
|
|
accountProcessor: processor.NewAccountProcessorImpl(repos.accountRepo, repos.chartOfAccountRepo),
|
|
orderIngredientTransactionProcessor: processor.NewOrderIngredientTransactionProcessorImpl(repos.orderIngredientTransactionRepo, repos.productRecipeRepo, repos.ingredientRepo, repos.unitRepo).(*processor.OrderIngredientTransactionProcessorImpl),
|
|
customerTokensProcessor: processor.NewCustomerTokensProcessor(repos.customerTokensRepo),
|
|
tierProcessor: processor.NewTierProcessor(repos.tierRepo),
|
|
gameProcessor: processor.NewGameProcessor(repos.gameRepo),
|
|
gamePrizeProcessor: processor.NewGamePrizeProcessor(repos.gamePrizeRepo),
|
|
gamePlayProcessor: processor.NewGamePlayProcessor(repos.gamePlayRepo, repos.gameRepo, repos.gamePrizeRepo, processor.NewWalletProcessor(repos.walletRepo), repos.walletQueryRepo, repos.txManager),
|
|
omsetTrackerProcessor: processor.NewOmsetTrackerProcessor(repos.omsetTrackerRepo),
|
|
rewardProcessor: processor.NewRewardProcessor(repos.rewardRepo),
|
|
campaignProcessor: processor.NewCampaignProcessor(repos.campaignRepo),
|
|
campaignRuleProcessor: processor.NewCampaignRuleProcessor(repos.campaignRuleRepo),
|
|
customerAuthProcessor: processor.NewCustomerAuthProcessor(repos.customerAuthRepo, otpProcessor, repos.otpRepo, cfg.GetCustomerJWTSecret(), cfg.GetCustomerJWTExpiresTTL()),
|
|
customerPointsProcessor: processor.NewCustomerPointsProcessor(processor.NewWalletQueryProcessor(repos.walletQueryRepo, processor.NewLoyaltySettingsProcessor(repos.loyaltySettingsRepo, repos.txManager)), repos.gameRepo),
|
|
otpProcessor: otpProcessor,
|
|
fileClient: fileClient,
|
|
inventoryMovementService: inventoryMovementService,
|
|
userDeviceProcessor: processor.NewUserDeviceProcessorImpl(repos.userDeviceRepo),
|
|
notificationProcessor: buildNotificationProcessor(repos, fcmClient),
|
|
productOutletPriceProcessor: processor.NewProductOutletPriceProcessorImpl(repos.productOutletPriceRepo, repos.productRepo, repos.outletRepo),
|
|
expenseProcessor: processor.NewExpenseProcessorImpl(repos.expenseRepo, repos.purchaseCategoryRepo, repos.cashAdvanceRepo),
|
|
cashAdvanceProcessor: processor.NewCashAdvanceProcessorImpl(repos.cashAdvanceRepo, repos.categoryRepo),
|
|
walletProcessor: processor.NewWalletProcessor(repos.walletRepo),
|
|
loyaltySettingsProcessor: loyaltySettingsProcessor,
|
|
earningProcessor: earningProcessor,
|
|
customerPinProcessor: customerPinProcessor,
|
|
paymentCodeProcessor: paymentCodeProcessor,
|
|
pointPaymentProcessor: pointPaymentProcessor,
|
|
walletExchangeProcessor: walletExchangeProcessor,
|
|
walletTransferProcessor: walletTransferProcessor,
|
|
walletTraceProcessor: processor.NewWalletTraceProcessor(repository.NewWalletTraceRepository(a.db)),
|
|
customerDeviceProcessor: customerDeviceProcessor,
|
|
walletAdminProcessor: processor.NewWalletAdminProcessor(repository.NewWalletAdminRepository(a.db), repos.walletQueryRepo, processor.NewWalletProcessor(repos.walletRepo), loyaltySettingsProcessor, repos.txManager),
|
|
}
|
|
}
|
|
|
|
type services struct {
|
|
userService *service.UserServiceImpl
|
|
authService service.AuthService
|
|
organizationService service.OrganizationService
|
|
outletService service.OutletService
|
|
outletSettingService service.OutletSettingService
|
|
categoryService service.CategoryService
|
|
productService service.ProductService
|
|
productVariantService service.ProductVariantService
|
|
inventoryService service.InventoryService
|
|
orderService service.OrderService
|
|
paymentMethodService service.PaymentMethodService
|
|
fileService service.FileService
|
|
customerService service.CustomerService
|
|
analyticsService *service.AnalyticsServiceImpl
|
|
reportService service.ReportService
|
|
tableService *service.TableServiceImpl
|
|
unitService *service.UnitServiceImpl
|
|
ingredientService *service.IngredientServiceImpl
|
|
productRecipeService *service.ProductRecipeServiceImpl
|
|
vendorService *service.VendorServiceImpl
|
|
purchaseOrderService *service.PurchaseOrderServiceImpl
|
|
purchaseCategoryService service.PurchaseCategoryService
|
|
unitConverterService *service.IngredientUnitConverterServiceImpl
|
|
chartOfAccountTypeService service.ChartOfAccountTypeService
|
|
chartOfAccountService service.ChartOfAccountService
|
|
accountService service.AccountService
|
|
orderIngredientTransactionService *service.OrderIngredientTransactionService
|
|
gamificationService service.GamificationService
|
|
rewardService service.RewardService
|
|
campaignService service.CampaignService
|
|
customerAuthService service.CustomerAuthService
|
|
customerPointsService service.CustomerPointsService
|
|
spinGameService service.SpinGameService
|
|
userDeviceService service.UserDeviceService
|
|
notificationService service.NotificationService
|
|
productOutletPriceService service.ProductOutletPriceService
|
|
expenseService *service.ExpenseServiceImpl
|
|
cashAdvanceService *service.CashAdvanceServiceImpl
|
|
walletAdminService *service.WalletAdminServiceImpl
|
|
loyaltySettingsService *service.LoyaltySettingsServiceImpl
|
|
customerPinService *service.CustomerPinServiceImpl
|
|
pointPaymentService *service.PointPaymentServiceImpl
|
|
customerOrderPaymentService *service.CustomerOrderPaymentServiceImpl
|
|
customerWalletService *service.CustomerWalletServiceImpl
|
|
customerDeviceService *service.CustomerDeviceServiceImpl
|
|
}
|
|
|
|
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
|
authConfig := cfg.Auth()
|
|
authService := service.NewAuthService(processors.userProcessor, processors.userDeviceProcessor, authConfig)
|
|
organizationService := service.NewOrganizationService(processors.organizationProcessor)
|
|
outletService := service.NewOutletService(processors.outletProcessor)
|
|
outletSettingService := service.NewOutletSettingService(processors.outletSettingProcessor)
|
|
categoryService := service.NewCategoryService(processors.categoryProcessor)
|
|
productService := service.NewProductService(processors.productProcessor)
|
|
productVariantService := service.NewProductVariantService(processors.productVariantProcessor)
|
|
inventoryService := service.NewInventoryService(processors.inventoryProcessor)
|
|
orderService := service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, nil, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo, processors.notificationProcessor, repos.userRepo) // Will be updated after orderIngredientTransactionService is created
|
|
paymentMethodService := service.NewPaymentMethodService(processors.paymentMethodProcessor)
|
|
fileService := service.NewFileServiceImpl(processors.fileProcessor)
|
|
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor)
|
|
analyticsService := service.NewAnalyticsServiceImpl(processors.analyticsProcessor)
|
|
reportService := service.NewReportService(analyticsService, repos.organizationRepo, repos.outletRepo, processors.fileClient)
|
|
tableService := service.NewTableService(processors.tableProcessor, transformer.NewTableTransformer())
|
|
unitService := service.NewUnitService(processors.unitProcessor)
|
|
ingredientService := service.NewIngredientService(processors.ingredientProcessor)
|
|
productRecipeService := service.NewProductRecipeService(processors.productRecipeProcessor)
|
|
vendorService := service.NewVendorService(processors.vendorProcessor)
|
|
purchaseOrderService := service.NewPurchaseOrderService(processors.purchaseOrderProcessor)
|
|
purchaseCategoryService := service.NewPurchaseCategoryService(processors.purchaseCategoryProcessor)
|
|
unitConverterService := service.NewIngredientUnitConverterService(processors.unitConverterProcessor)
|
|
chartOfAccountTypeService := service.NewChartOfAccountTypeService(processors.chartOfAccountTypeProcessor)
|
|
chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor)
|
|
accountService := service.NewAccountService(processors.accountProcessor)
|
|
orderIngredientTransactionService := service.NewOrderIngredientTransactionService(processors.orderIngredientTransactionProcessor, repos.txManager)
|
|
gamificationService := service.NewGamificationService(processors.customerPointsProcessor, processors.customerTokensProcessor, processors.tierProcessor, processors.gameProcessor, processors.gamePrizeProcessor, processors.gamePlayProcessor, processors.omsetTrackerProcessor)
|
|
rewardService := service.NewRewardService(processors.rewardProcessor)
|
|
campaignService := service.NewCampaignService(processors.campaignProcessor, processors.campaignRuleProcessor)
|
|
customerAuthService := service.NewCustomerAuthService(processors.customerAuthProcessor)
|
|
customerPointsService := service.NewCustomerPointsService(processors.customerPointsProcessor)
|
|
spinGameService := service.NewSpinGameService(processors.gamePlayProcessor)
|
|
userDeviceService := service.NewUserDeviceService(processors.userDeviceProcessor)
|
|
notificationService := service.NewNotificationService(processors.notificationProcessor)
|
|
|
|
// Update order service with order ingredient transaction service
|
|
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo, processors.notificationProcessor, repos.userRepo)
|
|
|
|
return &services{
|
|
userService: service.NewUserService(processors.userProcessor),
|
|
authService: authService,
|
|
organizationService: organizationService,
|
|
outletService: outletService,
|
|
outletSettingService: outletSettingService,
|
|
categoryService: categoryService,
|
|
productService: productService,
|
|
productVariantService: productVariantService,
|
|
inventoryService: inventoryService,
|
|
orderService: orderService,
|
|
paymentMethodService: paymentMethodService,
|
|
fileService: fileService,
|
|
customerService: customerService,
|
|
analyticsService: analyticsService,
|
|
reportService: reportService,
|
|
tableService: tableService,
|
|
unitService: unitService,
|
|
ingredientService: ingredientService,
|
|
productRecipeService: productRecipeService,
|
|
vendorService: vendorService,
|
|
purchaseOrderService: purchaseOrderService,
|
|
purchaseCategoryService: purchaseCategoryService,
|
|
unitConverterService: unitConverterService,
|
|
chartOfAccountTypeService: chartOfAccountTypeService,
|
|
chartOfAccountService: chartOfAccountService,
|
|
accountService: accountService,
|
|
orderIngredientTransactionService: orderIngredientTransactionService,
|
|
gamificationService: gamificationService,
|
|
rewardService: rewardService,
|
|
campaignService: campaignService,
|
|
customerAuthService: customerAuthService,
|
|
customerPointsService: customerPointsService,
|
|
spinGameService: spinGameService,
|
|
userDeviceService: userDeviceService,
|
|
notificationService: notificationService,
|
|
productOutletPriceService: service.NewProductOutletPriceService(processors.productOutletPriceProcessor),
|
|
expenseService: service.NewExpenseService(processors.expenseProcessor),
|
|
cashAdvanceService: service.NewCashAdvanceService(processors.cashAdvanceProcessor),
|
|
walletAdminService: service.NewWalletAdminService(processors.walletAdminProcessor, processors.walletTraceProcessor),
|
|
loyaltySettingsService: service.NewLoyaltySettingsService(processors.loyaltySettingsProcessor, repos.walletQueryRepo),
|
|
customerPinService: service.NewCustomerPinService(processors.customerPinProcessor, processors.paymentCodeProcessor),
|
|
pointPaymentService: service.NewPointPaymentService(processors.pointPaymentProcessor),
|
|
customerOrderPaymentService: service.NewCustomerOrderPaymentService(processors.orderProcessor),
|
|
customerWalletService: service.NewCustomerWalletService(processors.walletExchangeProcessor, processors.walletTransferProcessor),
|
|
customerDeviceService: service.NewCustomerDeviceService(processors.customerDeviceProcessor),
|
|
}
|
|
}
|
|
|
|
type middlewares struct {
|
|
authMiddleware *middleware.AuthMiddleware
|
|
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
|
}
|
|
|
|
func (a *App) initMiddleware(services *services, cfg *config.Config) *middlewares {
|
|
return &middlewares{
|
|
authMiddleware: middleware.NewAuthMiddleware(services.authService),
|
|
customerAuthMiddleware: middleware.NewCustomerAuthMiddleware(cfg.GetCustomerJWTSecret()),
|
|
}
|
|
}
|
|
|
|
type validators struct {
|
|
userValidator *validator.UserValidatorImpl
|
|
organizationValidator validator.OrganizationValidator
|
|
outletValidator validator.OutletValidator
|
|
categoryValidator validator.CategoryValidator
|
|
productValidator validator.ProductValidator
|
|
productVariantValidator validator.ProductVariantValidator
|
|
inventoryValidator validator.InventoryValidator
|
|
orderValidator validator.OrderValidator
|
|
paymentMethodValidator validator.PaymentMethodValidator
|
|
fileValidator validator.FileValidator
|
|
customerValidator validator.CustomerValidator
|
|
tableValidator *validator.TableValidator
|
|
vendorValidator *validator.VendorValidatorImpl
|
|
purchaseOrderValidator *validator.PurchaseOrderValidatorImpl
|
|
purchaseCategoryValidator *validator.PurchaseCategoryValidatorImpl
|
|
unitConverterValidator *validator.IngredientUnitConverterValidatorImpl
|
|
chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl
|
|
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
|
|
accountValidator *validator.AccountValidatorImpl
|
|
orderIngredientTransactionValidator *validator.OrderIngredientTransactionValidatorImpl
|
|
gamificationValidator *validator.GamificationValidatorImpl
|
|
rewardValidator validator.RewardValidator
|
|
campaignValidator validator.CampaignValidator
|
|
customerAuthValidator validator.CustomerAuthValidator
|
|
userDeviceValidator *validator.UserDeviceValidatorImpl
|
|
notificationValidator *validator.NotificationValidatorImpl
|
|
productOutletPriceValidator *validator.ProductOutletPriceValidatorImpl
|
|
expenseValidator *validator.ExpenseValidatorImpl
|
|
cashAdvanceValidator *validator.CashAdvanceValidatorImpl
|
|
walletValidator *validator.WalletValidatorImpl
|
|
}
|
|
|
|
func (a *App) initValidators() *validators {
|
|
return &validators{
|
|
userValidator: validator.NewUserValidator(),
|
|
organizationValidator: validator.NewOrganizationValidator(),
|
|
outletValidator: validator.NewOutletValidator(),
|
|
categoryValidator: validator.NewCategoryValidator(),
|
|
productValidator: validator.NewProductValidator(),
|
|
productVariantValidator: validator.NewProductVariantValidator(),
|
|
inventoryValidator: validator.NewInventoryValidator(),
|
|
orderValidator: validator.NewOrderValidator(),
|
|
paymentMethodValidator: validator.NewPaymentMethodValidator(),
|
|
fileValidator: validator.NewFileValidatorImpl(),
|
|
customerValidator: validator.NewCustomerValidator(),
|
|
tableValidator: validator.NewTableValidator(),
|
|
vendorValidator: validator.NewVendorValidator(),
|
|
purchaseOrderValidator: validator.NewPurchaseOrderValidator(),
|
|
purchaseCategoryValidator: validator.NewPurchaseCategoryValidator(),
|
|
unitConverterValidator: validator.NewIngredientUnitConverterValidator().(*validator.IngredientUnitConverterValidatorImpl),
|
|
chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl),
|
|
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
|
|
accountValidator: validator.NewAccountValidator().(*validator.AccountValidatorImpl),
|
|
orderIngredientTransactionValidator: validator.NewOrderIngredientTransactionValidator().(*validator.OrderIngredientTransactionValidatorImpl),
|
|
gamificationValidator: validator.NewGamificationValidator(),
|
|
rewardValidator: validator.NewRewardValidator(),
|
|
campaignValidator: validator.NewCampaignValidator(),
|
|
customerAuthValidator: validator.NewCustomerAuthValidator(),
|
|
userDeviceValidator: validator.NewUserDeviceValidator(),
|
|
notificationValidator: validator.NewNotificationValidator(),
|
|
productOutletPriceValidator: validator.NewProductOutletPriceValidator(),
|
|
expenseValidator: validator.NewExpenseValidator(),
|
|
cashAdvanceValidator: validator.NewCashAdvanceValidator(),
|
|
walletValidator: validator.NewWalletValidator(),
|
|
}
|
|
}
|
|
|
|
// buildFCMClient connects to FCM, or returns nil when it is not configured or fails to
|
|
// start. FCM failing is non-fatal: pushes are skipped and notifications are still
|
|
// persisted.
|
|
func buildFCMClient(cfg *config.Config) client.FCMClient {
|
|
if cfg.FCM.CredentialsFile == "" {
|
|
return nil
|
|
}
|
|
fcmClient, err := client.NewFCMClient(&cfg.FCM)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return fcmClient
|
|
}
|
|
|
|
// 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(repos *repositories, fcmClient client.FCMClient) *processor.NotificationProcessorImpl {
|
|
return processor.NewNotificationProcessor(
|
|
repos.notificationRepo,
|
|
repos.notificationReceiverRepo,
|
|
repos.notificationDeliveryRepo,
|
|
repos.userDeviceRepo,
|
|
repos.userRepo,
|
|
fcmClient,
|
|
)
|
|
}
|