new-api/model/payment_method_guard_test.go
zizi 1de1cdb14f feat: add promo code bonus flow
Add promo code models and APIs, persist selected promo codes on top-up orders, and apply bonus quota idempotently when payments complete.
2026-05-20 12:57:54 +08:00

257 lines
8.3 KiB
Go

package model
import (
"testing"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func insertUserForPaymentGuardTest(t *testing.T, id int, quota int) {
t.Helper()
user := &User{
Id: id,
Username: "payment_guard_user",
Status: common.UserStatusEnabled,
Quota: quota,
}
require.NoError(t, DB.Create(user).Error)
}
func insertSubscriptionPlanForPaymentGuardTest(t *testing.T, id int) *SubscriptionPlan {
t.Helper()
plan := &SubscriptionPlan{
Id: id,
Title: "Guard Plan",
PriceAmount: 9.99,
Currency: "USD",
DurationUnit: SubscriptionDurationMonth,
DurationValue: 1,
Enabled: true,
TotalAmount: 1000,
}
require.NoError(t, DB.Create(plan).Error)
return plan
}
func insertSubscriptionOrderForPaymentGuardTest(t *testing.T, tradeNo string, userID int, planID int, paymentProvider string) {
t.Helper()
order := &SubscriptionOrder{
UserId: userID,
PlanId: planID,
Money: 9.99,
TradeNo: tradeNo,
PaymentMethod: paymentProvider,
PaymentProvider: paymentProvider,
Status: common.TopUpStatusPending,
CreateTime: time.Now().Unix(),
}
require.NoError(t, order.Insert())
}
func insertTopUpForPaymentGuardTest(t *testing.T, tradeNo string, userID int, paymentProvider string) {
t.Helper()
topUp := &TopUp{
UserId: userID,
Amount: 2,
Money: 9.99,
TradeNo: tradeNo,
PaymentMethod: paymentProvider,
PaymentProvider: paymentProvider,
Status: common.TopUpStatusPending,
CreateTime: time.Now().Unix(),
}
require.NoError(t, topUp.Insert())
}
func getTopUpStatusForPaymentGuardTest(t *testing.T, tradeNo string) string {
t.Helper()
topUp := GetTopUpByTradeNo(tradeNo)
require.NotNil(t, topUp)
return topUp.Status
}
func countUserSubscriptionsForPaymentGuardTest(t *testing.T, userID int) int64 {
t.Helper()
var count int64
require.NoError(t, DB.Model(&UserSubscription{}).Where("user_id = ?", userID).Count(&count).Error)
return count
}
func getUserQuotaForPaymentGuardTest(t *testing.T, userID int) int {
t.Helper()
var user User
require.NoError(t, DB.Select("quota").Where("id = ?", userID).First(&user).Error)
return user.Quota
}
func insertPromoCodeForPaymentGuardTest(t *testing.T, code string, bonusAmount int, minRechargeAmount int, maxUses int) *PromoCode {
t.Helper()
promoCode := &PromoCode{
Code: code,
BonusAmount: bonusAmount,
MaxUses: maxUses,
Status: 1,
MinRechargeAmount: minRechargeAmount,
}
require.NoError(t, DB.Create(promoCode).Error)
return promoCode
}
func getPromoCodeUsedCountForPaymentGuardTest(t *testing.T, promoCodeID int) int {
t.Helper()
var promoCode PromoCode
require.NoError(t, DB.Select("used_count").Where("id = ?", promoCodeID).First(&promoCode).Error)
return promoCode.UsedCount
}
func countPromoCodeUsagesForPaymentGuardTest(t *testing.T, promoCodeID int, userID int) int64 {
t.Helper()
var count int64
require.NoError(t, DB.Model(&PromoCodeUsage{}).Where("promo_code_id = ? AND user_id = ?", promoCodeID, userID).Count(&count).Error)
return count
}
func TestRechargeWaffoPancake_RejectsMismatchedPaymentMethod(t *testing.T) {
truncateTables(t)
insertUserForPaymentGuardTest(t, 101, 0)
insertTopUpForPaymentGuardTest(t, "waffo-pancake-guard", 101, PaymentProviderStripe)
err := RechargeWaffoPancake("waffo-pancake-guard")
require.Error(t, err)
topUp := GetTopUpByTradeNo("waffo-pancake-guard")
require.NotNil(t, topUp)
assert.Equal(t, common.TopUpStatusPending, topUp.Status)
assert.Equal(t, 0, getUserQuotaForPaymentGuardTest(t, 101))
}
func TestUpdatePendingTopUpStatus_RejectsMismatchedPaymentProvider(t *testing.T) {
testCases := []struct {
name string
tradeNo string
storedPaymentProvider string
expectedPaymentProvider string
targetStatus string
}{
{
name: "stripe expire",
tradeNo: "stripe-expire-guard",
storedPaymentProvider: PaymentProviderCreem,
expectedPaymentProvider: PaymentProviderStripe,
targetStatus: common.TopUpStatusExpired,
},
{
name: "waffo failed",
tradeNo: "waffo-failed-guard",
storedPaymentProvider: PaymentProviderStripe,
expectedPaymentProvider: PaymentProviderWaffo,
targetStatus: common.TopUpStatusFailed,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
truncateTables(t)
insertUserForPaymentGuardTest(t, 150, 0)
insertTopUpForPaymentGuardTest(t, tc.tradeNo, 150, tc.storedPaymentProvider)
err := UpdatePendingTopUpStatus(tc.tradeNo, tc.expectedPaymentProvider, tc.targetStatus)
require.ErrorIs(t, err, ErrPaymentMethodMismatch)
assert.Equal(t, common.TopUpStatusPending, getTopUpStatusForPaymentGuardTest(t, tc.tradeNo))
})
}
}
func TestValidateTopUpPromoCode_RejectsBelowMinimumAndUsedCode(t *testing.T) {
truncateTables(t)
insertUserForPaymentGuardTest(t, 180, 0)
promoCode := insertPromoCodeForPaymentGuardTest(t, "PROMO_MIN", 123, 10, 0)
err := ValidateTopUpPromoCode(180, promoCode.Id, 9)
require.Error(t, err)
assert.Contains(t, err.Error(), "最低要求")
require.NoError(t, ValidateTopUpPromoCode(180, promoCode.Id, 10))
require.NoError(t, DB.Create(&PromoCodeUsage{
PromoCodeId: promoCode.Id,
UserId: 180,
UsedAt: time.Now(),
}).Error)
err = ValidateTopUpPromoCode(180, promoCode.Id, 10)
require.Error(t, err)
assert.Contains(t, err.Error(), "已使用")
}
func TestRechargeWaffo_AppliesPromoCodeBonusIdempotently(t *testing.T) {
truncateTables(t)
insertUserForPaymentGuardTest(t, 181, 10)
promoCode := insertPromoCodeForPaymentGuardTest(t, "WAFFO_BONUS", 123, 2, 1)
topUp := &TopUp{
UserId: 181,
Amount: 2,
Money: 2,
TradeNo: "waffo-promo-guard",
PaymentMethod: PaymentMethodWaffo,
PaymentProvider: PaymentProviderWaffo,
PromoCodeId: promoCode.Id,
Status: common.TopUpStatusPending,
CreateTime: time.Now().Unix(),
}
require.NoError(t, topUp.Insert())
require.NoError(t, RechargeWaffo("waffo-promo-guard", "127.0.0.1"))
baseQuota := int(decimal.NewFromInt(2).Mul(decimal.NewFromFloat(common.QuotaPerUnit)).IntPart())
assert.Equal(t, 10+baseQuota+123, getUserQuotaForPaymentGuardTest(t, 181))
assert.Equal(t, common.TopUpStatusSuccess, getTopUpStatusForPaymentGuardTest(t, "waffo-promo-guard"))
assert.Equal(t, 1, getPromoCodeUsedCountForPaymentGuardTest(t, promoCode.Id))
assert.Equal(t, int64(1), countPromoCodeUsagesForPaymentGuardTest(t, promoCode.Id, 181))
require.NoError(t, RechargeWaffo("waffo-promo-guard", "127.0.0.1"))
assert.Equal(t, 10+baseQuota+123, getUserQuotaForPaymentGuardTest(t, 181))
assert.Equal(t, 1, getPromoCodeUsedCountForPaymentGuardTest(t, promoCode.Id))
assert.Equal(t, int64(1), countPromoCodeUsagesForPaymentGuardTest(t, promoCode.Id, 181))
}
func TestCompleteSubscriptionOrder_RejectsMismatchedPaymentProvider(t *testing.T) {
truncateTables(t)
insertUserForPaymentGuardTest(t, 202, 0)
plan := insertSubscriptionPlanForPaymentGuardTest(t, 301)
insertSubscriptionOrderForPaymentGuardTest(t, "sub-guard-order", 202, plan.Id, PaymentProviderStripe)
err := CompleteSubscriptionOrder("sub-guard-order", `{"provider":"epay"}`, PaymentProviderEpay, "alipay")
require.ErrorIs(t, err, ErrPaymentMethodMismatch)
order := GetSubscriptionOrderByTradeNo("sub-guard-order")
require.NotNil(t, order)
assert.Equal(t, common.TopUpStatusPending, order.Status)
assert.Zero(t, countUserSubscriptionsForPaymentGuardTest(t, 202))
topUp := GetTopUpByTradeNo("sub-guard-order")
assert.Nil(t, topUp)
}
func TestExpireSubscriptionOrder_RejectsMismatchedPaymentProvider(t *testing.T) {
truncateTables(t)
insertUserForPaymentGuardTest(t, 303, 0)
plan := insertSubscriptionPlanForPaymentGuardTest(t, 401)
insertSubscriptionOrderForPaymentGuardTest(t, "sub-expire-guard", 303, plan.Id, PaymentProviderStripe)
err := ExpireSubscriptionOrder("sub-expire-guard", PaymentProviderCreem)
require.ErrorIs(t, err, ErrPaymentMethodMismatch)
order := GetSubscriptionOrderByTradeNo("sub-expire-guard")
require.NotNil(t, order)
assert.Equal(t, common.TopUpStatusPending, order.Status)
}