213 lines
8.5 KiB
Go
213 lines
8.5 KiB
Go
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"`
|
||
// 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, 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"`
|
||
}
|
||
|
||
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 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
|
||
}
|