Settles note N4 of docs/prd-point-coin.md: both expiry models are
supported, chosen per currency by the owner, defaulting to one fixed date a
year (PC-501, F12).
New organization keys, per currency (loyalty.point.* / loyalty.coin.*):
- expiry_mode: FIXED_DATE (default) or ROLLING.
- expiry_fixed_dates: the days of the year balances expire on, as sorted
MM-DD values ("12-31" by default, "06-30,12-31" for twice a year). 29 Feb
is refused.
- expiry_grace_months: 0 to 24, default 3. A balance lasts at least this
long before a fixed date takes it.
The existing period, unit and end_of_month keys now belong to ROLLING, and
reminder_days to both.
ComputeExpiry gives the expiry of a balance received at a time: the first
fixed date on or after the day received plus the grace months, or the day
received plus the period (to the end of that month when asked). Days are
the customer's (WIB), a shorter month keeps to its last day, and a lot
lasts to 23:59:59 of its day so the apps group it under that day. Nil when
expiry is off. ActivationExpiry, RefundExpiry and EarlierExpiry hold the
other decided rules and are used by PC-502.
GET and PUT /marketing/loyalty-settings return expiry_preview: when a
balance received now would expire, for the dashboard's "received today
expires on ..." hint, also on a dry run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
487 lines
18 KiB
Go
487 lines
18 KiB
Go
package processor
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"apskel-pos-be/internal/constants"
|
|
"apskel-pos-be/internal/entities"
|
|
"apskel-pos-be/internal/logger"
|
|
"apskel-pos-be/internal/models"
|
|
"apskel-pos-be/internal/repository"
|
|
)
|
|
|
|
// ErrInvalidLoyaltySettings wraps every rejection of a setting that breaks the rules in
|
|
// docs/prd-point-coin.md F1, F2 and F12.
|
|
var ErrInvalidLoyaltySettings = errors.New("invalid loyalty settings")
|
|
|
|
// LoyaltySettingsProcessor reads and writes the loyalty settings of outlets (F1) and
|
|
// organizations (F2, F12). Reads always return every setting, with the PRD default for
|
|
// a key that was never set, so callers never handle a missing value. Every write
|
|
// records what changed in loyalty_setting_changes.
|
|
type LoyaltySettingsProcessor struct {
|
|
repo repository.LoyaltySettingsRepository
|
|
tx TxRunner
|
|
}
|
|
|
|
func NewLoyaltySettingsProcessor(repo repository.LoyaltySettingsRepository, tx TxRunner) *LoyaltySettingsProcessor {
|
|
return &LoyaltySettingsProcessor{repo: repo, tx: tx}
|
|
}
|
|
|
|
// Outlet returns an outlet's settings.
|
|
func (p *LoyaltySettingsProcessor) Outlet(ctx context.Context, outletID uuid.UUID) (*models.OutletLoyaltySettings, error) {
|
|
raw, err := p.repo.GetOutletValues(ctx, outletID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var s models.OutletLoyaltySettings
|
|
loadLoyaltyFields(outletLoyaltyFields(&s), raw, "outlet "+outletID.String())
|
|
return &s, nil
|
|
}
|
|
|
|
// OutletForOrganization returns an outlet's settings for the dashboard, and
|
|
// repository.ErrLoyaltyOutletNotFound unless the outlet belongs to the organization.
|
|
func (p *LoyaltySettingsProcessor) OutletForOrganization(ctx context.Context, organizationID, outletID uuid.UUID) (*models.OutletLoyaltySettings, error) {
|
|
owner, err := p.repo.OutletOrganizationID(ctx, outletID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if owner != organizationID {
|
|
return nil, repository.ErrLoyaltyOutletNotFound
|
|
}
|
|
return p.Outlet(ctx, outletID)
|
|
}
|
|
|
|
// Organization returns an organization's settings.
|
|
func (p *LoyaltySettingsProcessor) Organization(ctx context.Context, organizationID uuid.UUID) (*models.OrganizationLoyaltySettings, error) {
|
|
raw, err := p.repo.GetOrganizationValues(ctx, organizationID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var s models.OrganizationLoyaltySettings
|
|
loadLoyaltyFields(organizationLoyaltyFields(&s), raw, "organization "+organizationID.String())
|
|
return &s, nil
|
|
}
|
|
|
|
// PointValue is the rupiah value of one EnakPoint in an organization.
|
|
func (p *LoyaltySettingsProcessor) PointValue(ctx context.Context, organizationID uuid.UUID) (int64, error) {
|
|
s, err := p.Organization(ctx, organizationID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return s.PointValue, nil
|
|
}
|
|
|
|
// UpdateOutlet replaces an outlet's settings and returns what changed. The outlet must
|
|
// belong to the organization; otherwise repository.ErrLoyaltyOutletNotFound.
|
|
func (p *LoyaltySettingsProcessor) UpdateOutlet(ctx context.Context, organizationID, outletID, changedBy uuid.UUID, settings models.OutletLoyaltySettings) ([]models.LoyaltySettingChange, error) {
|
|
if err := validateLoyaltyFields(outletLoyaltyFields(&settings)); err != nil {
|
|
return nil, err
|
|
}
|
|
var changes []models.LoyaltySettingChange
|
|
err := p.tx.WithTransaction(ctx, func(ctx context.Context) error {
|
|
owner, err := p.repo.OutletOrganizationID(ctx, outletID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if owner != organizationID {
|
|
return repository.ErrLoyaltyOutletNotFound
|
|
}
|
|
if err := p.repo.LockScope(ctx, outletID); err != nil {
|
|
return err
|
|
}
|
|
raw, err := p.repo.GetOutletValues(ctx, outletID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var current models.OutletLoyaltySettings
|
|
loadLoyaltyFields(outletLoyaltyFields(¤t), raw, "outlet "+outletID.String())
|
|
|
|
changes, err = p.apply(ctx, organizationID, &outletID, changedBy, raw,
|
|
outletLoyaltyFields(¤t), outletLoyaltyFields(&settings),
|
|
func(key string, value *string) error { return p.repo.SetOutletValue(ctx, outletID, key, value) })
|
|
return err
|
|
})
|
|
return changes, err
|
|
}
|
|
|
|
// UpdateOrganization replaces an organization's settings and returns what changed.
|
|
func (p *LoyaltySettingsProcessor) UpdateOrganization(ctx context.Context, organizationID, changedBy uuid.UUID, settings models.OrganizationLoyaltySettings) ([]models.LoyaltySettingChange, error) {
|
|
if err := validateLoyaltyFields(organizationLoyaltyFields(&settings)); err != nil {
|
|
return nil, err
|
|
}
|
|
var changes []models.LoyaltySettingChange
|
|
err := p.tx.WithTransaction(ctx, func(ctx context.Context) error {
|
|
if err := p.repo.LockScope(ctx, organizationID); err != nil {
|
|
return err
|
|
}
|
|
raw, err := p.repo.GetOrganizationValues(ctx, organizationID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var current models.OrganizationLoyaltySettings
|
|
loadLoyaltyFields(organizationLoyaltyFields(¤t), raw, "organization "+organizationID.String())
|
|
|
|
changes, err = p.apply(ctx, organizationID, nil, changedBy, raw,
|
|
organizationLoyaltyFields(¤t), organizationLoyaltyFields(&settings),
|
|
func(key string, value *string) error {
|
|
return p.repo.SetOrganizationValue(ctx, organizationID, key, value)
|
|
})
|
|
return err
|
|
})
|
|
return changes, err
|
|
}
|
|
|
|
// apply stores every field whose effective value changes and records it. A key left
|
|
// on its default and not changed is not written, so the history only shows real
|
|
// changes. current and next list the same keys in the same order.
|
|
func (p *LoyaltySettingsProcessor) apply(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, changedBy uuid.UUID, raw map[string]string, current, next []loyaltyField, store func(key string, value *string) error) ([]models.LoyaltySettingChange, error) {
|
|
rows := diffLoyaltyFields(organizationID, outletID, changedBy, raw, current, next)
|
|
for _, row := range rows {
|
|
if err := store(row.Key, row.NewValue); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := p.repo.InsertChanges(ctx, rows); err != nil {
|
|
return nil, err
|
|
}
|
|
changes := make([]models.LoyaltySettingChange, 0, len(rows))
|
|
for _, row := range rows {
|
|
changes = append(changes, loyaltySettingChangeModel(row))
|
|
}
|
|
return changes, nil
|
|
}
|
|
|
|
// ListChanges returns a page of an organization's settings history, newest first.
|
|
// A nil outlet lists the whole organization, outlets included.
|
|
func (p *LoyaltySettingsProcessor) ListChanges(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int) (*models.PaginatedResponse[models.LoyaltySettingChange], error) {
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if limit < 1 || limit > 100 {
|
|
limit = 20
|
|
}
|
|
rows, total, err := p.repo.ListChanges(ctx, repository.LoyaltySettingChangeFilter{
|
|
OrganizationID: organizationID, OutletID: outletID, Offset: (page - 1) * limit, Limit: limit,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]models.LoyaltySettingChange, 0, len(rows))
|
|
for _, row := range rows {
|
|
items = append(items, loyaltySettingChangeModel(row))
|
|
}
|
|
return &models.PaginatedResponse[models.LoyaltySettingChange]{
|
|
Data: items,
|
|
Pagination: models.Pagination{
|
|
Page: page, Limit: limit, Total: total, TotalPages: int((total + int64(limit) - 1) / int64(limit)),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func loyaltySettingChangeModel(row entities.LoyaltySettingChange) models.LoyaltySettingChange {
|
|
return models.LoyaltySettingChange{
|
|
ID: row.ID,
|
|
OrganizationID: row.OrganizationID,
|
|
OutletID: row.OutletID,
|
|
Key: row.Key,
|
|
OldValue: row.OldValue,
|
|
NewValue: row.NewValue,
|
|
ChangedBy: row.ChangedBy,
|
|
CreatedAt: row.CreatedAt,
|
|
}
|
|
}
|
|
|
|
// outletLoyaltyFields describes every outlet key (F1), bound to s.
|
|
func outletLoyaltyFields(s *models.OutletLoyaltySettings) []loyaltyField {
|
|
return []loyaltyField{
|
|
boolLoyaltyField(constants.LoyaltyPointEnabledKey, &s.Point.Enabled, false),
|
|
intLoyaltyField(constants.LoyaltyPointEarnPerAmountKey, &s.Point.EarnPerAmount, constants.LoyaltyPointEarnPerAmountDefault, 1, noLoyaltyMax),
|
|
intLoyaltyField(constants.LoyaltyPointEarnValueKey, &s.Point.EarnValue, constants.LoyaltyEarnValueDefault, 0, noLoyaltyMax),
|
|
intLoyaltyField(constants.LoyaltyPointMinOrderAmountKey, &s.Point.MinOrderAmount, 0, 0, noLoyaltyMax),
|
|
optionalIntLoyaltyField(constants.LoyaltyPointMaxPerOrderKey, &s.Point.MaxPerOrder, 0),
|
|
|
|
boolLoyaltyField(constants.LoyaltyCoinEnabledKey, &s.Coin.Enabled, false),
|
|
intLoyaltyField(constants.LoyaltyCoinEarnPerAmountKey, &s.Coin.EarnPerAmount, constants.LoyaltyCoinEarnPerAmountDefault, 1, noLoyaltyMax),
|
|
intLoyaltyField(constants.LoyaltyCoinEarnValueKey, &s.Coin.EarnValue, constants.LoyaltyEarnValueDefault, 0, noLoyaltyMax),
|
|
intLoyaltyField(constants.LoyaltyCoinMinOrderAmountKey, &s.Coin.MinOrderAmount, 0, 0, noLoyaltyMax),
|
|
optionalIntLoyaltyField(constants.LoyaltyCoinMaxPerOrderKey, &s.Coin.MaxPerOrder, 0),
|
|
|
|
boolLoyaltyField(constants.LoyaltyPointAcceptPaymentKey, &s.PointPayment.AcceptPayment, false),
|
|
intLoyaltyField(constants.LoyaltyPointMinPaymentPointsKey, &s.PointPayment.MinPaymentPoints, constants.LoyaltyMinPaymentPointsDefault, 1, noLoyaltyMax),
|
|
intLoyaltyField(constants.LoyaltyPointMaxPaymentPercentKey, &s.PointPayment.MaxPaymentPercent, constants.LoyaltyMaxPaymentPercentDefault, 0, 100),
|
|
}
|
|
}
|
|
|
|
// organizationLoyaltyFields describes every organization key (F2, F12), bound to s.
|
|
func organizationLoyaltyFields(s *models.OrganizationLoyaltySettings) []loyaltyField {
|
|
fields := []loyaltyField{
|
|
intLoyaltyField(constants.LoyaltyPointValueKey, &s.PointValue, constants.LoyaltyPointValueDefault, 1, noLoyaltyMax),
|
|
intLoyaltyField(constants.LoyaltyExchangeCoinAmountKey, &s.Exchange.CoinAmount, constants.LoyaltyExchangeAmountDefault, 1, noLoyaltyMax),
|
|
intLoyaltyField(constants.LoyaltyExchangePointAmountKey, &s.Exchange.PointAmount, constants.LoyaltyExchangeAmountDefault, 1, noLoyaltyMax),
|
|
boolLoyaltyField(constants.LoyaltyTransferEnabledKey, &s.Transfer.Enabled, constants.LoyaltyTransferEnabledDefault),
|
|
intLoyaltyField(constants.LoyaltyTransferMinAmountKey, &s.Transfer.MinAmount, constants.LoyaltyTransferMinAmountDefault, 1, noLoyaltyMax),
|
|
optionalIntLoyaltyField(constants.LoyaltyTransferMaxPerTransactionKey, &s.Transfer.MaxPerTransaction, 1),
|
|
optionalIntLoyaltyField(constants.LoyaltyTransferDailyLimitKey, &s.Transfer.DailyLimit, 1),
|
|
}
|
|
fields = append(fields, expiryLoyaltyFields("loyalty.point.", &s.PointExpiry)...)
|
|
return append(fields, expiryLoyaltyFields("loyalty.coin.", &s.CoinExpiry)...)
|
|
}
|
|
|
|
func expiryLoyaltyFields(prefix string, s *models.LoyaltyExpirySettings) []loyaltyField {
|
|
return []loyaltyField{
|
|
boolLoyaltyField(prefix+constants.LoyaltyExpiryEnabledSuffix, &s.Enabled, false),
|
|
enumLoyaltyField(prefix+constants.LoyaltyExpiryModeSuffix, &s.Mode, constants.LoyaltyExpiryModeDefault,
|
|
constants.LoyaltyExpiryModeFixedDate, constants.LoyaltyExpiryModeRolling),
|
|
dayOfYearListLoyaltyField(prefix+constants.LoyaltyExpiryFixedDatesSuffix, &s.FixedDates, constants.LoyaltyExpiryFixedDatesDefault),
|
|
intLoyaltyField(prefix+constants.LoyaltyExpiryGraceMonthsSuffix, &s.GraceMonths, constants.LoyaltyExpiryGraceMonthsDefault, 0, loyaltyExpiryMaxGraceMonths),
|
|
intLoyaltyField(prefix+constants.LoyaltyExpiryPeriodSuffix, &s.Period, constants.LoyaltyExpiryPeriodDefault, 1, noLoyaltyMax),
|
|
enumLoyaltyField(prefix+constants.LoyaltyExpiryUnitSuffix, &s.Unit, constants.LoyaltyExpiryUnitDefault,
|
|
constants.LoyaltyExpiryUnitDay, constants.LoyaltyExpiryUnitMonth),
|
|
boolLoyaltyField(prefix+constants.LoyaltyExpiryEndOfMonthSuffix, &s.EndOfMonth, false),
|
|
intLoyaltyField(prefix+constants.LoyaltyExpiryReminderDaysSuffix, &s.ReminderDays, constants.LoyaltyExpiryReminderDaysDefault, 0, noLoyaltyMax),
|
|
}
|
|
}
|
|
|
|
// loyaltyField is one setting key bound to the struct field that holds it. The same
|
|
// description drives reading (parse or default), validating, and writing (format).
|
|
type loyaltyField struct {
|
|
key string
|
|
// parse sets the field from a stored value, or reports it unusable.
|
|
parse func(raw string) bool
|
|
// reset sets the field to its default.
|
|
reset func()
|
|
// validate checks the field's current value.
|
|
validate func() error
|
|
// format is the field's current value as stored; nil means no stored value.
|
|
format func() *string
|
|
}
|
|
|
|
const noLoyaltyMax = int64(-1)
|
|
|
|
func intLoyaltyField(key string, v *int64, def, min, max int64) loyaltyField {
|
|
check := func(n int64) error {
|
|
if n < min {
|
|
return fmt.Errorf("%w: %s must be at least %d", ErrInvalidLoyaltySettings, key, min)
|
|
}
|
|
if max != noLoyaltyMax && n > max {
|
|
return fmt.Errorf("%w: %s must be at most %d", ErrInvalidLoyaltySettings, key, max)
|
|
}
|
|
return nil
|
|
}
|
|
return loyaltyField{
|
|
key: key,
|
|
parse: func(raw string) bool {
|
|
n, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
|
|
if err != nil || check(n) != nil {
|
|
return false
|
|
}
|
|
*v = n
|
|
return true
|
|
},
|
|
reset: func() { *v = def },
|
|
validate: func() error { return check(*v) },
|
|
format: func() *string { s := strconv.FormatInt(*v, 10); return &s },
|
|
}
|
|
}
|
|
|
|
// optionalIntLoyaltyField is a limit that may be unset, meaning no limit.
|
|
func optionalIntLoyaltyField(key string, v **int64, min int64) loyaltyField {
|
|
return loyaltyField{
|
|
key: key,
|
|
parse: func(raw string) bool {
|
|
n, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
|
|
if err != nil || n < min {
|
|
return false
|
|
}
|
|
*v = &n
|
|
return true
|
|
},
|
|
reset: func() { *v = nil },
|
|
validate: func() error {
|
|
if *v != nil && **v < min {
|
|
return fmt.Errorf("%w: %s must be at least %d, or empty for no limit", ErrInvalidLoyaltySettings, key, min)
|
|
}
|
|
return nil
|
|
},
|
|
format: func() *string {
|
|
if *v == nil {
|
|
return nil
|
|
}
|
|
s := strconv.FormatInt(**v, 10)
|
|
return &s
|
|
},
|
|
}
|
|
}
|
|
|
|
func boolLoyaltyField(key string, v *bool, def bool) loyaltyField {
|
|
return loyaltyField{
|
|
key: key,
|
|
parse: func(raw string) bool {
|
|
b, err := strconv.ParseBool(strings.TrimSpace(raw))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
*v = b
|
|
return true
|
|
},
|
|
reset: func() { *v = def },
|
|
validate: func() error { return nil },
|
|
format: func() *string { s := strconv.FormatBool(*v); return &s },
|
|
}
|
|
}
|
|
|
|
func enumLoyaltyField(key string, v *string, def string, allowed ...string) loyaltyField {
|
|
valid := func(s string) bool {
|
|
for _, a := range allowed {
|
|
if s == a {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
return loyaltyField{
|
|
key: key,
|
|
parse: func(raw string) bool {
|
|
s := strings.ToUpper(strings.TrimSpace(raw))
|
|
if !valid(s) {
|
|
return false
|
|
}
|
|
*v = s
|
|
return true
|
|
},
|
|
reset: func() { *v = def },
|
|
validate: func() error {
|
|
if !valid(*v) {
|
|
return fmt.Errorf("%w: %s must be one of %s", ErrInvalidLoyaltySettings, key, strings.Join(allowed, ", "))
|
|
}
|
|
return nil
|
|
},
|
|
format: func() *string { s := *v; return &s },
|
|
}
|
|
}
|
|
|
|
// dayOfYearListLoyaltyField is a non-empty set of days of the year, stored as sorted
|
|
// MM-DD values joined by commas, such as "06-30,12-31".
|
|
func dayOfYearListLoyaltyField(key string, v *[]string, def string) loyaltyField {
|
|
normalize := func(days []string) ([]string, error) {
|
|
out, err := normalizeDaysOfYear(days)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %s %v", ErrInvalidLoyaltySettings, key, err)
|
|
}
|
|
return out, nil
|
|
}
|
|
return loyaltyField{
|
|
key: key,
|
|
parse: func(raw string) bool {
|
|
days, err := normalize(strings.Split(raw, ","))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
*v = days
|
|
return true
|
|
},
|
|
reset: func() { *v, _ = normalizeDaysOfYear(strings.Split(def, ",")) },
|
|
validate: func() error {
|
|
days, err := normalize(*v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*v = days
|
|
return nil
|
|
},
|
|
format: func() *string {
|
|
days, err := normalizeDaysOfYear(*v)
|
|
if err != nil {
|
|
days = *v
|
|
}
|
|
s := strings.Join(days, ",")
|
|
return &s
|
|
},
|
|
}
|
|
}
|
|
|
|
// loadLoyaltyFields fills each field from its stored value, or its default when there
|
|
// is none. A stored value that cannot be used also falls back to the default, so a bad
|
|
// row can never reach a calculation, and it is logged so it gets fixed.
|
|
func loadLoyaltyFields(fields []loyaltyField, raw map[string]string, scope string) {
|
|
for _, f := range fields {
|
|
f.reset()
|
|
value, ok := raw[f.key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if !f.parse(value) {
|
|
f.reset()
|
|
logger.NonContext.Warnf("Loyalty setting %s of %s has an unusable value %q; using the default", f.key, scope, value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func validateLoyaltyFields(fields []loyaltyField) error {
|
|
for _, f := range fields {
|
|
if err := f.validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func equalOptionalStrings(a, b *string) bool {
|
|
if a == nil || b == nil {
|
|
return a == nil && b == nil
|
|
}
|
|
return *a == *b
|
|
}
|
|
|
|
// diffLoyaltyFields lists the keys whose effective value differs between current and
|
|
// next, with the stored old value (nil when the key was on its default). current and
|
|
// next list the same keys in the same order.
|
|
func diffLoyaltyFields(organizationID uuid.UUID, outletID *uuid.UUID, changedBy uuid.UUID, raw map[string]string, current, next []loyaltyField) []entities.LoyaltySettingChange {
|
|
var rows []entities.LoyaltySettingChange
|
|
for i, field := range next {
|
|
newValue := field.format()
|
|
if equalOptionalStrings(current[i].format(), newValue) {
|
|
continue
|
|
}
|
|
var oldValue *string
|
|
if v, ok := raw[field.key]; ok {
|
|
oldValue = &v
|
|
}
|
|
rows = append(rows, entities.LoyaltySettingChange{
|
|
OrganizationID: organizationID,
|
|
OutletID: outletID,
|
|
Key: field.key,
|
|
OldValue: oldValue,
|
|
NewValue: newValue,
|
|
ChangedBy: changedBy,
|
|
})
|
|
}
|
|
return rows
|
|
}
|
|
|
|
// PreviewOrganization validates new organization settings and lists what saving them
|
|
// would change, without writing anything.
|
|
func (p *LoyaltySettingsProcessor) PreviewOrganization(ctx context.Context, organizationID uuid.UUID, settings models.OrganizationLoyaltySettings) ([]models.LoyaltySettingChange, error) {
|
|
if err := validateLoyaltyFields(organizationLoyaltyFields(&settings)); err != nil {
|
|
return nil, err
|
|
}
|
|
raw, err := p.repo.GetOrganizationValues(ctx, organizationID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var current models.OrganizationLoyaltySettings
|
|
loadLoyaltyFields(organizationLoyaltyFields(¤t), raw, "organization "+organizationID.String())
|
|
rows := diffLoyaltyFields(organizationID, nil, uuid.Nil, raw, organizationLoyaltyFields(¤t), organizationLoyaltyFields(&settings))
|
|
changes := make([]models.LoyaltySettingChange, 0, len(rows))
|
|
for _, row := range rows {
|
|
changes = append(changes, loyaltySettingChangeModel(row))
|
|
}
|
|
return changes, nil
|
|
}
|