194 lines
7.6 KiB
Go
194 lines
7.6 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 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. 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"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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
|
|||
|
|
}
|