Revert "feat(loyalty): EnakPoint & EnakCoin" (#32)
This reverts merge commit645da30, returning main tof0ff59f. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
645da3048e
commit
4e24f9bbb0
@@ -1,170 +0,0 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
// Expiry rules of docs/prd-point-coin.md F12, with the model of note N4 decided: both
|
||||
// FIXED_DATE and ROLLING are supported and the owner picks one per currency.
|
||||
//
|
||||
// A lot expires at the last second of its expiry day in the customer's time zone, so
|
||||
// "expires on 31 Dec" means it can still be spent all of 31 Dec, and the day the apps
|
||||
// group it under is that day.
|
||||
|
||||
const (
|
||||
// A longer grace would push a balance past a whole cycle of fixed dates.
|
||||
loyaltyExpiryMaxGraceMonths = 24
|
||||
// A refund gives the EnakPoint back at least this long, even when the lot they
|
||||
// came from has expired or is about to (note N4, decided).
|
||||
refundMinimumValidity = 7 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// ExpirySettings picks the settings of one currency.
|
||||
func ExpirySettings(s *models.OrganizationLoyaltySettings, currency string) models.LoyaltyExpirySettings {
|
||||
if currency == constants.WalletCurrencyCoin {
|
||||
return s.CoinExpiry
|
||||
}
|
||||
return s.PointExpiry
|
||||
}
|
||||
|
||||
// ComputeExpiry is when a balance received at receivedAt expires, or nil when it never
|
||||
// does because expiry is off. It is the expiry of every lot that is not carried over
|
||||
// from another one: EARN, ADJUSTMENT and, capped by its origin, EXCHANGE_IN.
|
||||
func ComputeExpiry(s models.LoyaltyExpirySettings, receivedAt time.Time) *time.Time {
|
||||
if !s.Enabled {
|
||||
return nil
|
||||
}
|
||||
day := walletDay(receivedAt)
|
||||
if s.Mode == constants.LoyaltyExpiryModeRolling {
|
||||
if s.Unit == constants.LoyaltyExpiryUnitDay {
|
||||
day = day.AddDate(0, 0, int(s.Period))
|
||||
} else {
|
||||
day = addMonthsClamped(day, int(s.Period))
|
||||
}
|
||||
if s.EndOfMonth {
|
||||
day = lastDayOfMonth(day)
|
||||
}
|
||||
return endOfWalletDay(day)
|
||||
}
|
||||
return endOfWalletDay(fixedDateOnOrAfter(s.FixedDates, addMonthsClamped(day, int(s.GraceMonths))))
|
||||
}
|
||||
|
||||
// ActivationExpiry is the expiry given to the balances that already exist, without an
|
||||
// expiry, when expiry is turned on (note N4, decided): a full period from activation
|
||||
// when ROLLING, the second fixed date after activation when FIXED_DATE. Either way no
|
||||
// customer loses a balance soon after the rule is announced.
|
||||
func ActivationExpiry(s models.LoyaltyExpirySettings, at time.Time) *time.Time {
|
||||
if s.Mode == constants.LoyaltyExpiryModeRolling {
|
||||
s.Enabled = true
|
||||
return ComputeExpiry(s, at)
|
||||
}
|
||||
first := fixedDateOnOrAfter(s.FixedDates, walletDay(at))
|
||||
return endOfWalletDay(fixedDateOnOrAfter(s.FixedDates, first.AddDate(0, 0, 1)))
|
||||
}
|
||||
|
||||
// RefundExpiry is the expiry of EnakPoint given back by a refund: that of the lot they
|
||||
// came from, but at least seven days from now (note N4, decided). A lot that never
|
||||
// expires stays so.
|
||||
func RefundExpiry(origin *time.Time, now time.Time) *time.Time {
|
||||
if origin == nil {
|
||||
return nil
|
||||
}
|
||||
minimum := endOfWalletDay(walletDay(now.Add(refundMinimumValidity)))
|
||||
if origin.Before(*minimum) {
|
||||
return minimum
|
||||
}
|
||||
return origin
|
||||
}
|
||||
|
||||
// EarlierExpiry is the sooner of two expiries, where nil means never.
|
||||
func EarlierExpiry(a, b *time.Time) *time.Time {
|
||||
switch {
|
||||
case a == nil:
|
||||
return b
|
||||
case b == nil:
|
||||
return a
|
||||
case b.Before(*a):
|
||||
return b
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// normalizeDaysOfYear checks days written as MM-DD and returns them sorted without
|
||||
// duplicates. 29 February is refused, as it does not come every year.
|
||||
func normalizeDaysOfYear(days []string) ([]string, error) {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, d := range days {
|
||||
d = strings.TrimSpace(d)
|
||||
if d == "" {
|
||||
continue
|
||||
}
|
||||
parsed, err := time.Parse("01-02", d)
|
||||
if err != nil || parsed.Format("01-02") != d {
|
||||
return nil, fmt.Errorf("has %q, which is not a day of the year written as MM-DD", d)
|
||||
}
|
||||
if d == "02-29" {
|
||||
return nil, errors.New("cannot use 02-29, which is not in every year")
|
||||
}
|
||||
if !seen[d] {
|
||||
seen[d] = true
|
||||
out = append(out, d)
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, errors.New("must list at least one day of the year, as MM-DD")
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// fixedDateOnOrAfter is the first of the days of the year that falls on or after day.
|
||||
func fixedDateOnOrAfter(days []string, day time.Time) time.Time {
|
||||
days, err := normalizeDaysOfYear(days)
|
||||
if err != nil {
|
||||
days = []string{constants.LoyaltyExpiryFixedDatesDefault}
|
||||
}
|
||||
for year := day.Year(); ; year++ {
|
||||
for _, d := range days {
|
||||
parsed, _ := time.Parse("01-02", d)
|
||||
candidate := time.Date(year, parsed.Month(), parsed.Day(), 0, 0, 0, 0, walletDisplayLocation)
|
||||
if !candidate.Before(day) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// walletDay is the calendar day of t in the customer's time zone, at midnight.
|
||||
func walletDay(t time.Time) time.Time {
|
||||
local := t.In(walletDisplayLocation)
|
||||
return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, walletDisplayLocation)
|
||||
}
|
||||
|
||||
func endOfWalletDay(day time.Time) *time.Time {
|
||||
end := time.Date(day.Year(), day.Month(), day.Day(), 23, 59, 59, 0, walletDisplayLocation)
|
||||
return &end
|
||||
}
|
||||
|
||||
// addMonthsClamped adds months, keeping to the last day of a shorter month: 31 Jan
|
||||
// plus one month is 28 or 29 Feb, not 3 Mar.
|
||||
func addMonthsClamped(day time.Time, months int) time.Time {
|
||||
first := time.Date(day.Year(), day.Month(), 1, 0, 0, 0, 0, day.Location()).AddDate(0, months, 0)
|
||||
last := lastDayOfMonth(first).Day()
|
||||
d := day.Day()
|
||||
if d > last {
|
||||
d = last
|
||||
}
|
||||
return time.Date(first.Year(), first.Month(), d, 0, 0, 0, 0, day.Location())
|
||||
}
|
||||
|
||||
func lastDayOfMonth(day time.Time) time.Time {
|
||||
return time.Date(day.Year(), day.Month()+1, 0, 0, 0, 0, 0, day.Location())
|
||||
}
|
||||
Reference in New Issue
Block a user