package models import ( "math" "time" "github.com/google/uuid" "apskel-pos-be/internal/constants" ) // OutletLoyaltySettings are an outlet's loyalty settings (docs/prd-point-coin.md F1). type OutletLoyaltySettings struct { Point LoyaltyEarnSettings `json:"point"` Coin LoyaltyEarnSettings `json:"coin"` } // LoyaltyEarnSettings is how much of one currency an order earns, nothing below // MinOrderAmount, and at most MaxPerOrder when set: // // - PER_AMOUNT: floor(basis / EarnPerAmount) × EarnValue; // - PERCENTAGE: floor(basis × EarnPercent / 100). // // The settings of the mode not in use are kept, so switching back restores them. type LoyaltyEarnSettings struct { Enabled bool `json:"enabled"` // PER_AMOUNT or PERCENTAGE. EarnMode string `json:"earn_mode"` EarnPerAmount int64 `json:"earn_per_amount"` EarnValue int64 `json:"earn_value"` // PERCENTAGE: percent of the basis earned, 0–100 with at most two decimals. EarnPercent float64 `json:"earn_percent"` MinOrderAmount int64 `json:"min_order_amount"` MaxPerOrder *int64 `json:"max_per_order"` } // 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"` EnakGame EnakGameLimitSettings `json:"enakgame"` } // EnakGameLimitSettings caps the EnakCoin EnakGame rewards per day, in Asia/Jakarta // (docs/rfc-enakgame.md §5.10). 0 means no limit. type EnakGameLimitSettings struct { // What one customer may receive. UserDailyLimit int64 `json:"user_daily_limit"` // What the whole organization may give out. GlobalDailyLimit int64 `json:"global_daily_limit"` } 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 when one currency expires once received (F12). Both // models of note N4 are supported, and the owner picks one: // // - FIXED_DATE: everything expires on the next of FixedDates falling on or after // the day received + GraceMonths, so a balance received just before a date moves // on to the one after. // - ROLLING: everything lasts Period Units from the day received, to the end of // that month when EndOfMonth is set. type LoyaltyExpirySettings struct { Enabled bool `json:"enabled"` // FIXED_DATE or ROLLING. Mode string `json:"mode"` // FIXED_DATE: the days of the year balances expire on, as MM-DD, sorted. FixedDates []string `json:"fixed_dates"` // FIXED_DATE: how many months a balance lasts at least before a fixed date takes it. GraceMonths int64 `json:"grace_months"` // ROLLING: how long a balance lasts. Period int64 `json:"period"` // ROLLING: DAY or MONTH. Unit string `json:"unit"` EndOfMonth bool `json:"end_of_month"` // Days before expiry the customer is reminded; 0 for no reminder. 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, or earn_percent × point_value in PERCENTAGE mode. 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 } // OrganizationLoyaltySettingsView is GET and PUT /marketing/loyalty-settings. type OrganizationLoyaltySettingsView struct { OrganizationLoyaltySettings // What the balances in circulation are worth, before and after the change. Impact LoyaltySettingsImpact `json:"impact"` // When a balance received now would expire under these settings (F12). ExpiryPreview LoyaltyExpiryPreview `json:"expiry_preview"` // The currencies this change turns expiry on for, and the balances affected. ExpiryActivations []LoyaltyExpiryActivation `json:"expiry_activations"` // On PUT, the keys that changed; on a dry run, the keys that would. Changes []LoyaltySettingChange `json:"changes"` // True when nothing was saved. DryRun bool `json:"dry_run"` } // LoyaltyExpiryActivation is expiry being turned on for a currency: the balances that // had no expiry and the expiry they get (F12). On a dry run nothing is dated yet. type LoyaltyExpiryActivation struct { Currency string `json:"currency"` Lots int64 `json:"lots"` Amount int64 `json:"amount"` ExpiresAt time.Time `json:"expires_at"` } // LoyaltyExpiryPreview is what the dashboard shows next to the expiry settings: "the // EnakPoint received today expire on …". Nil means they never expire. type LoyaltyExpiryPreview struct { Point *time.Time `json:"point"` Coin *time.Time `json:"coin"` } // LoyaltySettingsImpact shows how a change of point value or exchange rate changes what // the balances in circulation are worth (F2). Before and after are equal when neither // changes. type LoyaltySettingsImpact struct { OutstandingPoints int64 `json:"outstanding_points"` OutstandingCoins int64 `json:"outstanding_coins"` PointValueBefore int64 `json:"point_value_before"` PointValueAfter int64 `json:"point_value_after"` PointRupiahBefore int64 `json:"point_rupiah_before"` PointRupiahAfter int64 `json:"point_rupiah_after"` // The coins in circulation exchanged at the rate, in EnakPoint and in rupiah. CoinsAsPointsBefore int64 `json:"coins_as_points_before"` CoinsAsPointsAfter int64 `json:"coins_as_points_after"` CoinRupiahBefore int64 `json:"coin_rupiah_before"` CoinRupiahAfter int64 `json:"coin_rupiah_after"` } // CashbackPercent is the rupiah value of what an order earns as a percentage of its // basis, in the mode the settings are in, rounded to two decimals. func (s LoyaltyEarnSettings) CashbackPercent(pointValue int64) float64 { if s.EarnMode == constants.LoyaltyEarnModePercentage { return math.Round(s.EarnPercent*float64(pointValue)*100) / 100 } return LoyaltyCashbackPercent(s.EarnValue, pointValue, s.EarnPerAmount) } // NewLoyaltySettingsImpact computes the impact of moving from one organization setting // to another on the balances in circulation. func NewLoyaltySettingsImpact(points, coins int64, before, after OrganizationLoyaltySettings) LoyaltySettingsImpact { asPoints := func(s OrganizationLoyaltySettings) int64 { if s.Exchange.CoinAmount <= 0 { return 0 } return coins * s.Exchange.PointAmount / s.Exchange.CoinAmount } impact := LoyaltySettingsImpact{ OutstandingPoints: points, OutstandingCoins: coins, PointValueBefore: before.PointValue, PointValueAfter: after.PointValue, PointRupiahBefore: points * before.PointValue, PointRupiahAfter: points * after.PointValue, CoinsAsPointsBefore: asPoints(before), CoinsAsPointsAfter: asPoints(after), } impact.CoinRupiahBefore = impact.CoinsAsPointsBefore * before.PointValue impact.CoinRupiahAfter = impact.CoinsAsPointsAfter * after.PointValue return impact }