diff --git a/backend/internal/payment/provider/wxpay_test.go b/backend/internal/payment/provider/wxpay_test.go index b8b99537..4b774d63 100644 --- a/backend/internal/payment/provider/wxpay_test.go +++ b/backend/internal/payment/provider/wxpay_test.go @@ -156,7 +156,6 @@ func TestNewWxpay(t *testing.T) { "apiV3Key": "12345678901234567890123456789012", // exactly 32 bytes "publicKey": "fake-public-key", "publicKeyId": "key-id-001", - "certSerial": "SERIAL001", } // helper to clone and override config fields diff --git a/backend/internal/service/payment_fulfillment.go b/backend/internal/service/payment_fulfillment.go index db92ff2b..47724db6 100644 --- a/backend/internal/service/payment_fulfillment.go +++ b/backend/internal/service/payment_fulfillment.go @@ -146,20 +146,46 @@ func (s *PaymentService) ExecuteBalanceFulfillment(ctx context.Context, oid int6 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 { // Idempotency: check if redeem code already exists (from a previous partial run) - existing, _ := s.redeemService.GetByCode(ctx, o.RechargeCode) - if existing != nil { - if existing.IsUsed() { - // Code already created and redeemed — just mark completed - return s.markCompleted(ctx, o, "RECHARGE_SUCCESS") - } - // Code exists but unused — skip creation, proceed to redeem - } else { + existing, lookupErr := s.redeemService.GetByCode(ctx, o.RechargeCode) + action := resolveRedeemAction(existing, lookupErr) + + switch action { + case redeemActionSkipCompleted: + // Code already created and redeemed — just mark completed + return s.markCompleted(ctx, o, "RECHARGE_SUCCESS") + case redeemActionCreate: rc := &RedeemCode{Code: o.RechargeCode, Type: RedeemTypeBalance, Value: o.Amount, Status: StatusUnused} if err := s.redeemService.CreateCode(ctx, rc); err != nil { 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 { return fmt.Errorf("redeem balance: %w", err)