Files
apskel-pos-backend/internal/processor/campaign_processor.go
T
efrilmandClaude Opus 5.5 41b75810fd feat(wallet): migrate legacy points and tokens into the wallet
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>
2026-09-30 08:47:48 +07:00

293 lines
9.4 KiB
Go

package processor
import (
"context"
"fmt"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository"
)
type CampaignProcessor interface {
CreateCampaign(ctx context.Context, req *contract.CreateCampaignRequest) (*models.CampaignResponse, error)
GetCampaign(ctx context.Context, id string) (*models.CampaignResponse, error)
ListCampaigns(ctx context.Context, req *contract.ListCampaignsRequest) (*models.ListCampaignsResponse, error)
UpdateCampaign(ctx context.Context, req *contract.UpdateCampaignRequest) (*models.CampaignResponse, error)
DeleteCampaign(ctx context.Context, id string) error
GetActiveCampaigns(ctx context.Context) ([]models.CampaignResponse, error)
GetCampaignsForApp(ctx context.Context) ([]models.CampaignResponse, error)
}
type campaignProcessor struct {
campaignRepo repository.CampaignRepository
}
func NewCampaignProcessor(campaignRepo repository.CampaignRepository) CampaignProcessor {
return &campaignProcessor{
campaignRepo: campaignRepo,
}
}
func (p *campaignProcessor) CreateCampaign(ctx context.Context, req *contract.CreateCampaignRequest) (*models.CampaignResponse, error) {
// Convert request to entity
entity := mappers.ToCampaignEntity(req)
if entity == nil {
return nil, fmt.Errorf("invalid request data")
}
// Create in repository
if err := p.campaignRepo.Create(ctx, entity); err != nil {
return nil, fmt.Errorf("failed to create campaign: %w", err)
}
// Convert entity to response
response := mappers.ToCampaignResponse(entity)
return response, nil
}
func (p *campaignProcessor) GetCampaign(ctx context.Context, id string) (*models.CampaignResponse, error) {
// Get from repository
entity, err := p.campaignRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get campaign: %w", err)
}
if entity == nil {
return nil, fmt.Errorf("campaign not found")
}
// Convert entity to response
response := mappers.ToCampaignResponse(entity)
return response, nil
}
func (p *campaignProcessor) ListCampaigns(ctx context.Context, req *contract.ListCampaignsRequest) (*models.ListCampaignsResponse, error) {
// Convert request to entity request
entityReq := &entities.ListCampaignsRequest{
Page: req.Page,
Limit: req.Limit,
Search: req.Search,
Type: string(entities.NormalizeCampaignType(req.Type)),
IsActive: req.IsActive,
ShowOnApp: req.ShowOnApp,
StartDate: req.StartDate,
EndDate: req.EndDate,
}
// Get from repository
entities, total, err := p.campaignRepo.List(ctx, entityReq)
if err != nil {
return nil, fmt.Errorf("failed to list campaigns: %w", err)
}
// Convert entities to response
campaigns := mappers.ToCampaignResponseSlice(entities)
response := &models.ListCampaignsResponse{
Campaigns: campaigns,
Total: total,
Page: req.Page,
Limit: req.Limit,
}
return response, nil
}
func (p *campaignProcessor) UpdateCampaign(ctx context.Context, req *contract.UpdateCampaignRequest) (*models.CampaignResponse, error) {
// Convert request to entity
entity := mappers.ToCampaignEntityFromUpdate(req)
if entity == nil {
return nil, fmt.Errorf("invalid request data")
}
// Update in repository
if err := p.campaignRepo.Update(ctx, entity); err != nil {
return nil, fmt.Errorf("failed to update campaign: %w", err)
}
// Get updated entity
updatedEntity, err := p.campaignRepo.GetByID(ctx, req.ID.String())
if err != nil {
return nil, fmt.Errorf("failed to get updated campaign: %w", err)
}
// Convert entity to response
response := mappers.ToCampaignResponse(updatedEntity)
return response, nil
}
func (p *campaignProcessor) DeleteCampaign(ctx context.Context, id string) error {
// Delete from repository
if err := p.campaignRepo.Delete(ctx, id); err != nil {
return fmt.Errorf("failed to delete campaign: %w", err)
}
return nil
}
func (p *campaignProcessor) GetActiveCampaigns(ctx context.Context) ([]models.CampaignResponse, error) {
// Get from repository
entities, err := p.campaignRepo.GetActiveCampaigns(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get active campaigns: %w", err)
}
// Convert entities to response
campaigns := mappers.ToCampaignResponseSlice(entities)
return campaigns, nil
}
func (p *campaignProcessor) GetCampaignsForApp(ctx context.Context) ([]models.CampaignResponse, error) {
// Get from repository
entities, err := p.campaignRepo.GetCampaignsForApp(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get campaigns for app: %w", err)
}
// Convert entities to response
campaigns := mappers.ToCampaignResponseSlice(entities)
return campaigns, nil
}
// Campaign Rule Processor
type CampaignRuleProcessor interface {
CreateCampaignRule(ctx context.Context, req *contract.CreateCampaignRuleRequest) (*models.CampaignRuleResponse, error)
GetCampaignRule(ctx context.Context, id string) (*models.CampaignRuleResponse, error)
ListCampaignRules(ctx context.Context, req *contract.ListCampaignRulesRequest) (*models.ListCampaignRulesResponse, error)
UpdateCampaignRule(ctx context.Context, req *contract.UpdateCampaignRuleRequest) (*models.CampaignRuleResponse, error)
DeleteCampaignRule(ctx context.Context, id string) error
GetCampaignRulesByCampaignID(ctx context.Context, campaignID string) ([]models.CampaignRuleResponse, error)
}
type campaignRuleProcessor struct {
campaignRuleRepo repository.CampaignRuleRepository
}
func NewCampaignRuleProcessor(campaignRuleRepo repository.CampaignRuleRepository) CampaignRuleProcessor {
return &campaignRuleProcessor{
campaignRuleRepo: campaignRuleRepo,
}
}
func (p *campaignRuleProcessor) CreateCampaignRule(ctx context.Context, req *contract.CreateCampaignRuleRequest) (*models.CampaignRuleResponse, error) {
// Convert request to entity
entity := &entities.CampaignRule{
CampaignID: req.CampaignID,
RuleType: entities.RuleType(req.RuleType),
ConditionValue: req.ConditionValue,
RewardType: entities.NormalizeCampaignRewardType(req.RewardType),
RewardValue: req.RewardValue,
RewardSubtype: (*entities.RewardSubtype)(req.RewardSubtype),
RewardRefID: req.RewardRefID,
Metadata: req.Metadata,
}
// Create in repository
if err := p.campaignRuleRepo.Create(ctx, entity); err != nil {
return nil, fmt.Errorf("failed to create campaign rule: %w", err)
}
// Convert entity to response
response := mappers.ToCampaignRuleResponse(entity)
return response, nil
}
func (p *campaignRuleProcessor) GetCampaignRule(ctx context.Context, id string) (*models.CampaignRuleResponse, error) {
// Get from repository
entity, err := p.campaignRuleRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get campaign rule: %w", err)
}
if entity == nil {
return nil, fmt.Errorf("campaign rule not found")
}
// Convert entity to response
response := mappers.ToCampaignRuleResponse(entity)
return response, nil
}
func (p *campaignRuleProcessor) ListCampaignRules(ctx context.Context, req *contract.ListCampaignRulesRequest) (*models.ListCampaignRulesResponse, error) {
// Convert request to entity request
entityReq := &entities.ListCampaignRulesRequest{
Page: req.Page,
Limit: req.Limit,
CampaignID: req.CampaignID,
RuleType: req.RuleType,
RewardType: string(entities.NormalizeCampaignRewardType(req.RewardType)),
}
// Get from repository
entities, total, err := p.campaignRuleRepo.List(ctx, entityReq)
if err != nil {
return nil, fmt.Errorf("failed to list campaign rules: %w", err)
}
// Convert entities to response
rules := mappers.ToCampaignRuleResponseSlice(entities)
response := &models.ListCampaignRulesResponse{
Rules: rules,
Total: total,
Page: req.Page,
Limit: req.Limit,
}
return response, nil
}
func (p *campaignRuleProcessor) UpdateCampaignRule(ctx context.Context, req *contract.UpdateCampaignRuleRequest) (*models.CampaignRuleResponse, error) {
// Convert request to entity
entity := &entities.CampaignRule{
ID: req.ID,
CampaignID: req.CampaignID,
RuleType: entities.RuleType(req.RuleType),
ConditionValue: req.ConditionValue,
RewardType: entities.NormalizeCampaignRewardType(req.RewardType),
RewardValue: req.RewardValue,
RewardSubtype: (*entities.RewardSubtype)(req.RewardSubtype),
RewardRefID: req.RewardRefID,
Metadata: req.Metadata,
}
// Update in repository
if err := p.campaignRuleRepo.Update(ctx, entity); err != nil {
return nil, fmt.Errorf("failed to update campaign rule: %w", err)
}
// Get updated entity
updatedEntity, err := p.campaignRuleRepo.GetByID(ctx, req.ID.String())
if err != nil {
return nil, fmt.Errorf("failed to get updated campaign rule: %w", err)
}
// Convert entity to response
response := mappers.ToCampaignRuleResponse(updatedEntity)
return response, nil
}
func (p *campaignRuleProcessor) DeleteCampaignRule(ctx context.Context, id string) error {
// Delete from repository
if err := p.campaignRuleRepo.Delete(ctx, id); err != nil {
return fmt.Errorf("failed to delete campaign rule: %w", err)
}
return nil
}
func (p *campaignRuleProcessor) GetCampaignRulesByCampaignID(ctx context.Context, campaignID string) ([]models.CampaignRuleResponse, error) {
// Get from repository
entities, err := p.campaignRuleRepo.GetByCampaignID(ctx, campaignID)
if err != nil {
return nil, fmt.Errorf("failed to get campaign rules by campaign ID: %w", err)
}
// Convert entities to response
rules := mappers.ToCampaignRuleResponseSlice(entities)
return rules, nil
}