Files
sub2api/backend/internal/service/payment_refund_test.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

679 lines
22 KiB
Go

//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
}