feat(payment): add complete payment system with multi-provider support
Add a full payment and subscription system supporting EasyPay (Alipay/WeChat), Stripe, and direct Alipay/WeChat Pay providers with multi-instance load balancing.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// resolveRedeemAction — pure idempotency decision logic
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestResolveRedeemAction_CodeNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
action := resolveRedeemAction(nil, nil)
|
||||
assert.Equal(t, redeemActionCreate, action, "nil code with nil error should create")
|
||||
}
|
||||
|
||||
func TestResolveRedeemAction_LookupError(t *testing.T) {
|
||||
t.Parallel()
|
||||
action := resolveRedeemAction(nil, errors.New("db connection lost"))
|
||||
assert.Equal(t, redeemActionCreate, action, "lookup error should fall back to create")
|
||||
}
|
||||
|
||||
func TestResolveRedeemAction_LookupErrorWithNonNilCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Edge case: both code and error are non-nil (shouldn't happen in practice,
|
||||
// but the function should still treat error as authoritative)
|
||||
code := &RedeemCode{Status: StatusUnused}
|
||||
action := resolveRedeemAction(code, errors.New("partial error"))
|
||||
assert.Equal(t, redeemActionCreate, action, "non-nil error should always result in create regardless of code")
|
||||
}
|
||||
|
||||
func TestResolveRedeemAction_CodeExistsAndUsed(t *testing.T) {
|
||||
t.Parallel()
|
||||
code := &RedeemCode{
|
||||
Code: "test-code-123",
|
||||
Status: StatusUsed,
|
||||
Type: RedeemTypeBalance,
|
||||
Value: 10.0,
|
||||
}
|
||||
action := resolveRedeemAction(code, nil)
|
||||
assert.Equal(t, redeemActionSkipCompleted, action, "used code should skip to completed")
|
||||
}
|
||||
|
||||
func TestResolveRedeemAction_CodeExistsAndUnused(t *testing.T) {
|
||||
t.Parallel()
|
||||
code := &RedeemCode{
|
||||
Code: "test-code-456",
|
||||
Status: StatusUnused,
|
||||
Type: RedeemTypeBalance,
|
||||
Value: 25.0,
|
||||
}
|
||||
action := resolveRedeemAction(code, nil)
|
||||
assert.Equal(t, redeemActionRedeem, action, "unused code should skip creation and proceed to redeem")
|
||||
}
|
||||
|
||||
func TestResolveRedeemAction_CodeExistsWithExpiredStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
// A code with a non-standard status (neither "unused" nor "used")
|
||||
// should NOT be treated as used, so it falls through to redeemActionRedeem.
|
||||
code := &RedeemCode{
|
||||
Code: "expired-code",
|
||||
Status: StatusExpired,
|
||||
}
|
||||
action := resolveRedeemAction(code, nil)
|
||||
assert.Equal(t, redeemActionRedeem, action, "expired-status code is not IsUsed(), should redeem")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Table-driven comprehensive test
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestResolveRedeemAction_Table(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
code *RedeemCode
|
||||
err error
|
||||
expected redeemAction
|
||||
}{
|
||||
{
|
||||
name: "nil code, nil error — first run",
|
||||
code: nil,
|
||||
err: nil,
|
||||
expected: redeemActionCreate,
|
||||
},
|
||||
{
|
||||
name: "nil code, lookup error — treat as not found",
|
||||
code: nil,
|
||||
err: ErrRedeemCodeNotFound,
|
||||
expected: redeemActionCreate,
|
||||
},
|
||||
{
|
||||
name: "nil code, generic DB error — treat as not found",
|
||||
code: nil,
|
||||
err: errors.New("connection refused"),
|
||||
expected: redeemActionCreate,
|
||||
},
|
||||
{
|
||||
name: "code exists, used — previous run completed redeem",
|
||||
code: &RedeemCode{Status: StatusUsed},
|
||||
err: nil,
|
||||
expected: redeemActionSkipCompleted,
|
||||
},
|
||||
{
|
||||
name: "code exists, unused — previous run created code but crashed before redeem",
|
||||
code: &RedeemCode{Status: StatusUnused},
|
||||
err: nil,
|
||||
expected: redeemActionRedeem,
|
||||
},
|
||||
{
|
||||
name: "code exists but error also set — error takes precedence",
|
||||
code: &RedeemCode{Status: StatusUsed},
|
||||
err: errors.New("unexpected"),
|
||||
expected: redeemActionCreate,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := resolveRedeemAction(tt.code, tt.err)
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// redeemAction enum value sanity
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestRedeemAction_DistinctValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Ensure the three actions have distinct values (iota correctness)
|
||||
assert.NotEqual(t, redeemActionCreate, redeemActionRedeem)
|
||||
assert.NotEqual(t, redeemActionCreate, redeemActionSkipCompleted)
|
||||
assert.NotEqual(t, redeemActionRedeem, redeemActionSkipCompleted)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RedeemCode.IsUsed / CanUse interaction with resolveRedeemAction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestResolveRedeemAction_IsUsedCanUseConsistency(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
usedCode := &RedeemCode{Status: StatusUsed}
|
||||
unusedCode := &RedeemCode{Status: StatusUnused}
|
||||
|
||||
// Verify our decision function is consistent with the domain model methods
|
||||
assert.True(t, usedCode.IsUsed())
|
||||
assert.False(t, usedCode.CanUse())
|
||||
assert.Equal(t, redeemActionSkipCompleted, resolveRedeemAction(usedCode, nil))
|
||||
|
||||
assert.False(t, unusedCode.IsUsed())
|
||||
assert.True(t, unusedCode.CanUse())
|
||||
assert.Equal(t, redeemActionRedeem, resolveRedeemAction(unusedCode, nil))
|
||||
}
|
||||
Reference in New Issue
Block a user