package order import ( "context" "testing" "github.com/perfect-panel/server/internal/model/promo" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "gorm.io/gorm" ) type fakePromoModel struct { rules []*promo.RuleWithPrice lastSubscribeID int64 lastQuantity int64 requireQuantity int64 quantityMismatch []*promo.RuleWithPrice } func (m *fakePromoModel) QueryEligibleRules(_ context.Context, subscribeID int64, quantity int64) ([]*promo.RuleWithPrice, error) { m.lastSubscribeID = subscribeID m.lastQuantity = quantity if m.requireQuantity > 0 && quantity != m.requireQuantity { return m.quantityMismatch, nil } return m.rules, nil } func (m *fakePromoModel) InsertUsage(context.Context, *promo.Usage, ...*gorm.DB) error { return nil } func (m *fakePromoModel) InsertRule(context.Context, *promo.Rule) error { return nil } func (m *fakePromoModel) FindRule(context.Context, int64) (*promo.Rule, error) { return nil, gorm.ErrRecordNotFound } func (m *fakePromoModel) UpdateRule(context.Context, *promo.Rule) error { return nil } func (m *fakePromoModel) DeleteRule(context.Context, int64) error { return nil } func (m *fakePromoModel) QueryRuleList(context.Context, int, int, string, *bool, string) (int64, []*promo.Rule, error) { return 0, nil, nil } func (m *fakePromoModel) UpsertPrices(context.Context, int64, []*promo.SubscribePromo) error { return nil } func (m *fakePromoModel) FindPrice(context.Context, int64) (*promo.SubscribePromo, error) { return nil, gorm.ErrRecordNotFound } func (m *fakePromoModel) DeletePrice(context.Context, int64) error { return nil } func (m *fakePromoModel) QueryPriceList(context.Context, promo.PriceFilter) (int64, []*promo.SubscribePromo, error) { return 0, nil, nil } func (m *fakePromoModel) QueryUsageList(context.Context, promo.UsageFilter) (int64, []*promo.Usage, error) { return 0, nil, nil } func (m *fakePromoModel) Transaction(context.Context, func(*gorm.DB) error) error { return nil } func TestCalculatePurchasePricePromoUsesQuantityTierTotalPrice(t *testing.T) { model := &fakePromoModel{rules: []*promo.RuleWithPrice{ { Rule: promo.Rule{ Id: 9, Name: "campaign", Type: promo.RuleTypeCampaign, Enabled: true, }, PromoPrice: 279, }, }} svcCtx := &svc.ServiceContext{ DB: &gorm.DB{}, PromoModel: model, } result, err := calculatePurchasePrice( context.Background(), svcCtx, 1, 2, 100, 7, []types.SubscribeDiscount{{Quantity: 7, Discount: 50}}, true, true, ) if err != nil { t.Fatalf("calculatePurchasePrice returned error: %v", err) } if result.OriginalPrice != 700 { t.Fatalf("OriginalPrice = %d, want 700", result.OriginalPrice) } if result.PayableBase != 279 { t.Fatalf("PayableBase = %d, want 279", result.PayableBase) } if result.DiscountAmount != 0 { t.Fatalf("DiscountAmount = %d, want 0", result.DiscountAmount) } if result.PromoRuleId != 9 { t.Fatalf("PromoRuleId = %d, want 9", result.PromoRuleId) } if result.PromoDiscount != 421 { t.Fatalf("PromoDiscount = %d, want 421", result.PromoDiscount) } if result.PromoPrice != 279 { t.Fatalf("PromoPrice = %d, want 279", result.PromoPrice) } if model.lastQuantity != 7 { t.Fatalf("promo query quantity = %d, want 7", model.lastQuantity) } } func TestCalculatePurchasePriceIgnoresInvalidPromoPrice(t *testing.T) { model := &fakePromoModel{rules: []*promo.RuleWithPrice{ { Rule: promo.Rule{ Id: 10, Name: "invalid campaign", Type: promo.RuleTypeCampaign, Enabled: true, }, PromoPrice: 3000, }, }} svcCtx := &svc.ServiceContext{ DB: &gorm.DB{}, PromoModel: model, } result, err := calculatePurchasePrice( context.Background(), svcCtx, 1, 2, 1000, 3, []types.SubscribeDiscount{{Quantity: 3, Discount: 50}}, true, true, ) if err != nil { t.Fatalf("calculatePurchasePrice returned error: %v", err) } if result.PayableBase != 1500 { t.Fatalf("PayableBase = %d, want 1500", result.PayableBase) } if result.DiscountAmount != 1500 { t.Fatalf("DiscountAmount = %d, want 1500", result.DiscountAmount) } if result.PromoRuleId != 0 || result.PromoDiscount != 0 { t.Fatalf("promo fields = (%d, %d), want (0, 0)", result.PromoRuleId, result.PromoDiscount) } } func TestCalculatePurchasePricePassesQuantityToPromoEvaluation(t *testing.T) { promoModel := &fakePromoModel{ requireQuantity: 6, rules: []*promo.RuleWithPrice{ { Rule: promo.Rule{ Id: 11, Name: "quantity campaign", Type: promo.RuleTypeCampaign, Enabled: true, }, PromoPrice: 3000, }, }, } svcCtx := &svc.ServiceContext{ DB: &gorm.DB{}, PromoModel: promoModel, } result, err := calculatePurchasePrice( context.Background(), svcCtx, 1, 2, 1000, 6, []types.SubscribeDiscount{{Quantity: 6, Discount: 80}}, true, true, ) if err != nil { t.Fatalf("calculatePurchasePrice returned error: %v", err) } if promoModel.lastSubscribeID != 2 { t.Fatalf("lastSubscribeID = %d, want 2", promoModel.lastSubscribeID) } if promoModel.lastQuantity != 6 { t.Fatalf("lastQuantity = %d, want 6", promoModel.lastQuantity) } if result.PayableBase != 3000 { t.Fatalf("PayableBase = %d, want 3000", result.PayableBase) } if result.PromoRuleId != 11 { t.Fatalf("PromoRuleId = %d, want 11", result.PromoRuleId) } } func TestCalculatePurchasePriceCampaignAppliesToReturningUsers(t *testing.T) { model := &fakePromoModel{rules: []*promo.RuleWithPrice{ { Rule: promo.Rule{ Id: 12, Name: "campaign", Type: promo.RuleTypeCampaign, Enabled: true, }, PromoPrice: 400, }, }} svcCtx := &svc.ServiceContext{DB: &gorm.DB{}, PromoModel: model} // isFirstPurchase=false simulates a returning user routed to renewal; the // Campaign promo must still apply because it has no first-purchase gate. result, err := calculatePurchasePrice( context.Background(), svcCtx, 42, 2, 1000, 1, nil, false, false, ) if err != nil { t.Fatalf("calculatePurchasePrice returned error: %v", err) } if result.PromoRuleId != 12 { t.Fatalf("PromoRuleId = %d, want 12 (campaign should apply to returning users)", result.PromoRuleId) } if result.PayableBase != 400 { t.Fatalf("PayableBase = %d, want 400", result.PayableBase) } } func TestCalculatePurchasePriceNewUserGatedByFirstPurchase(t *testing.T) { model := &fakePromoModel{rules: []*promo.RuleWithPrice{ { Rule: promo.Rule{ Id: 13, Name: "new user", Type: promo.RuleTypeNewUser, Enabled: true, Params: `{"window_hours": 72}`, }, PromoPrice: 200, }, }} svcCtx := &svc.ServiceContext{DB: &gorm.DB{}, PromoModel: model} // isFirstPurchase=false → NewUser rule must be skipped, regular discount applies. result, err := calculatePurchasePrice( context.Background(), svcCtx, 42, 2, 1000, 1, []types.SubscribeDiscount{{Quantity: 1, Discount: 90}}, true, false, ) if err != nil { t.Fatalf("calculatePurchasePrice returned error: %v", err) } if result.PromoRuleId != 0 || result.PromoDiscount != 0 { t.Fatalf("promo fields = (%d, %d), want (0, 0) when isFirstPurchase=false", result.PromoRuleId, result.PromoDiscount) } if result.PayableBase != 900 { t.Fatalf("PayableBase = %d, want 900 (regular 90%% discount)", result.PayableBase) } }