test(payment): add unit tests for payment audit fixes + allow empty supported_types
Tests (1033 new lines, 100% coverage on modified functions): - amount.go: YuanToFen/FenToYuan with precision edge cases - wxpay: mapWxState, wxSV, formatPEM, NewWxpay validation - alipay: isTradeNotExist, NewAlipay validation - webhook: writeSuccessResponse (wxpay JSON, stripe empty, others text) - config: validateProviderRequest, isSensitiveConfigField, joinTypes - fulfillment: resolveRedeemAction idempotency logic Business logic changes: - Allow empty supported_types on provider instances - Block removing payment types when instance has pending orders - Extract resolveRedeemAction as testable pure function
This commit is contained in:
@@ -156,7 +156,6 @@ func TestNewWxpay(t *testing.T) {
|
|||||||
"apiV3Key": "12345678901234567890123456789012", // exactly 32 bytes
|
"apiV3Key": "12345678901234567890123456789012", // exactly 32 bytes
|
||||||
"publicKey": "fake-public-key",
|
"publicKey": "fake-public-key",
|
||||||
"publicKeyId": "key-id-001",
|
"publicKeyId": "key-id-001",
|
||||||
"certSerial": "SERIAL001",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper to clone and override config fields
|
// helper to clone and override config fields
|
||||||
|
|||||||
@@ -146,20 +146,46 @@ func (s *PaymentService) ExecuteBalanceFulfillment(ctx context.Context, oid int6
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// redeemAction represents the idempotency decision for balance fulfillment.
|
||||||
|
type redeemAction int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// redeemActionCreate: code does not exist — create it, then redeem.
|
||||||
|
redeemActionCreate redeemAction = iota
|
||||||
|
// redeemActionRedeem: code exists but is unused — skip creation, redeem only.
|
||||||
|
redeemActionRedeem
|
||||||
|
// redeemActionSkipCompleted: code exists and is already used — skip to mark completed.
|
||||||
|
redeemActionSkipCompleted
|
||||||
|
)
|
||||||
|
|
||||||
|
// resolveRedeemAction decides the idempotency action based on an existing redeem code lookup.
|
||||||
|
// existing is the result of GetByCode; lookupErr is the error from that call.
|
||||||
|
func resolveRedeemAction(existing *RedeemCode, lookupErr error) redeemAction {
|
||||||
|
if existing == nil || lookupErr != nil {
|
||||||
|
return redeemActionCreate
|
||||||
|
}
|
||||||
|
if existing.IsUsed() {
|
||||||
|
return redeemActionSkipCompleted
|
||||||
|
}
|
||||||
|
return redeemActionRedeem
|
||||||
|
}
|
||||||
|
|
||||||
func (s *PaymentService) doBalance(ctx context.Context, o *dbent.PaymentOrder) error {
|
func (s *PaymentService) doBalance(ctx context.Context, o *dbent.PaymentOrder) error {
|
||||||
// Idempotency: check if redeem code already exists (from a previous partial run)
|
// Idempotency: check if redeem code already exists (from a previous partial run)
|
||||||
existing, _ := s.redeemService.GetByCode(ctx, o.RechargeCode)
|
existing, lookupErr := s.redeemService.GetByCode(ctx, o.RechargeCode)
|
||||||
if existing != nil {
|
action := resolveRedeemAction(existing, lookupErr)
|
||||||
if existing.IsUsed() {
|
|
||||||
// Code already created and redeemed — just mark completed
|
switch action {
|
||||||
return s.markCompleted(ctx, o, "RECHARGE_SUCCESS")
|
case redeemActionSkipCompleted:
|
||||||
}
|
// Code already created and redeemed — just mark completed
|
||||||
// Code exists but unused — skip creation, proceed to redeem
|
return s.markCompleted(ctx, o, "RECHARGE_SUCCESS")
|
||||||
} else {
|
case redeemActionCreate:
|
||||||
rc := &RedeemCode{Code: o.RechargeCode, Type: RedeemTypeBalance, Value: o.Amount, Status: StatusUnused}
|
rc := &RedeemCode{Code: o.RechargeCode, Type: RedeemTypeBalance, Value: o.Amount, Status: StatusUnused}
|
||||||
if err := s.redeemService.CreateCode(ctx, rc); err != nil {
|
if err := s.redeemService.CreateCode(ctx, rc); err != nil {
|
||||||
return fmt.Errorf("create redeem code: %w", err)
|
return fmt.Errorf("create redeem code: %w", err)
|
||||||
}
|
}
|
||||||
|
case redeemActionRedeem:
|
||||||
|
// Code exists but unused — skip creation, proceed to redeem
|
||||||
}
|
}
|
||||||
if _, err := s.redeemService.Redeem(ctx, o.UserID, o.RechargeCode); err != nil {
|
if _, err := s.redeemService.Redeem(ctx, o.UserID, o.RechargeCode); err != nil {
|
||||||
return fmt.Errorf("redeem balance: %w", err)
|
return fmt.Errorf("redeem balance: %w", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user