Reapply "feat(loyalty): EnakPoint & EnakCoin" (#32)
This reverts commit 4e24f9bbb0.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
)
|
||||
|
||||
// walletTraceMaxDepth bounds how far back a chain is followed. Every step is a
|
||||
// transfer, exchange or refund of the step before, so real chains are short; the
|
||||
// bound only guards against a loop in bad data.
|
||||
const walletTraceMaxDepth = 100
|
||||
|
||||
// WalletTraceProcessor traces a ledger row lot by lot for the dashboard
|
||||
// (docs/prd-point-coin.md F7, §8.1). It only reads.
|
||||
type WalletTraceProcessor struct {
|
||||
repo repository.WalletTraceRepository
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewWalletTraceProcessor(repo repository.WalletTraceRepository) *WalletTraceProcessor {
|
||||
return &WalletTraceProcessor{repo: repo, now: time.Now}
|
||||
}
|
||||
|
||||
type walletTraceStart struct {
|
||||
lotID uuid.UUID
|
||||
amount int64
|
||||
}
|
||||
|
||||
// Trace follows a row of the organization's ledger: a debit through the lots it took
|
||||
// from, a credit through the lots it created, and each lot back through origin_lot_id
|
||||
// to the lot that first brought the balance in. A row of another organization is
|
||||
// repository.ErrWalletTransactionNotFound.
|
||||
func (p *WalletTraceProcessor) Trace(ctx context.Context, organizationID, transactionID uuid.UUID) (*models.AdminWalletTrace, error) {
|
||||
row, err := p.repo.GetTransaction(ctx, organizationID, transactionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var starts []walletTraceStart
|
||||
if row.Amount < 0 {
|
||||
allocations, err := p.repo.ListAllocations(ctx, row.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, a := range allocations {
|
||||
starts = append(starts, walletTraceStart{lotID: a.LotID, amount: a.Amount})
|
||||
}
|
||||
} else {
|
||||
created, err := p.repo.ListLotsBySource(ctx, row.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, lot := range created {
|
||||
starts = append(starts, walletTraceStart{lotID: lot.ID, amount: lot.OriginalAmount})
|
||||
}
|
||||
}
|
||||
|
||||
lots, err := p.loadChains(ctx, starts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sourceIDs := make([]uuid.UUID, 0, len(lots))
|
||||
customerIDs := []uuid.UUID{row.CustomerID}
|
||||
for _, lot := range lots {
|
||||
sourceIDs = append(sourceIDs, lot.SourceTransactionID)
|
||||
customerIDs = append(customerIDs, lot.CustomerID)
|
||||
}
|
||||
sourceRows, err := p.repo.GetTransactions(ctx, uniqueIDs(sourceIDs))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sources := make(map[uuid.UUID]entities.WalletTransaction, len(sourceRows))
|
||||
for _, s := range sourceRows {
|
||||
sources[s.ID] = s
|
||||
}
|
||||
names, err := p.repo.CustomerNames(ctx, uniqueIDs(customerIDs))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := p.now()
|
||||
trace := &models.AdminWalletTrace{
|
||||
Transaction: walletTraceRow(*row, names),
|
||||
Lots: make([]models.AdminWalletTraceLot, 0, len(starts)),
|
||||
}
|
||||
for _, start := range starts {
|
||||
item := models.AdminWalletTraceLot{Amount: start.amount, Chain: []models.AdminWalletTraceStep{}}
|
||||
visited := map[uuid.UUID]bool{}
|
||||
for id := &start.lotID; id != nil && !visited[*id]; {
|
||||
lot, ok := lots[*id]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
visited[*id] = true
|
||||
item.Chain = append(item.Chain, models.AdminWalletTraceStep{
|
||||
Lot: adminWalletLot(lot, now),
|
||||
Source: walletTraceRow(sources[lot.SourceTransactionID], names),
|
||||
})
|
||||
id = lot.OriginLotID
|
||||
}
|
||||
trace.Lots = append(trace.Lots, item)
|
||||
}
|
||||
return trace, nil
|
||||
}
|
||||
|
||||
// loadChains loads the starting lots and every lot they were carried over from, one
|
||||
// generation at a time.
|
||||
func (p *WalletTraceProcessor) loadChains(ctx context.Context, starts []walletTraceStart) (map[uuid.UUID]entities.WalletLot, error) {
|
||||
lots := map[uuid.UUID]entities.WalletLot{}
|
||||
pending := make([]uuid.UUID, 0, len(starts))
|
||||
for _, s := range starts {
|
||||
pending = append(pending, s.lotID)
|
||||
}
|
||||
for depth := 0; len(pending) > 0 && depth < walletTraceMaxDepth; depth++ {
|
||||
found, err := p.repo.GetLots(ctx, uniqueIDs(pending))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pending = pending[:0]
|
||||
for _, lot := range found {
|
||||
lots[lot.ID] = lot
|
||||
}
|
||||
for _, lot := range found {
|
||||
if lot.OriginLotID != nil {
|
||||
if _, seen := lots[*lot.OriginLotID]; !seen {
|
||||
pending = append(pending, *lot.OriginLotID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return lots, nil
|
||||
}
|
||||
|
||||
func walletTraceRow(row entities.WalletTransaction, names map[uuid.UUID]string) models.AdminWalletTraceRow {
|
||||
return models.AdminWalletTraceRow{
|
||||
ID: row.ID,
|
||||
Customer: models.AdminWalletNamedRef{ID: row.CustomerID, Name: names[row.CustomerID]},
|
||||
Currency: row.Currency,
|
||||
Type: row.Type,
|
||||
Amount: row.Amount,
|
||||
Description: row.Description,
|
||||
ReferenceType: row.ReferenceType,
|
||||
ReferenceID: row.ReferenceID,
|
||||
OutletID: row.OutletID,
|
||||
CreatedAt: row.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func adminWalletLot(lot entities.WalletLot, now time.Time) models.AdminWalletLot {
|
||||
return models.AdminWalletLot{
|
||||
ID: lot.ID,
|
||||
Currency: lot.Currency,
|
||||
OriginalAmount: lot.OriginalAmount,
|
||||
RemainingAmount: lot.RemainingAmount,
|
||||
ExpiresAt: lot.ExpiresAt,
|
||||
Expired: lot.ExpiresAt != nil && !lot.ExpiresAt.After(now),
|
||||
SourceTransactionID: lot.SourceTransactionID,
|
||||
OriginLotID: lot.OriginLotID,
|
||||
CreatedAt: lot.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueIDs(ids []uuid.UUID) []uuid.UUID {
|
||||
seen := make(map[uuid.UUID]bool, len(ids))
|
||||
out := make([]uuid.UUID, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if !seen[id] {
|
||||
seen[id] = true
|
||||
out = append(out, id)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user