feat(loyalty): give new balances their expiry
Every lot now gets its expiry when it is created (docs/prd-point-coin.md F12, PC-502), where it used to never expire until note N4 was settled: - EARN and an ADJUSTMENT that adds: ComputeExpiry of the organization's settings for that currency, from the moment received. - EXCHANGE_IN: the sooner of the EnakCoin lot's expiry and when EnakPoint received now expire (F4). - PAYMENT_REFUND: the expiry of the lot the EnakPoint came from, but at least seven days from the refund (N4, decided). A lot that never expired stays so. - TRANSFER_IN: unchanged, exactly the sender's expiry. Turning expiry on for a currency for the first time dates every lot of the organization that still holds something and has no expiry, MIGRATION lots included, in the same transaction as the setting: a full period from now when ROLLING, the second fixed date on or after today when FIXED_DATE, so no customer loses a balance soon after the rule is announced (N4, decided). Turning it off leaves dated lots as they are. PUT /marketing/loyalty-settings reports these as expiry_activations (currency, lots, amount, expires_at); a dry run counts them without dating anything. The earning processor now also reads the organization settings, and the wallet admin processor takes the settings reader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9ce55e6002
commit
4d63673a25
@@ -134,7 +134,7 @@ func (s *LoyaltySettingsServiceImpl) GetOrganizationSettings(ctx context.Context
|
||||
if err != nil {
|
||||
return loyaltyErrorResponse(err)
|
||||
}
|
||||
view, err := s.organizationView(ctx, apctx.OrganizationID, *current, *current, nil, false)
|
||||
view, err := s.organizationView(ctx, apctx.OrganizationID, *current, *current, nil, nil, false)
|
||||
if err != nil {
|
||||
return loyaltyErrorResponse(err)
|
||||
}
|
||||
@@ -145,10 +145,11 @@ func (s *LoyaltySettingsServiceImpl) GetOrganizationSettings(ctx context.Context
|
||||
// of the GET response, which are ignored.
|
||||
type organizationSettingsInput struct {
|
||||
*models.OrganizationLoyaltySettings
|
||||
Impact json.RawMessage `json:"impact"`
|
||||
ExpiryPreview json.RawMessage `json:"expiry_preview"`
|
||||
Changes json.RawMessage `json:"changes"`
|
||||
DryRun json.RawMessage `json:"dry_run"`
|
||||
Impact json.RawMessage `json:"impact"`
|
||||
ExpiryPreview json.RawMessage `json:"expiry_preview"`
|
||||
ExpiryActivations json.RawMessage `json:"expiry_activations"`
|
||||
Changes json.RawMessage `json:"changes"`
|
||||
DryRun json.RawMessage `json:"dry_run"`
|
||||
}
|
||||
|
||||
func (s *LoyaltySettingsServiceImpl) UpdateOrganizationSettings(ctx context.Context, apctx *appcontext.ContextInfo, body []byte, dryRun bool) *contract.Response {
|
||||
@@ -166,10 +167,11 @@ func (s *LoyaltySettingsServiceImpl) UpdateOrganizationSettings(ctx context.Cont
|
||||
}
|
||||
|
||||
var changes []models.LoyaltySettingChange
|
||||
var activations []models.LoyaltyExpiryActivation
|
||||
if dryRun {
|
||||
changes, err = s.settings.PreviewOrganization(ctx, apctx.OrganizationID, next)
|
||||
changes, activations, err = s.settings.PreviewOrganization(ctx, apctx.OrganizationID, next)
|
||||
} else {
|
||||
changes, err = s.settings.UpdateOrganization(ctx, apctx.OrganizationID, apctx.UserID, next)
|
||||
changes, activations, err = s.settings.UpdateOrganization(ctx, apctx.OrganizationID, apctx.UserID, next)
|
||||
}
|
||||
if err != nil {
|
||||
return loyaltyErrorResponse(err)
|
||||
@@ -182,7 +184,7 @@ func (s *LoyaltySettingsServiceImpl) UpdateOrganizationSettings(ctx context.Cont
|
||||
}
|
||||
after = *saved
|
||||
}
|
||||
view, err := s.organizationView(ctx, apctx.OrganizationID, *current, after, changes, dryRun)
|
||||
view, err := s.organizationView(ctx, apctx.OrganizationID, *current, after, changes, activations, dryRun)
|
||||
if err != nil {
|
||||
return loyaltyErrorResponse(err)
|
||||
}
|
||||
@@ -197,12 +199,15 @@ func (s *LoyaltySettingsServiceImpl) ListHistory(ctx context.Context, apctx *app
|
||||
return contract.BuildSuccessResponse(history)
|
||||
}
|
||||
|
||||
func (s *LoyaltySettingsServiceImpl) organizationView(ctx context.Context, organizationID uuid.UUID, before, after models.OrganizationLoyaltySettings, changes []models.LoyaltySettingChange, dryRun bool) (*models.OrganizationLoyaltySettingsView, error) {
|
||||
func (s *LoyaltySettingsServiceImpl) organizationView(ctx context.Context, organizationID uuid.UUID, before, after models.OrganizationLoyaltySettings, changes []models.LoyaltySettingChange, activations []models.LoyaltyExpiryActivation, dryRun bool) (*models.OrganizationLoyaltySettingsView, error) {
|
||||
now := time.Now()
|
||||
points, coins, err := s.outstanding.OrganizationOutstanding(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if activations == nil {
|
||||
activations = []models.LoyaltyExpiryActivation{}
|
||||
}
|
||||
if changes == nil {
|
||||
changes = []models.LoyaltySettingChange{}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user