mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:19:18 +08:00
fix(payment): 区分退款 pending 并收敛匿名查单
This commit is contained in:
@@ -454,8 +454,9 @@ func (h *PaymentHandler) VerifyOrder(c *gin.Context) {
|
||||
response.Success(c, sanitizePaymentOrderForResponse(order))
|
||||
}
|
||||
|
||||
// PublicOrderResult is the limited order info returned by the public verify endpoint.
|
||||
// No user details are exposed — only payment status information.
|
||||
// PublicOrderResult is returned after a signed resume-token lookup. The token
|
||||
// proves possession of the checkout session, so the result keeps the legacy
|
||||
// frontend contract needed by payment result pages.
|
||||
type PublicOrderResult struct {
|
||||
ID int64 `json:"id"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
@@ -478,6 +479,18 @@ type PublicOrderResult struct {
|
||||
PlanID *int64 `json:"plan_id,omitempty"`
|
||||
}
|
||||
|
||||
// PublicOrderVerifyResult is returned by the legacy anonymous out_trade_no
|
||||
// lookup. Keep this intentionally minimal because out_trade_no is not secret.
|
||||
type PublicOrderVerifyResult struct {
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
Status string `json:"status"`
|
||||
Paid bool `json:"paid"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
PaidAt *time.Time `json:"paid_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
}
|
||||
|
||||
func buildPublicOrderResult(order *dbent.PaymentOrder) PublicOrderResult {
|
||||
return PublicOrderResult{
|
||||
ID: order.ID,
|
||||
@@ -502,6 +515,34 @@ func buildPublicOrderResult(order *dbent.PaymentOrder) PublicOrderResult {
|
||||
}
|
||||
}
|
||||
|
||||
func buildPublicOrderVerifyResult(order *dbent.PaymentOrder) PublicOrderVerifyResult {
|
||||
return PublicOrderVerifyResult{
|
||||
OutTradeNo: order.OutTradeNo,
|
||||
Status: order.Status,
|
||||
Paid: publicOrderStatusPaid(order.Status),
|
||||
CreatedAt: order.CreatedAt,
|
||||
ExpiresAt: order.ExpiresAt,
|
||||
PaidAt: order.PaidAt,
|
||||
CompletedAt: order.CompletedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func publicOrderStatusPaid(status string) bool {
|
||||
switch status {
|
||||
case service.OrderStatusPaid,
|
||||
service.OrderStatusCompleted,
|
||||
service.OrderStatusRefundRequested,
|
||||
service.OrderStatusRefunding,
|
||||
service.OrderStatusRefundPending,
|
||||
service.OrderStatusPartiallyRefunded,
|
||||
service.OrderStatusRefunded,
|
||||
service.OrderStatusRefundFailed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyOrderPublic keeps the legacy anonymous out_trade_no lookup available as
|
||||
// a compatibility path for older result pages and staggered deploys.
|
||||
// POST /api/v1/payment/public/orders/verify
|
||||
@@ -517,7 +558,7 @@ func (h *PaymentHandler) VerifyOrderPublic(c *gin.Context) {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, buildPublicOrderResult(order))
|
||||
response.Success(c, buildPublicOrderVerifyResult(order))
|
||||
}
|
||||
|
||||
// ResolveOrderPublicByResumeToken resolves a payment order from a signed resume token.
|
||||
|
||||
@@ -135,35 +135,34 @@ func TestVerifyOrderPublicReturnsLegacyOrderState(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var resp struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
ID int64 `json:"id"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
Amount float64 `json:"amount"`
|
||||
PayAmount float64 `json:"pay_amount"`
|
||||
FeeRate float64 `json:"fee_rate"`
|
||||
Currency string `json:"currency"`
|
||||
PaymentType string `json:"payment_type"`
|
||||
OrderType string `json:"order_type"`
|
||||
Status string `json:"status"`
|
||||
RefundAmount float64 `json:"refund_amount"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
} `json:"data"`
|
||||
Code int `json:"code"`
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
|
||||
require.Equal(t, 0, resp.Code)
|
||||
require.Equal(t, order.ID, resp.Data.ID)
|
||||
require.Equal(t, "legacy-order-no", resp.Data.OutTradeNo)
|
||||
require.Equal(t, 90.64, resp.Data.PayAmount)
|
||||
require.Equal(t, 0.03, resp.Data.FeeRate)
|
||||
require.Equal(t, "HKD", resp.Data.Currency)
|
||||
require.Equal(t, payment.TypeAlipay, resp.Data.PaymentType)
|
||||
require.Equal(t, payment.OrderTypeBalance, resp.Data.OrderType)
|
||||
require.Equal(t, service.OrderStatusPending, resp.Data.Status)
|
||||
require.Equal(t, 0.0, resp.Data.RefundAmount)
|
||||
require.NotEmpty(t, resp.Data.CreatedAt)
|
||||
require.NotEmpty(t, resp.Data.ExpiresAt)
|
||||
require.Equal(t, "legacy-order-no", resp.Data["out_trade_no"])
|
||||
require.Equal(t, service.OrderStatusPending, resp.Data["status"])
|
||||
require.Equal(t, false, resp.Data["paid"])
|
||||
require.NotEmpty(t, resp.Data["created_at"])
|
||||
require.NotEmpty(t, resp.Data["expires_at"])
|
||||
for _, field := range []string{
|
||||
"id",
|
||||
"amount",
|
||||
"pay_amount",
|
||||
"fee_rate",
|
||||
"currency",
|
||||
"payment_type",
|
||||
"order_type",
|
||||
"refund_amount",
|
||||
"refund_reason",
|
||||
"refund_requested_at",
|
||||
"refund_requested_by",
|
||||
"refund_request_reason",
|
||||
"plan_id",
|
||||
} {
|
||||
require.NotContains(t, resp.Data, field)
|
||||
}
|
||||
require.NotZero(t, order.ID)
|
||||
}
|
||||
|
||||
func TestResolveOrderPublicByResumeTokenReturnsFrontendContractFields(t *testing.T) {
|
||||
|
||||
@@ -31,6 +31,7 @@ const (
|
||||
OrderStatusFailed = "FAILED"
|
||||
OrderStatusRefundRequested = "REFUND_REQUESTED"
|
||||
OrderStatusRefunding = "REFUNDING"
|
||||
OrderStatusRefundPending = "REFUND_PENDING"
|
||||
OrderStatusPartiallyRefunded = "PARTIALLY_REFUNDED"
|
||||
OrderStatusRefunded = "REFUNDED"
|
||||
OrderStatusRefundFailed = "REFUND_FAILED"
|
||||
|
||||
@@ -316,29 +316,30 @@ func (s *PaymentService) ExecuteRefund(ctx context.Context, p *RefundPlan) (*Ref
|
||||
p.SubDaysToDeduct = 0
|
||||
}
|
||||
}
|
||||
if err := s.gwRefund(ctx, p); err != nil {
|
||||
resp, err := s.gwRefund(ctx, p)
|
||||
if err != nil {
|
||||
return s.handleGwFail(ctx, p, err)
|
||||
}
|
||||
return s.markRefundOk(ctx, p)
|
||||
return s.finishRefund(ctx, p, resp)
|
||||
}
|
||||
|
||||
func (s *PaymentService) gwRefund(ctx context.Context, p *RefundPlan) error {
|
||||
func (s *PaymentService) gwRefund(ctx context.Context, p *RefundPlan) (*payment.RefundResponse, error) {
|
||||
if p.Order.PaymentTradeNo == "" {
|
||||
s.writeAuditLog(ctx, p.Order.ID, "REFUND_NO_TRADE_NO", "admin", map[string]any{"detail": "skipped"})
|
||||
return nil
|
||||
return &payment.RefundResponse{Status: payment.ProviderStatusSuccess}, nil
|
||||
}
|
||||
|
||||
// Use the exact provider instance that created this order, not a random one
|
||||
// from the registry. Each instance has its own merchant credentials.
|
||||
prov, err := s.getRefundProvider(ctx, p.Order)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get refund provider: %w", err)
|
||||
return nil, fmt.Errorf("get refund provider: %w", err)
|
||||
}
|
||||
if err := validateProviderSnapshotMetadata(p.Order, prov.ProviderKey(), providerMerchantIdentityMetadata(prov)); err != nil {
|
||||
s.writeAuditLog(ctx, p.Order.ID, "REFUND_PROVIDER_METADATA_MISMATCH", "admin", map[string]any{
|
||||
"detail": err.Error(),
|
||||
})
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
resp, err := prov.Refund(ctx, payment.RefundRequest{
|
||||
TradeNo: p.Order.PaymentTradeNo,
|
||||
@@ -347,9 +348,15 @@ func (s *PaymentService) gwRefund(ctx context.Context, p *RefundPlan) error {
|
||||
Reason: p.Reason,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
if resp != nil && strings.TrimSpace(resp.Status) == payment.ProviderStatusPending {
|
||||
return resp, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return validateRefundProviderResponse(resp)
|
||||
if err := validateRefundProviderResponse(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func formatGatewayRefundAmount(amount float64, order *dbent.PaymentOrder) string {
|
||||
@@ -371,6 +378,20 @@ func validateRefundProviderResponse(resp *payment.RefundResponse) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PaymentService) finishRefund(ctx context.Context, p *RefundPlan, resp *payment.RefundResponse) (*RefundResult, error) {
|
||||
if err := validateRefundProviderResponse(resp); err != nil {
|
||||
return s.handleGwFail(ctx, p, err)
|
||||
}
|
||||
switch strings.TrimSpace(resp.Status) {
|
||||
case payment.ProviderStatusSuccess, payment.ProviderStatusRefunded:
|
||||
return s.markRefundOk(ctx, p)
|
||||
case payment.ProviderStatusPending:
|
||||
return s.markRefundPending(ctx, p)
|
||||
default:
|
||||
return s.handleGwFail(ctx, p, fmt.Errorf("payment refund returned unknown status: %s", strings.TrimSpace(resp.Status)))
|
||||
}
|
||||
}
|
||||
|
||||
// getRefundProvider creates a provider using the order's original instance config.
|
||||
// Delegates to getOrderProvider which handles instance lookup and fallback.
|
||||
func (s *PaymentService) getRefundProvider(ctx context.Context, o *dbent.PaymentOrder) (payment.Provider, error) {
|
||||
@@ -410,6 +431,47 @@ func (s *PaymentService) markRefundOk(ctx context.Context, p *RefundPlan) (*Refu
|
||||
return &RefundResult{Success: true, BalanceDeducted: p.BalanceToDeduct, SubDaysDeducted: p.SubDaysToDeduct}, nil
|
||||
}
|
||||
|
||||
func (s *PaymentService) markRefundPending(ctx context.Context, p *RefundPlan) (*RefundResult, error) {
|
||||
balanceDeducted := p.BalanceToDeduct
|
||||
subDaysDeducted := p.SubDaysToDeduct
|
||||
rollbackOK := s.RollbackRefund(ctx, p, nil)
|
||||
if rollbackOK {
|
||||
p.BalanceToDeduct = 0
|
||||
p.SubDaysToDeduct = 0
|
||||
}
|
||||
|
||||
_, err := s.entClient.PaymentOrder.UpdateOneID(p.OrderID).
|
||||
SetStatus(OrderStatusRefundPending).
|
||||
SetRefundAmount(p.RefundAmount).
|
||||
SetRefundReason(p.Reason).
|
||||
ClearRefundAt().
|
||||
SetForceRefund(p.Force).
|
||||
ClearFailedAt().
|
||||
ClearFailedReason().
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mark refund pending: %w", err)
|
||||
}
|
||||
|
||||
detail := map[string]any{
|
||||
"refundAmount": p.RefundAmount,
|
||||
"reason": p.Reason,
|
||||
"force": p.Force,
|
||||
"balanceDeducted": p.BalanceToDeduct,
|
||||
"subDaysDeducted": p.SubDaysToDeduct,
|
||||
"balanceRolledBack": balanceDeducted,
|
||||
"subDaysRolledBack": subDaysDeducted,
|
||||
"deductionRollbackOK": rollbackOK,
|
||||
}
|
||||
s.writeAuditLog(ctx, p.OrderID, "REFUND_PENDING", "admin", detail)
|
||||
|
||||
warning := "gateway refund is pending confirmation"
|
||||
if !rollbackOK {
|
||||
warning += "; refund deduction rollback failed"
|
||||
}
|
||||
return &RefundResult{Success: false, Warning: warning}, nil
|
||||
}
|
||||
|
||||
func (s *PaymentService) RollbackRefund(ctx context.Context, p *RefundPlan, gErr error) bool {
|
||||
if p.DeductionType == payment.DeductionTypeBalance && p.BalanceToDeduct > 0 {
|
||||
if err := s.userRepo.UpdateBalance(ctx, p.Order.UserID, p.BalanceToDeduct); err != nil {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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"
|
||||
@@ -176,7 +177,7 @@ func TestGwRefundRejectsAlipayMerchantIdentitySnapshotMismatch(t *testing.T) {
|
||||
loadBalancer: newWebhookProviderTestLoadBalancer(client),
|
||||
}
|
||||
|
||||
err = svc.gwRefund(ctx, &RefundPlan{
|
||||
_, err = svc.gwRefund(ctx, &RefundPlan{
|
||||
OrderID: order.ID,
|
||||
Order: order,
|
||||
RefundAmount: order.Amount,
|
||||
@@ -208,3 +209,153 @@ func TestValidateRefundProviderResponseAcceptsPending(t *testing.T) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ const (
|
||||
OrderStatusFailed = payment.OrderStatusFailed
|
||||
OrderStatusRefundRequested = payment.OrderStatusRefundRequested
|
||||
OrderStatusRefunding = payment.OrderStatusRefunding
|
||||
OrderStatusRefundPending = payment.OrderStatusRefundPending
|
||||
OrderStatusPartiallyRefunded = payment.OrderStatusPartiallyRefunded
|
||||
OrderStatusRefunded = payment.OrderStatusRefunded
|
||||
OrderStatusRefundFailed = payment.OrderStatusRefundFailed
|
||||
@@ -252,7 +253,7 @@ func (s *PaymentService) loadProviders(ctx context.Context) {
|
||||
|
||||
func psIsRefundStatus(s string) bool {
|
||||
switch s {
|
||||
case OrderStatusRefundRequested, OrderStatusRefunding, OrderStatusPartiallyRefunded, OrderStatusRefunded, OrderStatusRefundFailed:
|
||||
case OrderStatusRefundRequested, OrderStatusRefunding, OrderStatusRefundPending, OrderStatusPartiallyRefunded, OrderStatusRefunded, OrderStatusRefundFailed:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -210,6 +210,7 @@ const statusFilterOptions = computed(() => [
|
||||
{ value: 'FAILED', label: t('payment.status.failed') },
|
||||
{ value: 'REFUNDED', label: t('payment.status.refunded') },
|
||||
{ value: 'REFUND_REQUESTED', label: t('payment.status.refund_requested') },
|
||||
{ value: 'REFUND_PENDING', label: t('payment.status.refund_pending') },
|
||||
{ value: 'REFUND_FAILED', label: t('payment.status.refund_failed') },
|
||||
])
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ const form = reactive({
|
||||
force: false,
|
||||
})
|
||||
|
||||
// In REFUND_REQUESTED status, refund_amount is the REQUESTED amount, not actually refunded.
|
||||
// In REFUND_REQUESTED / REFUND_PENDING status, refund_amount is requested/pending, not actually refunded.
|
||||
// Only PARTIALLY_REFUNDED / REFUNDED have real refund amounts.
|
||||
const actuallyRefunded = computed(() => {
|
||||
if (!props.order) return 0
|
||||
|
||||
@@ -28,6 +28,7 @@ const statusMap: Record<OrderStatus, { key: string; class: string }> = {
|
||||
FAILED: { key: 'payment.status.failed', class: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400' },
|
||||
REFUND_REQUESTED: { key: 'payment.status.refund_requested', class: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400' },
|
||||
REFUNDING: { key: 'payment.status.refunding', class: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400' },
|
||||
REFUND_PENDING: { key: 'payment.status.refund_pending', class: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400' },
|
||||
REFUNDED: { key: 'payment.status.refunded', class: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400' },
|
||||
PARTIALLY_REFUNDED: { key: 'payment.status.partially_refunded', class: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400' },
|
||||
REFUND_FAILED: { key: 'payment.status.refund_failed', class: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400' },
|
||||
|
||||
@@ -13,6 +13,7 @@ const STATUS_BADGE_MAP: Record<string, string> = {
|
||||
FAILED: 'badge-danger',
|
||||
REFUND_REQUESTED: 'badge-warning',
|
||||
REFUNDING: 'badge-warning',
|
||||
REFUND_PENDING: 'badge-warning',
|
||||
PARTIALLY_REFUNDED: 'badge-warning',
|
||||
REFUNDED: 'badge-info',
|
||||
REFUND_FAILED: 'badge-danger',
|
||||
|
||||
@@ -7012,6 +7012,7 @@ export default {
|
||||
failed: 'Failed',
|
||||
refund_requested: 'Refund Requested',
|
||||
refunding: 'Refunding',
|
||||
refund_pending: 'Refund Pending',
|
||||
refunded: 'Refunded',
|
||||
partially_refunded: 'Partially Refunded',
|
||||
refund_failed: 'Refund Failed',
|
||||
|
||||
@@ -7192,6 +7192,7 @@ export default {
|
||||
failed: '失败',
|
||||
refund_requested: '退款申请中',
|
||||
refunding: '退款中',
|
||||
refund_pending: '退款处理中',
|
||||
refunded: '已退款',
|
||||
partially_refunded: '部分退款',
|
||||
refund_failed: '退款失败',
|
||||
|
||||
@@ -14,6 +14,7 @@ export type OrderStatus =
|
||||
| 'FAILED'
|
||||
| 'REFUND_REQUESTED'
|
||||
| 'REFUNDING'
|
||||
| 'REFUND_PENDING'
|
||||
| 'PARTIALLY_REFUNDED'
|
||||
| 'REFUNDED'
|
||||
| 'REFUND_FAILED'
|
||||
|
||||
@@ -190,6 +190,7 @@ const statusFilterOptions = computed(() => [
|
||||
{ value: 'FAILED', label: t('payment.status.failed') },
|
||||
{ value: 'REFUNDED', label: t('payment.status.refunded') },
|
||||
{ value: 'REFUND_REQUESTED', label: t('payment.status.refund_requested') },
|
||||
{ value: 'REFUND_PENDING', label: t('payment.status.refund_pending') },
|
||||
{ value: 'REFUND_FAILED', label: t('payment.status.refund_failed') },
|
||||
])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user