From 7316d83027a8f8d73fec994d6867431344c9863e Mon Sep 17 00:00:00 2001 From: wucm667 Date: Sat, 27 Jun 2026 12:11:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(payment):=20=E5=8C=BA=E5=88=86=E9=80=80?= =?UTF-8?q?=E6=AC=BE=20pending=20=E5=B9=B6=E6=94=B6=E6=95=9B=E5=8C=BF?= =?UTF-8?q?=E5=90=8D=E6=9F=A5=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/payment_handler.go | 47 +++++- .../handler/payment_handler_resume_test.go | 51 +++--- backend/internal/payment/types.go | 1 + backend/internal/service/payment_refund.go | 78 ++++++++- .../internal/service/payment_refund_test.go | 153 +++++++++++++++++- backend/internal/service/payment_service.go | 3 +- .../admin/payment/AdminOrderTable.vue | 1 + .../admin/payment/AdminRefundDialog.vue | 2 +- .../components/payment/OrderStatusBadge.vue | 1 + frontend/src/components/payment/orderUtils.ts | 1 + frontend/src/i18n/locales/en.ts | 1 + frontend/src/i18n/locales/zh.ts | 1 + frontend/src/types/payment.ts | 1 + .../views/admin/orders/AdminOrdersView.vue | 1 + 14 files changed, 302 insertions(+), 40 deletions(-) diff --git a/backend/internal/handler/payment_handler.go b/backend/internal/handler/payment_handler.go index f3c16f5d7c..4047cbfd27 100644 --- a/backend/internal/handler/payment_handler.go +++ b/backend/internal/handler/payment_handler.go @@ -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. diff --git a/backend/internal/handler/payment_handler_resume_test.go b/backend/internal/handler/payment_handler_resume_test.go index 752088320b..21fd8ad763 100644 --- a/backend/internal/handler/payment_handler_resume_test.go +++ b/backend/internal/handler/payment_handler_resume_test.go @@ -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) { diff --git a/backend/internal/payment/types.go b/backend/internal/payment/types.go index d27d9696b2..9f08d96149 100644 --- a/backend/internal/payment/types.go +++ b/backend/internal/payment/types.go @@ -31,6 +31,7 @@ const ( OrderStatusFailed = "FAILED" OrderStatusRefundRequested = "REFUND_REQUESTED" OrderStatusRefunding = "REFUNDING" + OrderStatusRefundPending = "REFUND_PENDING" OrderStatusPartiallyRefunded = "PARTIALLY_REFUNDED" OrderStatusRefunded = "REFUNDED" OrderStatusRefundFailed = "REFUND_FAILED" diff --git a/backend/internal/service/payment_refund.go b/backend/internal/service/payment_refund.go index 4df3e41f77..866b9e4542 100644 --- a/backend/internal/service/payment_refund.go +++ b/backend/internal/service/payment_refund.go @@ -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 { diff --git a/backend/internal/service/payment_refund_test.go b/backend/internal/service/payment_refund_test.go index c275bb65ce..c0b7587360 100644 --- a/backend/internal/service/payment_refund_test.go +++ b/backend/internal/service/payment_refund_test.go @@ -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) + }) + } +} diff --git a/backend/internal/service/payment_service.go b/backend/internal/service/payment_service.go index c1deb667ef..3698a59c0b 100644 --- a/backend/internal/service/payment_service.go +++ b/backend/internal/service/payment_service.go @@ -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 diff --git a/frontend/src/components/admin/payment/AdminOrderTable.vue b/frontend/src/components/admin/payment/AdminOrderTable.vue index dda0ec27b0..5290866cae 100644 --- a/frontend/src/components/admin/payment/AdminOrderTable.vue +++ b/frontend/src/components/admin/payment/AdminOrderTable.vue @@ -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') }, ]) diff --git a/frontend/src/components/admin/payment/AdminRefundDialog.vue b/frontend/src/components/admin/payment/AdminRefundDialog.vue index f5f5412d54..f58b810388 100644 --- a/frontend/src/components/admin/payment/AdminRefundDialog.vue +++ b/frontend/src/components/admin/payment/AdminRefundDialog.vue @@ -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 diff --git a/frontend/src/components/payment/OrderStatusBadge.vue b/frontend/src/components/payment/OrderStatusBadge.vue index a0425492a2..87975ef5eb 100644 --- a/frontend/src/components/payment/OrderStatusBadge.vue +++ b/frontend/src/components/payment/OrderStatusBadge.vue @@ -28,6 +28,7 @@ const statusMap: Record = { 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' }, diff --git a/frontend/src/components/payment/orderUtils.ts b/frontend/src/components/payment/orderUtils.ts index e89eaeac28..974a4c4199 100644 --- a/frontend/src/components/payment/orderUtils.ts +++ b/frontend/src/components/payment/orderUtils.ts @@ -13,6 +13,7 @@ const STATUS_BADGE_MAP: Record = { 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', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 34f5e891c8..5bd19d6923 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -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', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index cf51d588d9..23a2f6ffa7 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -7192,6 +7192,7 @@ export default { failed: '失败', refund_requested: '退款申请中', refunding: '退款中', + refund_pending: '退款处理中', refunded: '已退款', partially_refunded: '部分退款', refund_failed: '退款失败', diff --git a/frontend/src/types/payment.ts b/frontend/src/types/payment.ts index d4c4fddaef..ac20b82e4f 100644 --- a/frontend/src/types/payment.ts +++ b/frontend/src/types/payment.ts @@ -14,6 +14,7 @@ export type OrderStatus = | 'FAILED' | 'REFUND_REQUESTED' | 'REFUNDING' + | 'REFUND_PENDING' | 'PARTIALLY_REFUNDED' | 'REFUNDED' | 'REFUND_FAILED' diff --git a/frontend/src/views/admin/orders/AdminOrdersView.vue b/frontend/src/views/admin/orders/AdminOrdersView.vue index a005741c89..6d9bf5dfee 100644 --- a/frontend/src/views/admin/orders/AdminOrdersView.vue +++ b/frontend/src/views/admin/orders/AdminOrdersView.vue @@ -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') }, ])