From e9aa1b13307586afbd81022d76db3c106dcd44e2 Mon Sep 17 00:00:00 2001 From: erio Date: Thu, 9 Apr 2026 23:21:38 +0800 Subject: [PATCH] fix(payment): use order's original provider instance for refund Previously gwRefund used registry.GetProvider(paymentType) which returns an arbitrary instance for that type. When multiple instances share the same payment type (e.g., two EasyPay merchants both supporting alipay), the refund would be sent to the wrong merchant. Now getRefundProvider() reads the order's ProviderInstanceID, loads that instance's config, and creates the correct provider. Falls back to registry lookup for legacy orders without an instance ID. chore: bump version to 0.1.108.142 --- backend/cmd/server/VERSION | 2 +- backend/internal/service/payment_refund.go | 45 ++++++++++++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/backend/cmd/server/VERSION b/backend/cmd/server/VERSION index 9a65da79f8..0f1ab4cbbc 100644 --- a/backend/cmd/server/VERSION +++ b/backend/cmd/server/VERSION @@ -1 +1 @@ -0.1.108.141 +0.1.108.142 diff --git a/backend/internal/service/payment_refund.go b/backend/internal/service/payment_refund.go index f3d2050993..58e8ae3065 100644 --- a/backend/internal/service/payment_refund.go +++ b/backend/internal/service/payment_refund.go @@ -12,6 +12,7 @@ import ( dbent "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/ent/paymentorder" "github.com/Wei-Shaw/sub2api/internal/payment" + "github.com/Wei-Shaw/sub2api/internal/payment/provider" infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" ) @@ -137,15 +138,51 @@ func (s *PaymentService) gwRefund(ctx context.Context, p *RefundPlan) error { s.writeAuditLog(ctx, p.Order.ID, "REFUND_NO_TRADE_NO", "admin", map[string]any{"detail": "skipped"}) return nil } - s.EnsureProviders(ctx) - prov, err := s.registry.GetProvider(p.Order.PaymentType) + + // 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 provider: %w", err) + return fmt.Errorf("get refund provider: %w", err) } - _, err = prov.Refund(ctx, payment.RefundRequest{TradeNo: p.Order.PaymentTradeNo, OrderID: p.Order.OutTradeNo, Amount: strconv.FormatFloat(p.GatewayAmount, 'f', 2, 64), Reason: p.Reason}) + _, err = prov.Refund(ctx, payment.RefundRequest{ + TradeNo: p.Order.PaymentTradeNo, + OrderID: p.Order.OutTradeNo, + Amount: strconv.FormatFloat(p.GatewayAmount, 'f', 2, 64), + Reason: p.Reason, + }) return err } +// getRefundProvider creates a provider using the order's original instance config. +// Falls back to registry lookup if instance ID is missing (legacy orders). +func (s *PaymentService) getRefundProvider(ctx context.Context, o *dbent.PaymentOrder) (payment.Provider, error) { + if o.ProviderInstanceID != nil && *o.ProviderInstanceID != "" { + instID, err := strconv.ParseInt(*o.ProviderInstanceID, 10, 64) + if err == nil { + cfg, err := s.loadBalancer.GetInstanceConfig(ctx, instID) + if err == nil { + providerKey := s.registry.GetProviderKey(o.PaymentType) + if providerKey == "" { + providerKey = o.PaymentType + } + p, err := provider.CreateProvider(providerKey, *o.ProviderInstanceID, cfg) + if err == nil { + return p, nil + } + slog.Warn("failed to create provider from instance, falling back to registry", + "instance_id", instID, "error", err) + } else { + slog.Warn("failed to get instance config for refund, falling back to registry", + "instance_id", instID, "error", err) + } + } + } + // Fallback: use registry (may pick wrong instance for multi-instance setups) + s.EnsureProviders(ctx) + return s.registry.GetProvider(o.PaymentType) +} + func (s *PaymentService) handleGwFail(ctx context.Context, p *RefundPlan, gErr error) (*RefundResult, error) { if s.RollbackRefund(ctx, p, gErr) { s.restoreStatus(ctx, p)