Adds GET and PUT /outlets/:id/loyalty-settings (docs/prd-point-coin.md F1, PC-201) on top of the typed settings processor. The response shows every setting with its default when unset, the organization's point value, and the effective EnakPoint cashback (earn_value × point_value / earn_per_amount), so an owner cannot misread the scale. PUT applies the body on top of the current settings: fields left out keep their value, null clears an optional limit, and unknown fields are refused so a typo cannot be ignored silently. The read-only fields of the GET response are accepted and ignored, so a client can send back what it received. It returns the keys that changed. Values outside the F1 bounds answer 400, and an outlet of another organization 404. RequireAdminOrManager also lets the purchasing role through, so loyalty settings and the manual wallet adjustment from PC-107 now use a stricter RequireLoyaltyManager (superadmin, admin, manager, owner). Adds a test that registers every route, since gin panics at startup when two routes name the same path parameter differently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
3.8 KiB
Go
106 lines
3.8 KiB
Go
package models
|
||
|
||
import (
|
||
"math"
|
||
"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"`
|
||
}
|
||
|
||
// OutletLoyaltySettingsView is GET and PUT /outlets/:id/loyalty-settings.
|
||
type OutletLoyaltySettingsView struct {
|
||
OutletID uuid.UUID `json:"outlet_id"`
|
||
OutletLoyaltySettings
|
||
// The organization's rupiah value of one EnakPoint, which the cashback depends on.
|
||
PointValue int64 `json:"point_value"`
|
||
// Effective EnakPoint cashback in percent: earn_value × point_value /
|
||
// earn_per_amount × 100. Shown next to the setting so an owner cannot misread the
|
||
// scale (F1).
|
||
PointCashbackPercent float64 `json:"point_cashback_percent"`
|
||
// Set on PUT: the keys that changed.
|
||
Changes []LoyaltySettingChange `json:"changes,omitempty"`
|
||
}
|
||
|
||
// LoyaltyCashbackPercent is earnValue × pointValue / earnPerAmount as a percentage,
|
||
// rounded to two decimals.
|
||
func LoyaltyCashbackPercent(earnValue, pointValue, earnPerAmount int64) float64 {
|
||
if earnPerAmount <= 0 {
|
||
return 0
|
||
}
|
||
return math.Round(float64(earnValue)*float64(pointValue)*10000/float64(earnPerAmount)) / 100
|
||
}
|