feat(loyalty): customer PIN
Adds the 6-digit customer PIN that approves every action moving EnakPoint or EnakCoin on the customer's request (docs/prd-point-coin.md K8, F11, Q16, Q17, PC-301). Migration 000093 adds the PIN columns to customers and the customer_security_events table. PIN data is read and written only through CustomerPinRepository, never the Customer entity, so the hash cannot reach a customer response. Only a bcrypt hash is stored. - /customer/pin: status, OTP (pin_setup, pin_reset), create, change, reset. The OTP must be for that purpose and sent to the customer's own number; the existing OTP validation checks neither. A new PIN is checked (6 digits, confirmed, not one digit, not a run up or down, not the birth date as DDMMYY or YYMMDD) before the OTP is spent. - Five wrong attempts in a row lock the PIN for 30 minutes; the counter is incremented in one statement so attempts at the same time all count, and a lock that ran out starts a new series. A locked PIN is refused even when right. The customer is told by WhatsApp, as there is no push channel to customers yet; only the attempt that reached the limit alerts. - A reset through OTP lifts the lock and holds outgoing transfers for 24 hours; paying and exchanging still work, and a held transfer costs no attempt. - VerifyPin(ctx, customer, pin, action) for the flows that follow, with PIN_NOT_SET, PIN_INVALID (attempts left), PIN_LOCKED and TRANSFER_BLOCKED (until when), which PinErrorResponse turns into distinct codes and statuses. - DELETE /marketing/customers/:id/pin (loyalty managers, reason required) and GET /marketing/customers/:id/security-events, scoped to the organization. Every PIN event is in the security log with IP and user agent. No message or binding error contains a PIN. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fc97c78300
commit
8370851ed2
@@ -156,6 +156,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
services.walletAdminService,
|
||||
validators.walletValidator,
|
||||
services.loyaltySettingsService,
|
||||
services.customerPinService,
|
||||
a.redisClient,
|
||||
)
|
||||
|
||||
@@ -386,12 +387,15 @@ type processors struct {
|
||||
walletAdminProcessor *processor.WalletAdminProcessor
|
||||
loyaltySettingsProcessor *processor.LoyaltySettingsProcessor
|
||||
earningProcessor *processor.EarningProcessor
|
||||
customerPinProcessor *processor.CustomerPinProcessor
|
||||
}
|
||||
|
||||
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
||||
fileClient := client.NewFileClient(cfg.S3Config)
|
||||
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, otpProcessor)
|
||||
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)
|
||||
@@ -448,6 +452,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
walletProcessor: processor.NewWalletProcessor(repos.walletRepo),
|
||||
loyaltySettingsProcessor: loyaltySettingsProcessor,
|
||||
earningProcessor: earningProcessor,
|
||||
customerPinProcessor: customerPinProcessor,
|
||||
walletAdminProcessor: processor.NewWalletAdminProcessor(repository.NewWalletAdminRepository(a.db), repos.walletQueryRepo, processor.NewWalletProcessor(repos.walletRepo), repos.txManager),
|
||||
}
|
||||
}
|
||||
@@ -493,6 +498,7 @@ type services struct {
|
||||
cashAdvanceService *service.CashAdvanceServiceImpl
|
||||
walletAdminService *service.WalletAdminServiceImpl
|
||||
loyaltySettingsService *service.LoyaltySettingsServiceImpl
|
||||
customerPinService *service.CustomerPinServiceImpl
|
||||
}
|
||||
|
||||
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
||||
@@ -576,6 +582,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
cashAdvanceService: service.NewCashAdvanceService(processors.cashAdvanceProcessor),
|
||||
walletAdminService: service.NewWalletAdminService(processors.walletAdminProcessor),
|
||||
loyaltySettingsService: service.NewLoyaltySettingsService(processors.loyaltySettingsProcessor),
|
||||
customerPinService: service.NewCustomerPinService(processors.customerPinProcessor),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user