| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678 |
- //go:build unit
- package service
- import (
- "context"
- "errors"
- "fmt"
- "strconv"
- "testing"
- "time"
- dbent "github.com/Wei-Shaw/sub2api/ent"
- "github.com/Wei-Shaw/sub2api/ent/paymentauditlog"
- "github.com/Wei-Shaw/sub2api/internal/payment"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/stretchr/testify/require"
- )
- func TestValidateRefundRequestRejectsLegacyGuessedProviderInstance(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- user, err := client.User.Create().
- SetEmail("refund-legacy@example.com").
- SetPasswordHash("hash").
- SetUsername("refund-legacy-user").
- Save(ctx)
- require.NoError(t, err)
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeAlipay).
- SetName("alipay-refund-instance").
- SetConfig("{}").
- SetSupportedTypes("alipay").
- SetEnabled(true).
- SetAllowUserRefund(true).
- SetRefundEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- order, err := client.PaymentOrder.Create().
- SetUserID(user.ID).
- SetUserEmail(user.Email).
- SetUserName(user.Username).
- SetAmount(88).
- SetPayAmount(88).
- SetFeeRate(0).
- SetRechargeCode("REFUND-LEGACY-ORDER").
- SetOutTradeNo("sub2_refund_legacy_order").
- SetPaymentType(payment.TypeAlipay).
- SetPaymentTradeNo("trade-legacy-refund").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusCompleted).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetPaidAt(time.Now()).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- Save(ctx)
- require.NoError(t, err)
- svc := &PaymentService{
- entClient: client,
- }
- _, err = svc.validateRefundRequest(ctx, order.ID, user.ID)
- require.Error(t, err)
- require.Equal(t, "USER_REFUND_DISABLED", infraerrors.Reason(err))
- }
- func TestPrepareRefundRejectsLegacyGuessedProviderInstance(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- user, err := client.User.Create().
- SetEmail("refund-legacy-admin@example.com").
- SetPasswordHash("hash").
- SetUsername("refund-legacy-admin-user").
- Save(ctx)
- require.NoError(t, err)
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeAlipay).
- SetName("alipay-refund-admin-instance").
- SetConfig("{}").
- SetSupportedTypes("alipay").
- SetEnabled(true).
- SetAllowUserRefund(true).
- SetRefundEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- order, err := client.PaymentOrder.Create().
- SetUserID(user.ID).
- SetUserEmail(user.Email).
- SetUserName(user.Username).
- SetAmount(188).
- SetPayAmount(188).
- SetFeeRate(0).
- SetRechargeCode("REFUND-LEGACY-ADMIN-ORDER").
- SetOutTradeNo("sub2_refund_legacy_admin_order").
- SetPaymentType(payment.TypeAlipay).
- SetPaymentTradeNo("trade-legacy-admin-refund").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusCompleted).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetPaidAt(time.Now()).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- Save(ctx)
- require.NoError(t, err)
- svc := &PaymentService{
- entClient: client,
- }
- plan, result, err := svc.PrepareRefund(ctx, order.ID, 0, "", false, false)
- require.Nil(t, plan)
- require.Nil(t, result)
- require.Error(t, err)
- require.Equal(t, "REFUND_DISABLED", infraerrors.Reason(err))
- }
- func TestPrepDeductBalanceRequiresForceWhenBalanceIsInsufficient(t *testing.T) {
- for _, tc := range []struct {
- name string
- balance float64
- force bool
- wantDeduct float64
- wantWarning bool
- }{
- {name: "insufficient balance", balance: 40, wantWarning: true},
- {name: "forced insufficient balance", balance: 40, force: true, wantDeduct: 40},
- {name: "equal balance", balance: 100, wantDeduct: 100},
- } {
- t.Run(tc.name, func(t *testing.T) {
- plan := &RefundPlan{RefundAmount: 100}
- svc := &PaymentService{userRepo: &mockUserRepo{getByIDUser: &User{Balance: tc.balance}}}
- result := svc.prepDeduct(context.Background(), &dbent.PaymentOrder{
- UserID: 1,
- OrderType: payment.OrderTypeBalance,
- }, plan, tc.force)
- if tc.wantWarning {
- require.NotNil(t, result)
- require.False(t, result.Success)
- require.True(t, result.RequireForce)
- require.Equal(t, "user balance is insufficient for deduction, use force", result.Warning)
- require.Zero(t, plan.BalanceToDeduct)
- return
- }
- require.Nil(t, result)
- require.Equal(t, payment.DeductionTypeBalance, plan.DeductionType)
- require.Equal(t, tc.wantDeduct, plan.BalanceToDeduct)
- })
- }
- }
- func TestExecuteRefundUsesActualAvailableBalanceDeduction(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- user, err := client.User.Create().
- SetEmail("refund-execute-clamp@example.com").
- SetPasswordHash("hash").
- SetUsername("refund-execute-clamp").
- Save(ctx)
- require.NoError(t, err)
- order, err := client.PaymentOrder.Create().
- SetUserID(user.ID).
- SetUserEmail(user.Email).
- SetUserName(user.Username).
- SetAmount(100).
- SetPayAmount(100).
- SetFeeRate(0).
- SetRechargeCode("REFUND-EXECUTE-CLAMP").
- SetOutTradeNo("refund_execute_clamp").
- SetPaymentType(payment.TypeStripe).
- SetPaymentTradeNo("").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusCompleted).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetPaidAt(time.Now()).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- Save(ctx)
- require.NoError(t, err)
- repo := &mockUserRepo{deductAvailableBalanceFn: func(_ context.Context, id int64, amount float64) (float64, error) {
- require.Equal(t, user.ID, id)
- require.Equal(t, 100.0, amount)
- return 25, nil
- }}
- plan := &RefundPlan{
- OrderID: order.ID, Order: order, RefundAmount: 100, GatewayAmount: 100,
- Reason: "concurrent spend", Force: true, DeductionType: payment.DeductionTypeBalance, BalanceToDeduct: 100,
- }
- result, err := (&PaymentService{entClient: client, userRepo: repo}).ExecuteRefund(ctx, plan)
- require.NoError(t, err)
- require.True(t, result.Success)
- require.Equal(t, 25.0, plan.BalanceToDeduct)
- require.Equal(t, 25.0, result.BalanceDeducted)
- audit, err := client.PaymentAuditLog.Query().
- Where(paymentauditlog.OrderIDEQ(strconv.FormatInt(order.ID, 10)), paymentauditlog.ActionEQ("REFUND_SUCCESS")).
- Only(ctx)
- require.NoError(t, err)
- require.Contains(t, audit.Detail, `"balanceDeducted":25`)
- }
- func TestGwRefundRejectsAlipayMerchantIdentitySnapshotMismatch(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- user, err := client.User.Create().
- SetEmail("refund-snapshot-mismatch@example.com").
- SetPasswordHash("hash").
- SetUsername("refund-snapshot-mismatch-user").
- Save(ctx)
- require.NoError(t, err)
- inst, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeAlipay).
- SetName("alipay-refund-mismatch-instance").
- SetConfig(encryptWebhookProviderConfig(t, map[string]string{
- "appId": "runtime-alipay-app",
- "privateKey": "runtime-private-key",
- })).
- SetSupportedTypes("alipay").
- SetEnabled(true).
- SetRefundEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- instID := strconv.FormatInt(inst.ID, 10)
- order, err := client.PaymentOrder.Create().
- SetUserID(user.ID).
- SetUserEmail(user.Email).
- SetUserName(user.Username).
- SetAmount(88).
- SetPayAmount(88).
- SetFeeRate(0).
- SetRechargeCode("REFUND-SNAPSHOT-MISMATCH-ORDER").
- SetOutTradeNo("sub2_refund_snapshot_mismatch_order").
- SetPaymentType(payment.TypeAlipay).
- SetPaymentTradeNo("trade-refund-snapshot-mismatch").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusCompleted).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetPaidAt(time.Now()).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- SetProviderInstanceID(instID).
- SetProviderKey(payment.TypeAlipay).
- SetProviderSnapshot(map[string]any{
- "schema_version": 2,
- "provider_instance_id": instID,
- "provider_key": payment.TypeAlipay,
- "merchant_app_id": "expected-alipay-app",
- }).
- Save(ctx)
- require.NoError(t, err)
- svc := &PaymentService{
- entClient: client,
- loadBalancer: newWebhookProviderTestLoadBalancer(client),
- }
- _, err = svc.gwRefund(ctx, &RefundPlan{
- OrderID: order.ID,
- Order: order,
- RefundAmount: order.Amount,
- GatewayAmount: order.Amount,
- Reason: "snapshot mismatch",
- })
- require.ErrorContains(t, err, "alipay app_id mismatch")
- }
- func TestCalculateGatewayRefundAmountUsesCurrencyPrecision(t *testing.T) {
- require.InDelta(t, 6.173, calculateGatewayRefundAmount(100, 12.345, 50, "KWD"), 1e-12)
- require.InDelta(t, 12.345, calculateGatewayRefundAmount(100, 12.345, 100, "KWD"), 1e-12)
- require.InDelta(t, 52, calculateGatewayRefundAmount(100, 103, 50, "JPY"), 1e-12)
- }
- func TestFormatGatewayRefundAmountUsesOrderCurrency(t *testing.T) {
- order := &dbent.PaymentOrder{
- ProviderSnapshot: map[string]any{
- "currency": "KWD",
- },
- }
- require.Equal(t, "12.345", formatGatewayRefundAmount(12.345, order))
- }
- func TestValidateRefundProviderResponseAcceptsPending(t *testing.T) {
- require.NoError(t, validateRefundProviderResponse(&payment.RefundResponse{Status: payment.ProviderStatusPending}))
- require.NoError(t, validateRefundProviderResponse(&payment.RefundResponse{Status: payment.ProviderStatusSuccess}))
- require.Error(t, validateRefundProviderResponse(&payment.RefundResponse{Status: payment.ProviderStatusFailed}))
- require.Error(t, validateRefundProviderResponse(nil))
- }
- func TestFinishRefundPendingMarksOrderPendingAndRollsBackDeduction(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- user, err := client.User.Create().
- SetEmail("refund-pending@example.com").
- SetPasswordHash("hash").
- SetUsername("refund-pending-user").
- Save(ctx)
- require.NoError(t, err)
- order, err := client.PaymentOrder.Create().
- SetUserID(user.ID).
- SetUserEmail(user.Email).
- SetUserName(user.Username).
- SetAmount(100).
- SetPayAmount(100).
- SetFeeRate(0).
- SetRechargeCode("REFUND-PENDING-ORDER").
- SetOutTradeNo("sub2_refund_pending_order").
- SetPaymentType(payment.TypeStripe).
- SetPaymentTradeNo("pi_refund_pending").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusRefunding).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetPaidAt(time.Now()).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- Save(ctx)
- require.NoError(t, err)
- var rolledBack float64
- userRepo := &mockUserRepo{}
- userRepo.updateBalanceFn = func(ctx context.Context, id int64, amount float64) error {
- require.Equal(t, user.ID, id)
- rolledBack += amount
- return nil
- }
- svc := &PaymentService{
- entClient: client,
- userRepo: userRepo,
- }
- plan := &RefundPlan{
- OrderID: order.ID,
- Order: order,
- RefundAmount: 40,
- GatewayAmount: 40,
- Reason: "gateway accepted but not final",
- Force: true,
- DeductionType: payment.DeductionTypeBalance,
- BalanceToDeduct: 40,
- }
- result, err := svc.finishRefund(ctx, plan, &payment.RefundResponse{Status: payment.ProviderStatusPending})
- require.NoError(t, err)
- require.NotNil(t, result)
- require.False(t, result.Success)
- require.Contains(t, result.Warning, "pending confirmation")
- require.Equal(t, 40.0, rolledBack)
- require.Zero(t, plan.BalanceToDeduct)
- reloaded, err := client.PaymentOrder.Get(ctx, order.ID)
- require.NoError(t, err)
- require.Equal(t, OrderStatusRefundPending, reloaded.Status)
- require.Equal(t, 40.0, reloaded.RefundAmount)
- require.NotNil(t, reloaded.RefundReason)
- require.Equal(t, "gateway accepted but not final", *reloaded.RefundReason)
- require.Nil(t, reloaded.RefundAt)
- pendingAudits, err := client.PaymentAuditLog.Query().
- Where(paymentauditlog.OrderIDEQ(strconv.FormatInt(order.ID, 10)), paymentauditlog.ActionEQ("REFUND_PENDING")).
- Count(ctx)
- require.NoError(t, err)
- require.Equal(t, 1, pendingAudits)
- successAudits, err := client.PaymentAuditLog.Query().
- Where(paymentauditlog.OrderIDEQ(strconv.FormatInt(order.ID, 10)), paymentauditlog.ActionEQ("REFUND_SUCCESS")).
- Count(ctx)
- require.NoError(t, err)
- require.Zero(t, successAudits)
- }
- func TestFinishRefundSuccessStatusesFinalize(t *testing.T) {
- for _, status := range []string{payment.ProviderStatusSuccess, payment.ProviderStatusRefunded} {
- t.Run(status, func(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- user, err := client.User.Create().
- SetEmail("refund-success-" + status + "@example.com").
- SetPasswordHash("hash").
- SetUsername("refund-success-" + status).
- Save(ctx)
- require.NoError(t, err)
- order, err := client.PaymentOrder.Create().
- SetUserID(user.ID).
- SetUserEmail(user.Email).
- SetUserName(user.Username).
- SetAmount(100).
- SetPayAmount(100).
- SetFeeRate(0).
- SetRechargeCode("REFUND-SUCCESS-" + status).
- SetOutTradeNo("sub2_refund_success_" + status).
- SetPaymentType(payment.TypeStripe).
- SetPaymentTradeNo("pi_refund_success_" + status).
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusRefunding).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetPaidAt(time.Now()).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- Save(ctx)
- require.NoError(t, err)
- svc := &PaymentService{entClient: client}
- plan := &RefundPlan{
- OrderID: order.ID,
- Order: order,
- RefundAmount: 100,
- GatewayAmount: 100,
- Reason: "final success",
- DeductionType: payment.DeductionTypeBalance,
- BalanceToDeduct: 100,
- }
- result, err := svc.finishRefund(ctx, plan, &payment.RefundResponse{Status: status})
- require.NoError(t, err)
- require.NotNil(t, result)
- require.True(t, result.Success)
- require.Equal(t, 100.0, result.BalanceDeducted)
- reloaded, err := client.PaymentOrder.Get(ctx, order.ID)
- require.NoError(t, err)
- require.Equal(t, OrderStatusRefunded, reloaded.Status)
- require.NotNil(t, reloaded.RefundAt)
- successAudits, err := client.PaymentAuditLog.Query().
- Where(paymentauditlog.OrderIDEQ(strconv.FormatInt(order.ID, 10)), paymentauditlog.ActionEQ("REFUND_SUCCESS")).
- Count(ctx)
- require.NoError(t, err)
- require.Equal(t, 1, successAudits)
- pendingAudits, err := client.PaymentAuditLog.Query().
- Where(paymentauditlog.OrderIDEQ(strconv.FormatInt(order.ID, 10)), paymentauditlog.ActionEQ("REFUND_PENDING")).
- Count(ctx)
- require.NoError(t, err)
- require.Zero(t, pendingAudits)
- })
- }
- }
- func TestQueryAndFinalizeRefundFinalizesProviderStatuses(t *testing.T) {
- for _, tc := range []struct {
- name string
- status string
- wantStatus string
- wantDeduct float64
- available float64
- }{
- {name: "success", status: payment.ProviderStatusSuccess, wantStatus: OrderStatusRefunded, wantDeduct: 100, available: 100},
- {name: "success clamps current balance", status: payment.ProviderStatusSuccess, wantStatus: OrderStatusRefunded, wantDeduct: 35, available: 35},
- {name: "failed", status: payment.ProviderStatusFailed, wantStatus: OrderStatusRefundFailed},
- {name: "pending", status: payment.ProviderStatusPending, wantStatus: OrderStatusRefundPending},
- } {
- t.Run(tc.name, func(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- order := createPendingRefundOrderForTest(t, ctx, client, "query-finalize-"+tc.name)
- var deducted float64
- svc := &PaymentService{
- entClient: client,
- loadBalancer: &captureLoadBalancer{},
- userRepo: &mockUserRepo{deductAvailableBalanceFn: func(ctx context.Context, id int64, amount float64) (float64, error) {
- deducted += tc.available
- return tc.available, nil
- }},
- }
- restore := replacePaymentProviderFactoryForTest(t, &refundQueryProviderTestDouble{
- refundResponse: &payment.RefundResponse{RefundID: "rf_test", Status: tc.status},
- })
- defer restore()
- result, err := svc.QueryAndFinalizeRefund(ctx, order.ID)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, tc.status == payment.ProviderStatusSuccess, result.Success)
- require.Equal(t, tc.wantDeduct, deducted)
- if tc.status == payment.ProviderStatusSuccess {
- require.Equal(t, tc.wantDeduct, result.BalanceDeducted)
- audit, err := client.PaymentAuditLog.Query().
- Where(paymentauditlog.OrderIDEQ(strconv.FormatInt(order.ID, 10)), paymentauditlog.ActionEQ("REFUND_SUCCESS")).
- Only(ctx)
- require.NoError(t, err)
- require.Contains(t, audit.Detail, fmt.Sprintf(`"balanceDeducted":%v`, tc.wantDeduct))
- }
- reloaded, err := client.PaymentOrder.Get(ctx, order.ID)
- require.NoError(t, err)
- require.Equal(t, tc.wantStatus, reloaded.Status)
- })
- }
- }
- func TestFinalizePendingRefundSuccessRejectsStaleCallerBeforeSecondDeduction(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- order := createPendingRefundOrderForTest(t, ctx, client, "finalize-stale")
- deductions := 0
- svc := &PaymentService{
- entClient: client,
- userRepo: &mockUserRepo{deductAvailableBalanceFn: func(ctx context.Context, id int64, amount float64) (float64, error) {
- require.NotNil(t, dbent.TxFromContext(ctx))
- deductions++
- return amount, nil
- }},
- }
- first, err := svc.finalizePendingRefundSuccess(ctx, svc.refundFinalizePlan(order))
- require.NoError(t, err)
- require.True(t, first.Success)
- second, err := svc.finalizePendingRefundSuccess(ctx, svc.refundFinalizePlan(order))
- require.Nil(t, second)
- require.Error(t, err)
- require.Equal(t, "CONFLICT", infraerrors.Reason(err))
- require.Equal(t, 1, deductions)
- successAudits, err := client.PaymentAuditLog.Query().
- Where(paymentauditlog.OrderIDEQ(strconv.FormatInt(order.ID, 10)), paymentauditlog.ActionEQ("REFUND_SUCCESS")).
- Count(ctx)
- require.NoError(t, err)
- require.Equal(t, 1, successAudits)
- }
- func TestFinalizePendingRefundSuccessRollsBackPostDeductionFailure(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- order := createPendingRefundOrderForTest(t, ctx, client, "finalize-rollback")
- _, err := client.User.UpdateOneID(order.UserID).SetBalance(100).Save(ctx)
- require.NoError(t, err)
- svc := &PaymentService{
- entClient: client,
- userRepo: &mockUserRepo{deductAvailableBalanceFn: func(ctx context.Context, id int64, amount float64) (float64, error) {
- tx := dbent.TxFromContext(ctx)
- require.NotNil(t, tx)
- if _, updateErr := tx.Client().User.UpdateOneID(id).AddBalance(-amount).Save(ctx); updateErr != nil {
- return 0, updateErr
- }
- return 0, errors.New("injected failure after deduction")
- }},
- }
- result, err := svc.finalizePendingRefundSuccess(ctx, svc.refundFinalizePlan(order))
- require.Nil(t, result)
- require.ErrorContains(t, err, "injected failure after deduction")
- user, err := client.User.Get(ctx, order.UserID)
- require.NoError(t, err)
- require.Equal(t, 100.0, user.Balance)
- reloaded, err := client.PaymentOrder.Get(ctx, order.ID)
- require.NoError(t, err)
- require.Equal(t, OrderStatusRefundPending, reloaded.Status)
- successAudits, err := client.PaymentAuditLog.Query().
- Where(paymentauditlog.OrderIDEQ(strconv.FormatInt(order.ID, 10)), paymentauditlog.ActionEQ("REFUND_SUCCESS")).
- Count(ctx)
- require.NoError(t, err)
- require.Zero(t, successAudits)
- }
- func TestQueryAndFinalizeRefundUnsupportedProviderReturnsClearError(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- order := createPendingRefundOrderForTest(t, ctx, client, "query-finalize-unsupported")
- svc := &PaymentService{entClient: client, loadBalancer: &captureLoadBalancer{}}
- restore := replacePaymentProviderFactoryForTest(t, refundProviderTestDouble{})
- defer restore()
- result, err := svc.QueryAndFinalizeRefund(ctx, order.ID)
- require.Nil(t, result)
- require.Error(t, err)
- require.Equal(t, "REFUND_QUERY_UNSUPPORTED", infraerrors.Reason(err))
- }
- func createPendingRefundOrderForTest(t *testing.T, ctx context.Context, client *dbent.Client, suffix string) *dbent.PaymentOrder {
- t.Helper()
- user, err := client.User.Create().
- SetEmail(suffix + "@example.com").
- SetPasswordHash("hash").
- SetUsername(suffix).
- Save(ctx)
- require.NoError(t, err)
- inst, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeStripe).
- SetName(suffix + "-provider").
- SetConfig("{}").
- SetSupportedTypes("stripe").
- SetEnabled(true).
- SetRefundEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- order, err := client.PaymentOrder.Create().
- SetUserID(user.ID).
- SetUserEmail(user.Email).
- SetUserName(user.Username).
- SetAmount(100).
- SetPayAmount(100).
- SetFeeRate(0).
- SetRechargeCode("REFUND-" + suffix).
- SetOutTradeNo("sub2_" + suffix).
- SetPaymentType(payment.TypeStripe).
- SetPaymentTradeNo("pi_" + suffix).
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusRefundPending).
- SetRefundAmount(100).
- SetRefundReason("pending refund").
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetPaidAt(time.Now()).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- SetProviderInstanceID(strconv.FormatInt(inst.ID, 10)).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.PaymentAuditLog.Create().
- SetOrderID(strconv.FormatInt(order.ID, 10)).
- SetAction("REFUND_PENDING").
- SetOperator("admin").
- SetDetail(`{"refundID":"rf_test","deductionRollbackOK":true}`).
- Save(ctx)
- require.NoError(t, err)
- return order
- }
- func replacePaymentProviderFactoryForTest(t *testing.T, prov payment.Provider) func() {
- t.Helper()
- original := createPaymentProviderFromInstance
- createPaymentProviderFromInstance = func(providerKey, instanceID string, config map[string]string) (payment.Provider, error) {
- return prov, nil
- }
- return func() { createPaymentProviderFromInstance = original }
- }
- type refundProviderTestDouble struct{}
- func (refundProviderTestDouble) Name() string { return "refund-test" }
- func (refundProviderTestDouble) ProviderKey() string {
- return payment.TypeStripe
- }
- func (refundProviderTestDouble) SupportedTypes() []payment.PaymentType {
- return []payment.PaymentType{payment.TypeStripe}
- }
- func (refundProviderTestDouble) CreatePayment(context.Context, payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
- return nil, nil
- }
- func (refundProviderTestDouble) QueryOrder(context.Context, string) (*payment.QueryOrderResponse, error) {
- return nil, nil
- }
- func (refundProviderTestDouble) VerifyNotification(context.Context, string, map[string]string) (*payment.PaymentNotification, error) {
- return nil, nil
- }
- func (refundProviderTestDouble) Refund(context.Context, payment.RefundRequest) (*payment.RefundResponse, error) {
- return nil, nil
- }
- type refundQueryProviderTestDouble struct {
- refundProviderTestDouble
- refundResponse *payment.RefundResponse
- }
- func (p *refundQueryProviderTestDouble) QueryRefund(context.Context, payment.RefundQueryRequest) (*payment.RefundResponse, error) {
- return p.refundResponse, nil
- }
|