feat(loyalty): typed loyalty settings with change history
Adds LoyaltySettingsProcessor (docs/prd-point-coin.md F1, F2, F12, PC-109). Reading returns typed settings for an outlet (earning per currency, paying with EnakPoint) and for an organization (point value, exchange rate, transfers, and the expiry settings awaiting note N4). A key that was never set takes the PRD default. A stored value that is unusable, such as an earn_per_amount of 0 that would divide by zero, also falls back to the default and is logged, so a bad row never reaches a calculation. Writing takes the whole settings struct, validates every rule in the PRD before touching the database, and stores and records in loyalty_setting_changes only the keys whose effective value changes: old value (NULL while it was on its default), new value, and who changed it. Clearing a limit deletes the stored value. Each save runs in one transaction under an advisory lock per outlet or organization, so two saves at once cannot both compute their change from the same old value. The outlet must belong to the caller's organization. Every key is described once (key, default, valid range, bound field), and reading, validating and diffing all use that description. GET /customer/wallet now reads the point value through this processor; the minimal organization settings repository from PC-106 is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
040780cd2d
commit
39e47ff0e6
@@ -0,0 +1,81 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// OutletLoyaltySettings are an outlet's loyalty settings (docs/prd-point-coin.md F1).
|
||||
type OutletLoyaltySettings struct {
|
||||
Point LoyaltyEarnSettings `json:"point"`
|
||||
Coin LoyaltyEarnSettings `json:"coin"`
|
||||
// Paying with EnakPoint. EnakCoin cannot pay, so it has no counterpart.
|
||||
PointPayment LoyaltyPointPaymentSettings `json:"point_payment"`
|
||||
}
|
||||
|
||||
// LoyaltyEarnSettings is how much of one currency an order earns:
|
||||
// floor(basis / EarnPerAmount) × EarnValue, nothing below MinOrderAmount, and at most
|
||||
// MaxPerOrder when set.
|
||||
type LoyaltyEarnSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
EarnPerAmount int64 `json:"earn_per_amount"`
|
||||
EarnValue int64 `json:"earn_value"`
|
||||
MinOrderAmount int64 `json:"min_order_amount"`
|
||||
MaxPerOrder *int64 `json:"max_per_order"`
|
||||
}
|
||||
|
||||
type LoyaltyPointPaymentSettings struct {
|
||||
AcceptPayment bool `json:"accept_payment"`
|
||||
MinPaymentPoints int64 `json:"min_payment_points"`
|
||||
// Largest share of the order total, 0–100, that EnakPoint may pay.
|
||||
MaxPaymentPercent int64 `json:"max_payment_percent"`
|
||||
}
|
||||
|
||||
// OrganizationLoyaltySettings are the loyalty settings shared by every outlet of an
|
||||
// organization (docs/prd-point-coin.md F2, F12).
|
||||
type OrganizationLoyaltySettings struct {
|
||||
// Rupiah value of one EnakPoint when paying.
|
||||
PointValue int64 `json:"point_value"`
|
||||
// CoinAmount EnakCoin exchange into PointAmount EnakPoint.
|
||||
Exchange LoyaltyExchangeSettings `json:"exchange"`
|
||||
Transfer LoyaltyTransferSettings `json:"transfer"`
|
||||
PointExpiry LoyaltyExpirySettings `json:"point_expiry"`
|
||||
CoinExpiry LoyaltyExpirySettings `json:"coin_expiry"`
|
||||
}
|
||||
|
||||
type LoyaltyExchangeSettings struct {
|
||||
CoinAmount int64 `json:"coin_amount"`
|
||||
PointAmount int64 `json:"point_amount"`
|
||||
}
|
||||
|
||||
type LoyaltyTransferSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
MinAmount int64 `json:"min_amount"`
|
||||
MaxPerTransaction *int64 `json:"max_per_transaction"`
|
||||
DailyLimit *int64 `json:"daily_limit"`
|
||||
}
|
||||
|
||||
// LoyaltyExpirySettings is how long one currency lasts once received. The expiry
|
||||
// model is still open (note N4); these are only the stored settings.
|
||||
type LoyaltyExpirySettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Period int64 `json:"period"`
|
||||
// DAY or MONTH.
|
||||
Unit string `json:"unit"`
|
||||
EndOfMonth bool `json:"end_of_month"`
|
||||
ReminderDays int64 `json:"reminder_days"`
|
||||
}
|
||||
|
||||
// LoyaltySettingChange is one row of the loyalty settings history.
|
||||
type LoyaltySettingChange struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Key string `json:"key"`
|
||||
// Nil when the key had no stored value, that is it was on its default.
|
||||
OldValue *string `json:"old_value"`
|
||||
NewValue *string `json:"new_value"`
|
||||
ChangedBy uuid.UUID `json:"changed_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
Reference in New Issue
Block a user