feat(loyalty): organization loyalty settings API

Adds GET and PUT /marketing/loyalty-settings and GET
/marketing/loyalty-settings/history (docs/prd-point-coin.md F2, PC-302).

The settings are the point value, the exchange rate, transfer limits and
the stored expiry settings. PUT merges the body like the outlet settings
and is limited to loyalty managers. Every response carries the impact of
the change on the balances in circulation: outstanding EnakPoint and
EnakCoin, their rupiah value, and the coins exchanged into points, before
and after. With ?dry_run=true nothing is saved and the response lists the
keys that would change, for the warning shown before saving.

Saving records each change in loyalty_setting_changes with who made it;
history can be filtered to one outlet. Changing the value leaves what was
already written alone. The diff behind saving and previewing is shared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
efrilm
2026-09-30 11:26:16 +07:00
co-authored by Claude Opus 5.5
parent 8370851ed2
commit fe2f459b03
12 changed files with 485 additions and 24 deletions
@@ -49,6 +49,8 @@ type WalletQueryRepository interface {
NearestExpiring(ctx context.Context, customerID uuid.UUID, asOf time.Time) ([]WalletExpiringAmount, error)
// ListTransactions returns a page of the ledger, newest first, and the total count.
ListTransactions(ctx context.Context, filter WalletTransactionFilter) ([]entities.WalletTransaction, int64, error)
// OrganizationOutstanding sums every wallet balance of an organization.
OrganizationOutstanding(ctx context.Context, organizationID uuid.UUID) (points, coins int64, err error)
// ListLotsBySourceTransactions returns the lots created by the given credits.
ListLotsBySourceTransactions(ctx context.Context, transactionIDs []uuid.UUID) ([]entities.WalletLot, error)
}
@@ -168,3 +170,14 @@ func (r *walletQueryRepository) ListLotsBySourceTransactions(ctx context.Context
}
return lots, nil
}
func (r *walletQueryRepository) OrganizationOutstanding(ctx context.Context, organizationID uuid.UUID) (int64, int64, error) {
var totals struct{ Points, Coins int64 }
err := DBFromContext(ctx, r.db).WithContext(ctx).Raw(`
SELECT COALESCE(SUM(point_balance), 0) AS points, COALESCE(SUM(coin_balance), 0) AS coins
FROM customer_wallets WHERE organization_id = ?`, organizationID).Scan(&totals).Error
if err != nil {
return 0, 0, fmt.Errorf("failed to sum outstanding balances: %w", err)
}
return totals.Points, totals.Coins, nil
}