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,219 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
)
|
||||
|
||||
// ErrLoyaltyOutletNotFound means the outlet does not exist or belongs to another
|
||||
// organization.
|
||||
var ErrLoyaltyOutletNotFound = errors.New("loyalty settings: outlet not found")
|
||||
|
||||
// LoyaltySettingChangeFilter narrows the loyalty settings history.
|
||||
type LoyaltySettingChangeFilter struct {
|
||||
OrganizationID uuid.UUID
|
||||
// Nil for the whole organization, outlets included.
|
||||
OutletID *uuid.UUID
|
||||
Offset int
|
||||
Limit int
|
||||
}
|
||||
|
||||
// LoyaltySettingsRepository stores the raw loyalty settings (the loyalty.* keys of
|
||||
// outlet_settings and organization_settings) and their history
|
||||
// (docs/prd-point-coin.md F1, F2). The loyalty settings processor turns them into
|
||||
// typed values and is the only writer.
|
||||
type LoyaltySettingsRepository interface {
|
||||
// GetOutletValues returns the stored loyalty keys of an outlet. A key with no row,
|
||||
// or a NULL value, is absent.
|
||||
GetOutletValues(ctx context.Context, outletID uuid.UUID) (map[string]string, error)
|
||||
GetOrganizationValues(ctx context.Context, organizationID uuid.UUID) (map[string]string, error)
|
||||
// OutletOrganizationID returns ErrLoyaltyOutletNotFound for an unknown outlet.
|
||||
OutletOrganizationID(ctx context.Context, outletID uuid.UUID) (uuid.UUID, error)
|
||||
|
||||
// LockScope serializes writers of one outlet's or one organization's settings
|
||||
// until the transaction ends, so two saves at once cannot both compute their
|
||||
// changes from the same old values.
|
||||
LockScope(ctx context.Context, scopeID uuid.UUID) error
|
||||
// SetOutletValue stores a value, or removes the key when value is nil so it falls
|
||||
// back to its default.
|
||||
SetOutletValue(ctx context.Context, outletID uuid.UUID, key string, value *string) error
|
||||
SetOrganizationValue(ctx context.Context, organizationID uuid.UUID, key string, value *string) error
|
||||
InsertChanges(ctx context.Context, changes []entities.LoyaltySettingChange) error
|
||||
// ListChanges returns a page of history, newest first, and the total count.
|
||||
ListChanges(ctx context.Context, filter LoyaltySettingChangeFilter) ([]entities.LoyaltySettingChange, int64, error)
|
||||
|
||||
// UndatedBalance counts an organization's lots of one currency that still hold
|
||||
// something and have no expiry, and what they hold.
|
||||
UndatedBalance(ctx context.Context, organizationID uuid.UUID, currency string) (lots, amount int64, err error)
|
||||
// DateUndatedLots gives those lots an expiry, when expiry is turned on (F12).
|
||||
DateUndatedLots(ctx context.Context, organizationID uuid.UUID, currency string, expiresAt time.Time) (lots, amount int64, err error)
|
||||
}
|
||||
|
||||
type loyaltySettingsRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewLoyaltySettingsRepository(db *gorm.DB) LoyaltySettingsRepository {
|
||||
return &loyaltySettingsRepository{db: db}
|
||||
}
|
||||
|
||||
// requireTx returns the caller's transaction. Writes must run in one, together with
|
||||
// the history rows they produce.
|
||||
func (r *loyaltySettingsRepository) requireTx(ctx context.Context) (*gorm.DB, error) {
|
||||
if tx, ok := ctx.Value(txKey).(*gorm.DB); ok && tx != nil {
|
||||
return tx.WithContext(ctx), nil
|
||||
}
|
||||
return nil, errors.New("loyalty settings: write must run inside a transaction")
|
||||
}
|
||||
|
||||
func (r *loyaltySettingsRepository) values(ctx context.Context, table, scopeColumn string, scopeID uuid.UUID) (map[string]string, error) {
|
||||
var rows []struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
err := DBFromContext(ctx, r.db).WithContext(ctx).
|
||||
Table(table).
|
||||
Select("key, value").
|
||||
Where(scopeColumn+" = ? AND key LIKE 'loyalty.%' AND value IS NOT NULL", scopeID).
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %s: %w", table, err)
|
||||
}
|
||||
values := make(map[string]string, len(rows))
|
||||
for _, row := range rows {
|
||||
values[row.Key] = row.Value
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (r *loyaltySettingsRepository) GetOutletValues(ctx context.Context, outletID uuid.UUID) (map[string]string, error) {
|
||||
return r.values(ctx, "outlet_settings", "outlet_id", outletID)
|
||||
}
|
||||
|
||||
func (r *loyaltySettingsRepository) GetOrganizationValues(ctx context.Context, organizationID uuid.UUID) (map[string]string, error) {
|
||||
return r.values(ctx, "organization_settings", "organization_id", organizationID)
|
||||
}
|
||||
|
||||
func (r *loyaltySettingsRepository) OutletOrganizationID(ctx context.Context, outletID uuid.UUID) (uuid.UUID, error) {
|
||||
var ids []string
|
||||
err := DBFromContext(ctx, r.db).WithContext(ctx).
|
||||
Table("outlets").
|
||||
Where("id = ?", outletID).
|
||||
Limit(1).
|
||||
Pluck("organization_id::text", &ids).Error
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("failed to get outlet organization: %w", err)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return uuid.Nil, ErrLoyaltyOutletNotFound
|
||||
}
|
||||
return uuid.Parse(ids[0])
|
||||
}
|
||||
|
||||
func (r *loyaltySettingsRepository) LockScope(ctx context.Context, scopeID uuid.UUID) error {
|
||||
db, err := r.requireTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Exec(`SELECT pg_advisory_xact_lock(hashtextextended(?, 0))`, "loyalty-settings:"+scopeID.String()).Error
|
||||
}
|
||||
|
||||
func (r *loyaltySettingsRepository) SetOutletValue(ctx context.Context, outletID uuid.UUID, key string, value *string) error {
|
||||
return r.setValue(ctx, "outlet_settings", "outlet_id", outletID, key, value)
|
||||
}
|
||||
|
||||
func (r *loyaltySettingsRepository) SetOrganizationValue(ctx context.Context, organizationID uuid.UUID, key string, value *string) error {
|
||||
return r.setValue(ctx, "organization_settings", "organization_id", organizationID, key, value)
|
||||
}
|
||||
|
||||
// setValue upserts or deletes one key. table and scopeColumn are never user input.
|
||||
func (r *loyaltySettingsRepository) setValue(ctx context.Context, table, scopeColumn string, scopeID uuid.UUID, key string, value *string) error {
|
||||
db, err := r.requireTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value == nil {
|
||||
err = db.Exec(`DELETE FROM `+table+` WHERE `+scopeColumn+` = ? AND key = ?`, scopeID, key).Error
|
||||
} else {
|
||||
err = db.Exec(`INSERT INTO `+table+` (`+scopeColumn+`, key, value) VALUES (?, ?, ?)
|
||||
ON CONFLICT (`+scopeColumn+`, key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()`,
|
||||
scopeID, key, *value).Error
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to store %s in %s: %w", key, table, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *loyaltySettingsRepository) InsertChanges(ctx context.Context, changes []entities.LoyaltySettingChange) error {
|
||||
if len(changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
db, err := r.requireTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Create(&changes).Error
|
||||
}
|
||||
|
||||
func (r *loyaltySettingsRepository) ListChanges(ctx context.Context, filter LoyaltySettingChangeFilter) ([]entities.LoyaltySettingChange, int64, error) {
|
||||
query := DBFromContext(ctx, r.db).WithContext(ctx).
|
||||
Model(&entities.LoyaltySettingChange{}).
|
||||
Where("organization_id = ?", filter.OrganizationID)
|
||||
if filter.OutletID != nil {
|
||||
query = query.Where("outlet_id = ?", *filter.OutletID)
|
||||
}
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count loyalty setting changes: %w", err)
|
||||
}
|
||||
var changes []entities.LoyaltySettingChange
|
||||
err := query.Order("created_at DESC, id").Offset(filter.Offset).Limit(filter.Limit).Find(&changes).Error
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list loyalty setting changes: %w", err)
|
||||
}
|
||||
return changes, total, nil
|
||||
}
|
||||
|
||||
func (r *loyaltySettingsRepository) UndatedBalance(ctx context.Context, organizationID uuid.UUID, currency string) (int64, int64, error) {
|
||||
var row struct{ Lots, Amount int64 }
|
||||
err := DBFromContext(ctx, r.db).WithContext(ctx).Raw(`
|
||||
SELECT COUNT(*) AS lots, COALESCE(SUM(remaining_amount), 0) AS amount
|
||||
FROM wallet_lots
|
||||
WHERE organization_id = ? AND currency = ? AND expires_at IS NULL AND remaining_amount > 0`,
|
||||
organizationID, currency).Scan(&row).Error
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to sum undated wallet lots: %w", err)
|
||||
}
|
||||
return row.Lots, row.Amount, nil
|
||||
}
|
||||
|
||||
func (r *loyaltySettingsRepository) DateUndatedLots(ctx context.Context, organizationID uuid.UUID, currency string, expiresAt time.Time) (int64, int64, error) {
|
||||
db, err := r.requireTx(ctx)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
var rows []struct{ RemainingAmount int64 }
|
||||
// Only lots that still hold something: an empty lot never expires anything, and
|
||||
// leaving it undated keeps the history as it was.
|
||||
err = db.Raw(`
|
||||
UPDATE wallet_lots SET expires_at = ?
|
||||
WHERE organization_id = ? AND currency = ? AND expires_at IS NULL AND remaining_amount > 0
|
||||
RETURNING remaining_amount`,
|
||||
expiresAt, organizationID, currency).Scan(&rows).Error
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to date wallet lots: %w", err)
|
||||
}
|
||||
var amount int64
|
||||
for _, row := range rows {
|
||||
amount += row.RemainingAmount
|
||||
}
|
||||
return int64(len(rows)), amount, nil
|
||||
}
|
||||
Reference in New Issue
Block a user