Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
267844eb
Commit
267844eb
authored
Apr 21, 2026
by
IanShaw027
Browse files
fix: fail closed for legacy refund provider resolution
parent
ebd053c8
Changes
2
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/payment_refund.go
View file @
267844eb
...
@@ -43,6 +43,37 @@ func (s *PaymentService) getOrderProviderInstance(ctx context.Context, o *dbent.
...
@@ -43,6 +43,37 @@ func (s *PaymentService) getOrderProviderInstance(ctx context.Context, o *dbent.
return
s
.
entClient
.
PaymentProviderInstance
.
Get
(
ctx
,
instID
)
return
s
.
entClient
.
PaymentProviderInstance
.
Get
(
ctx
,
instID
)
}
}
// getRefundOrderProviderInstance resolves the provider instance for refund paths.
// Refunds must be pinned to an explicit historical binding, so legacy
// "best-effort" provider guessing is intentionally not allowed here.
func
(
s
*
PaymentService
)
getRefundOrderProviderInstance
(
ctx
context
.
Context
,
o
*
dbent
.
PaymentOrder
)
(
*
dbent
.
PaymentProviderInstance
,
error
)
{
if
s
==
nil
||
s
.
entClient
==
nil
||
o
==
nil
{
return
nil
,
nil
}
if
snapshot
:=
psOrderProviderSnapshot
(
o
);
snapshot
!=
nil
{
return
s
.
resolveSnapshotOrderProviderInstance
(
ctx
,
o
,
snapshot
)
}
instIDStr
:=
strings
.
TrimSpace
(
psStringValue
(
o
.
ProviderInstanceID
))
if
instIDStr
==
""
{
return
nil
,
nil
}
instID
,
err
:=
strconv
.
ParseInt
(
instIDStr
,
10
,
64
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"order %d refund provider instance id is invalid: %s"
,
o
.
ID
,
instIDStr
)
}
inst
,
err
:=
s
.
entClient
.
PaymentProviderInstance
.
Get
(
ctx
,
instID
)
if
err
!=
nil
{
if
dbent
.
IsNotFound
(
err
)
{
return
nil
,
fmt
.
Errorf
(
"order %d refund provider instance %s is missing"
,
o
.
ID
,
instIDStr
)
}
return
nil
,
err
}
return
inst
,
nil
}
func
(
s
*
PaymentService
)
resolveUniqueLegacyOrderProviderInstance
(
ctx
context
.
Context
,
o
*
dbent
.
PaymentOrder
)
(
*
dbent
.
PaymentProviderInstance
,
error
)
{
func
(
s
*
PaymentService
)
resolveUniqueLegacyOrderProviderInstance
(
ctx
context
.
Context
,
o
*
dbent
.
PaymentOrder
)
(
*
dbent
.
PaymentProviderInstance
,
error
)
{
paymentType
:=
payment
.
GetBasePaymentType
(
strings
.
TrimSpace
(
o
.
PaymentType
))
paymentType
:=
payment
.
GetBasePaymentType
(
strings
.
TrimSpace
(
o
.
PaymentType
))
providerKey
:=
strings
.
TrimSpace
(
psStringValue
(
o
.
ProviderKey
))
providerKey
:=
strings
.
TrimSpace
(
psStringValue
(
o
.
ProviderKey
))
...
@@ -157,7 +188,7 @@ func (s *PaymentService) validateRefundRequest(ctx context.Context, oid, uid int
...
@@ -157,7 +188,7 @@ func (s *PaymentService) validateRefundRequest(ctx context.Context, oid, uid int
return
nil
,
infraerrors
.
BadRequest
(
"INVALID_STATUS"
,
"only completed orders can request refund"
)
return
nil
,
infraerrors
.
BadRequest
(
"INVALID_STATUS"
,
"only completed orders can request refund"
)
}
}
// Check provider instance allows user refund
// Check provider instance allows user refund
inst
,
err
:=
s
.
getOrderProviderInstance
(
ctx
,
o
)
inst
,
err
:=
s
.
get
Refund
OrderProviderInstance
(
ctx
,
o
)
if
err
!=
nil
||
inst
==
nil
{
if
err
!=
nil
||
inst
==
nil
{
return
nil
,
infraerrors
.
Forbidden
(
"USER_REFUND_DISABLED"
,
"refund is not available for this order"
)
return
nil
,
infraerrors
.
Forbidden
(
"USER_REFUND_DISABLED"
,
"refund is not available for this order"
)
}
}
...
@@ -177,7 +208,7 @@ func (s *PaymentService) PrepareRefund(ctx context.Context, oid int64, amt float
...
@@ -177,7 +208,7 @@ func (s *PaymentService) PrepareRefund(ctx context.Context, oid int64, amt float
return
nil
,
nil
,
infraerrors
.
BadRequest
(
"INVALID_STATUS"
,
"order status does not allow refund"
)
return
nil
,
nil
,
infraerrors
.
BadRequest
(
"INVALID_STATUS"
,
"order status does not allow refund"
)
}
}
// Check provider instance allows admin refund
// Check provider instance allows admin refund
inst
,
instErr
:=
s
.
getOrderProviderInstance
(
ctx
,
o
)
inst
,
instErr
:=
s
.
get
Refund
OrderProviderInstance
(
ctx
,
o
)
if
instErr
!=
nil
{
if
instErr
!=
nil
{
slog
.
Warn
(
"refund: provider instance lookup failed"
,
"orderID"
,
oid
,
"error"
,
instErr
)
slog
.
Warn
(
"refund: provider instance lookup failed"
,
"orderID"
,
oid
,
"error"
,
instErr
)
return
nil
,
nil
,
infraerrors
.
InternalServer
(
"PROVIDER_LOOKUP_FAILED"
,
"failed to look up payment provider for this order"
)
return
nil
,
nil
,
infraerrors
.
InternalServer
(
"PROVIDER_LOOKUP_FAILED"
,
"failed to look up payment provider for this order"
)
...
@@ -314,7 +345,14 @@ func (s *PaymentService) gwRefund(ctx context.Context, p *RefundPlan) error {
...
@@ -314,7 +345,14 @@ func (s *PaymentService) gwRefund(ctx context.Context, p *RefundPlan) error {
// getRefundProvider creates a provider using the order's original instance config.
// getRefundProvider creates a provider using the order's original instance config.
// Delegates to getOrderProvider which handles instance lookup and fallback.
// Delegates to getOrderProvider which handles instance lookup and fallback.
func
(
s
*
PaymentService
)
getRefundProvider
(
ctx
context
.
Context
,
o
*
dbent
.
PaymentOrder
)
(
payment
.
Provider
,
error
)
{
func
(
s
*
PaymentService
)
getRefundProvider
(
ctx
context
.
Context
,
o
*
dbent
.
PaymentOrder
)
(
payment
.
Provider
,
error
)
{
return
s
.
getOrderProvider
(
ctx
,
o
)
inst
,
err
:=
s
.
getRefundOrderProviderInstance
(
ctx
,
o
)
if
err
!=
nil
{
return
nil
,
err
}
if
inst
==
nil
{
return
nil
,
fmt
.
Errorf
(
"refund provider instance is unavailable for order %d"
,
o
.
ID
)
}
return
s
.
createProviderFromInstance
(
ctx
,
inst
)
}
}
func
(
s
*
PaymentService
)
handleGwFail
(
ctx
context
.
Context
,
p
*
RefundPlan
,
gErr
error
)
(
*
RefundResult
,
error
)
{
func
(
s
*
PaymentService
)
handleGwFail
(
ctx
context
.
Context
,
p
*
RefundPlan
,
gErr
error
)
(
*
RefundResult
,
error
)
{
...
...
backend/internal/service/payment_refund_test.go
0 → 100644
View file @
267844eb
//go:build unit
package
service
import
(
"context"
"testing"
"time"
"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
))
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment