Adds cmd/wallet-migrate (make wallet-migrate, args=-dry-run to only report), which moves customer_points and customer_tokens into the wallet (docs/prd-point-coin.md §10, PC-105). Each customer gets a MIGRATION ledger row and a non-expiring lot per currency, written through WalletProcessor in one transaction per customer. EnakCoin is the sum of every token type (Q6), with the legacy rows listed in the row's metadata. It credits the difference between the legacy balance and what earlier runs migrated, so running it again never doubles a balance and picks up only what the old code added since. A legacy balance that shrank after being migrated is reported and left alone, since only an admin adjustment may take balance away, and the command then exits non-zero. It ends with a legacy / migrated / wallet total per currency. Migration 000092 renames TOKENS to COINS in campaigns.type and campaign_rules.reward_type. The campaign API now validates COINS; it still accepts TOKENS, including as a list filter, and stores it as COINS so older dashboards keep working while they are updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
153 lines
5.0 KiB
Go
153 lines
5.0 KiB
Go
package entities
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type CampaignType string
|
|
|
|
const (
|
|
CampaignTypeReward CampaignType = "REWARD"
|
|
CampaignTypePoints CampaignType = "POINTS"
|
|
CampaignTypeCoins CampaignType = "COINS"
|
|
CampaignTypeMixed CampaignType = "MIXED"
|
|
)
|
|
|
|
// legacyCampaignTokens is what COINS was called before tokens became EnakCoin
|
|
// (docs/prd-point-coin.md §10). Requests may still send it; it is stored as COINS.
|
|
const legacyCampaignTokens = "TOKENS"
|
|
|
|
// NormalizeCampaignType maps the legacy TOKENS to COINS and leaves anything else as is.
|
|
func NormalizeCampaignType(t string) CampaignType {
|
|
if t == legacyCampaignTokens {
|
|
return CampaignTypeCoins
|
|
}
|
|
return CampaignType(t)
|
|
}
|
|
|
|
// NormalizeCampaignRewardType maps the legacy TOKENS to COINS and leaves anything
|
|
// else as is.
|
|
func NormalizeCampaignRewardType(t string) CampaignRewardType {
|
|
if t == legacyCampaignTokens {
|
|
return CampaignRewardTypeCoins
|
|
}
|
|
return CampaignRewardType(t)
|
|
}
|
|
|
|
type RuleType string
|
|
|
|
const (
|
|
RuleTypeTier RuleType = "TIER"
|
|
RuleTypeSpend RuleType = "SPEND"
|
|
RuleTypeProduct RuleType = "PRODUCT"
|
|
RuleTypeCategory RuleType = "CATEGORY"
|
|
RuleTypeDay RuleType = "DAY"
|
|
RuleTypeLocation RuleType = "LOCATION"
|
|
)
|
|
|
|
type CampaignRewardType string
|
|
|
|
const (
|
|
CampaignRewardTypePoints CampaignRewardType = "POINTS"
|
|
CampaignRewardTypeCoins CampaignRewardType = "COINS"
|
|
CampaignRewardTypeReward CampaignRewardType = "REWARD"
|
|
)
|
|
|
|
type RewardSubtype string
|
|
|
|
const (
|
|
RewardSubtypeMultiplier RewardSubtype = "MULTIPLIER"
|
|
RewardSubtypeSpin RewardSubtype = "SPIN"
|
|
RewardSubtypeBonus RewardSubtype = "BONUS"
|
|
RewardSubtypePhysical RewardSubtype = "PHYSICAL"
|
|
)
|
|
|
|
type Campaign struct {
|
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
Name string `gorm:"type:varchar(150);not null" json:"name"`
|
|
Description *string `gorm:"type:text" json:"description,omitempty"`
|
|
Type CampaignType `gorm:"type:varchar(50);not null" json:"type"`
|
|
StartDate time.Time `gorm:"type:timestamp;not null" json:"start_date"`
|
|
EndDate time.Time `gorm:"type:timestamp;not null" json:"end_date"`
|
|
IsActive bool `gorm:"type:boolean;default:true" json:"is_active"`
|
|
ShowOnApp bool `gorm:"type:boolean;default:true" json:"show_on_app"`
|
|
Position int `gorm:"type:int;default:0" json:"position"`
|
|
Metadata *Metadata `gorm:"type:jsonb" json:"metadata,omitempty"`
|
|
CreatedAt time.Time `gorm:"type:timestamp;default:now()" json:"created_at"`
|
|
UpdatedAt time.Time `gorm:"type:timestamp;default:now()" json:"updated_at"`
|
|
|
|
// Relations
|
|
Rules []CampaignRule `gorm:"foreignKey:CampaignID;constraint:OnDelete:CASCADE" json:"rules,omitempty"`
|
|
}
|
|
|
|
func (Campaign) TableName() string {
|
|
return "campaigns"
|
|
}
|
|
|
|
func (c *Campaign) BeforeCreate(tx *gorm.DB) error {
|
|
if c.ID == uuid.Nil {
|
|
c.ID = uuid.New()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Campaign) BeforeUpdate(tx *gorm.DB) error {
|
|
c.UpdatedAt = time.Now()
|
|
return nil
|
|
}
|
|
|
|
type CampaignRule struct {
|
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
|
CampaignID uuid.UUID `gorm:"type:uuid;not null" json:"campaign_id"`
|
|
RuleType RuleType `gorm:"type:varchar(50);not null" json:"rule_type"`
|
|
ConditionValue *string `gorm:"type:varchar(255)" json:"condition_value,omitempty"`
|
|
RewardType CampaignRewardType `gorm:"type:varchar(50);not null" json:"reward_type"`
|
|
RewardValue *int64 `gorm:"type:bigint" json:"reward_value,omitempty"`
|
|
RewardSubtype *RewardSubtype `gorm:"type:varchar(50)" json:"reward_subtype,omitempty"`
|
|
RewardRefID *uuid.UUID `gorm:"type:uuid" json:"reward_ref_id,omitempty"`
|
|
Metadata *Metadata `gorm:"type:jsonb" json:"metadata,omitempty"`
|
|
CreatedAt time.Time `gorm:"type:timestamp;default:now()" json:"created_at"`
|
|
UpdatedAt time.Time `gorm:"type:timestamp;default:now()" json:"updated_at"`
|
|
|
|
// Relations
|
|
Campaign Campaign `gorm:"foreignKey:CampaignID" json:"campaign,omitempty"`
|
|
}
|
|
|
|
func (CampaignRule) TableName() string {
|
|
return "campaign_rules"
|
|
}
|
|
|
|
func (cr *CampaignRule) BeforeCreate(tx *gorm.DB) error {
|
|
if cr.ID == uuid.Nil {
|
|
cr.ID = uuid.New()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (cr *CampaignRule) BeforeUpdate(tx *gorm.DB) error {
|
|
cr.UpdatedAt = time.Now()
|
|
return nil
|
|
}
|
|
|
|
type ListCampaignsRequest struct {
|
|
Page int `form:"page" binding:"min=1"`
|
|
Limit int `form:"limit" binding:"min=1,max=100"`
|
|
Search string `form:"search"`
|
|
Type string `form:"type"`
|
|
IsActive *bool `form:"is_active"`
|
|
ShowOnApp *bool `form:"show_on_app"`
|
|
StartDate *time.Time `form:"start_date"`
|
|
EndDate *time.Time `form:"end_date"`
|
|
}
|
|
|
|
type ListCampaignRulesRequest struct {
|
|
Page int `form:"page" binding:"min=1"`
|
|
Limit int `form:"limit" binding:"min=1,max=100"`
|
|
CampaignID string `form:"campaign_id"`
|
|
RuleType string `form:"rule_type"`
|
|
RewardType string `form:"reward_type"`
|
|
}
|