| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866 |
- //go:build unit
- package service
- import (
- "context"
- "database/sql"
- "testing"
- "time"
- dbent "github.com/Wei-Shaw/sub2api/ent"
- "github.com/Wei-Shaw/sub2api/ent/enttest"
- "github.com/Wei-Shaw/sub2api/internal/payment"
- "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
- "github.com/stretchr/testify/require"
- "entgo.io/ent/dialect"
- entsql "entgo.io/ent/dialect/sql"
- _ "modernc.org/sqlite"
- )
- type paymentOrderLifecycleQueryProvider struct {
- key string
- lastQueryTradeNo string
- lastCancelTradeNo string
- queryCalls int
- cancelCalls int
- responses []*payment.QueryOrderResponse
- resp *payment.QueryOrderResponse
- }
- type paymentOrderLifecycleRedeemRepo struct {
- codesByCode map[string]*RedeemCode
- useCalls []struct {
- id int64
- userID int64
- }
- }
- func (p *paymentOrderLifecycleQueryProvider) Name() string {
- return "payment-order-lifecycle-query-provider"
- }
- func (p *paymentOrderLifecycleQueryProvider) ProviderKey() string {
- if p.key != "" {
- return p.key
- }
- return payment.TypeAlipay
- }
- func (p *paymentOrderLifecycleQueryProvider) SupportedTypes() []payment.PaymentType {
- return []payment.PaymentType{p.ProviderKey()}
- }
- func (p *paymentOrderLifecycleQueryProvider) CreatePayment(context.Context, payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
- panic("unexpected call")
- }
- func (p *paymentOrderLifecycleQueryProvider) QueryOrder(_ context.Context, tradeNo string) (*payment.QueryOrderResponse, error) {
- p.lastQueryTradeNo = tradeNo
- p.queryCalls++
- if len(p.responses) > 0 {
- resp := p.responses[0]
- if len(p.responses) > 1 {
- p.responses = p.responses[1:]
- }
- return resp, nil
- }
- return p.resp, nil
- }
- func (p *paymentOrderLifecycleQueryProvider) VerifyNotification(context.Context, string, map[string]string) (*payment.PaymentNotification, error) {
- panic("unexpected call")
- }
- func (p *paymentOrderLifecycleQueryProvider) Refund(context.Context, payment.RefundRequest) (*payment.RefundResponse, error) {
- panic("unexpected call")
- }
- func (p *paymentOrderLifecycleQueryProvider) CancelPayment(_ context.Context, tradeNo string) error {
- p.lastCancelTradeNo = tradeNo
- p.cancelCalls++
- return nil
- }
- func (r *paymentOrderLifecycleRedeemRepo) Create(context.Context, *RedeemCode) error {
- panic("unexpected call")
- }
- func (r *paymentOrderLifecycleRedeemRepo) CreateBatch(context.Context, []RedeemCode) error {
- panic("unexpected call")
- }
- func (r *paymentOrderLifecycleRedeemRepo) GetByID(_ context.Context, id int64) (*RedeemCode, error) {
- for _, code := range r.codesByCode {
- if code.ID != id {
- continue
- }
- cloned := *code
- return &cloned, nil
- }
- return nil, ErrRedeemCodeNotFound
- }
- func (r *paymentOrderLifecycleRedeemRepo) GetByCode(_ context.Context, code string) (*RedeemCode, error) {
- redeemCode, ok := r.codesByCode[code]
- if !ok {
- return nil, ErrRedeemCodeNotFound
- }
- cloned := *redeemCode
- return &cloned, nil
- }
- func (r *paymentOrderLifecycleRedeemRepo) Update(context.Context, *RedeemCode) error {
- panic("unexpected call")
- }
- func (r *paymentOrderLifecycleRedeemRepo) BatchUpdate(context.Context, []int64, RedeemCodeBatchUpdateFields) (int64, error) {
- panic("unexpected call")
- }
- func (r *paymentOrderLifecycleRedeemRepo) Delete(context.Context, int64) error {
- panic("unexpected call")
- }
- func (r *paymentOrderLifecycleRedeemRepo) Use(_ context.Context, id, userID int64) error {
- for code, redeemCode := range r.codesByCode {
- if redeemCode.ID != id {
- continue
- }
- now := time.Now().UTC()
- redeemCode.Status = StatusUsed
- redeemCode.UsedBy = &userID
- redeemCode.UsedAt = &now
- r.codesByCode[code] = redeemCode
- r.useCalls = append(r.useCalls, struct {
- id int64
- userID int64
- }{id: id, userID: userID})
- return nil
- }
- return ErrRedeemCodeNotFound
- }
- func (r *paymentOrderLifecycleRedeemRepo) List(context.Context, pagination.PaginationParams) ([]RedeemCode, *pagination.PaginationResult, error) {
- panic("unexpected call")
- }
- func (r *paymentOrderLifecycleRedeemRepo) ListWithFilters(context.Context, pagination.PaginationParams, string, string, string) ([]RedeemCode, *pagination.PaginationResult, error) {
- panic("unexpected call")
- }
- func (r *paymentOrderLifecycleRedeemRepo) ListByUser(context.Context, int64, int) ([]RedeemCode, error) {
- panic("unexpected call")
- }
- func (r *paymentOrderLifecycleRedeemRepo) ListByUserPaginated(context.Context, int64, pagination.PaginationParams, string) ([]RedeemCode, *pagination.PaginationResult, error) {
- panic("unexpected call")
- }
- func (r *paymentOrderLifecycleRedeemRepo) SumPositiveBalanceByUser(context.Context, int64) (float64, error) {
- panic("unexpected call")
- }
- func TestVerifyOrderByOutTradeNoBackfillsTradeNoFromPaidQuery(t *testing.T) {
- ctx := context.Background()
- client := newPaymentOrderLifecycleTestClient(t)
- user, err := client.User.Create().
- SetEmail("checkpaid@example.com").
- SetPasswordHash("hash").
- SetUsername("checkpaid-user").
- 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("CHECKPAID-UPSTREAM-TRADE-NO").
- SetOutTradeNo("sub2_checkpaid_trade_no_missing").
- SetPaymentType(payment.TypeAlipay).
- SetPaymentTradeNo("").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusPending).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- Save(ctx)
- require.NoError(t, err)
- userRepo := &mockUserRepo{
- getByIDUser: &User{
- ID: user.ID,
- Email: user.Email,
- Username: user.Username,
- Balance: 0,
- },
- }
- userRepo.updateBalanceFn = func(ctx context.Context, id int64, amount float64) error {
- require.Equal(t, user.ID, id)
- if userRepo.getByIDUser != nil {
- userRepo.getByIDUser.Balance += amount
- }
- return nil
- }
- redeemRepo := &paymentOrderLifecycleRedeemRepo{
- codesByCode: map[string]*RedeemCode{
- order.RechargeCode: {
- ID: 1,
- Code: order.RechargeCode,
- Type: RedeemTypeBalance,
- Value: order.Amount,
- Status: StatusUnused,
- },
- },
- }
- redeemService := NewRedeemService(
- redeemRepo,
- userRepo,
- nil,
- nil,
- nil,
- client,
- nil,
- nil,
- )
- registry := payment.NewRegistry()
- provider := &paymentOrderLifecycleQueryProvider{
- resp: &payment.QueryOrderResponse{
- TradeNo: "upstream-trade-123",
- Status: payment.ProviderStatusPaid,
- Amount: 88,
- },
- }
- registry.Register(provider)
- svc := &PaymentService{
- entClient: client,
- registry: registry,
- redeemService: redeemService,
- userRepo: userRepo,
- providersLoaded: true,
- }
- got, err := svc.VerifyOrderByOutTradeNo(ctx, order.OutTradeNo, user.ID)
- require.NoError(t, err)
- require.Equal(t, order.OutTradeNo, provider.lastQueryTradeNo)
- require.Equal(t, OrderStatusCompleted, got.Status)
- require.Equal(t, "upstream-trade-123", got.PaymentTradeNo)
- reloaded, err := client.PaymentOrder.Get(ctx, order.ID)
- require.NoError(t, err)
- require.Equal(t, OrderStatusCompleted, reloaded.Status)
- require.Equal(t, "upstream-trade-123", reloaded.PaymentTradeNo)
- require.Equal(t, 88.0, userRepo.getByIDUser.Balance)
- require.Len(t, redeemRepo.useCalls, 1)
- require.Equal(t, int64(1), redeemRepo.useCalls[0].id)
- require.Equal(t, user.ID, redeemRepo.useCalls[0].userID)
- }
- func TestVerifyOrderByOutTradeNoRetriesZeroAmountPaidQueryOnce(t *testing.T) {
- ctx := context.Background()
- client := newPaymentOrderLifecycleTestClient(t)
- user, err := client.User.Create().
- SetEmail("checkpaid-retry@example.com").
- SetPasswordHash("hash").
- SetUsername("checkpaid-retry-user").
- 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("CHECKPAID-UPSTREAM-RETRY").
- SetOutTradeNo("sub2_checkpaid_retry_zero_amount").
- SetPaymentType(payment.TypeAlipay).
- SetPaymentTradeNo("").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusPending).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- Save(ctx)
- require.NoError(t, err)
- userRepo := &mockUserRepo{
- getByIDUser: &User{
- ID: user.ID,
- Email: user.Email,
- Username: user.Username,
- Balance: 0,
- },
- }
- userRepo.updateBalanceFn = func(ctx context.Context, id int64, amount float64) error {
- require.Equal(t, user.ID, id)
- if userRepo.getByIDUser != nil {
- userRepo.getByIDUser.Balance += amount
- }
- return nil
- }
- redeemRepo := &paymentOrderLifecycleRedeemRepo{
- codesByCode: map[string]*RedeemCode{
- order.RechargeCode: {
- ID: 1,
- Code: order.RechargeCode,
- Type: RedeemTypeBalance,
- Value: order.Amount,
- Status: StatusUnused,
- },
- },
- }
- redeemService := NewRedeemService(
- redeemRepo,
- userRepo,
- nil,
- nil,
- nil,
- client,
- nil,
- nil,
- )
- registry := payment.NewRegistry()
- provider := &paymentOrderLifecycleQueryProvider{
- responses: []*payment.QueryOrderResponse{
- {
- TradeNo: "upstream-trade-zero",
- Status: payment.ProviderStatusPaid,
- Amount: 0,
- },
- {
- TradeNo: "upstream-trade-retry",
- Status: payment.ProviderStatusPaid,
- Amount: 88,
- },
- },
- }
- registry.Register(provider)
- svc := &PaymentService{
- entClient: client,
- registry: registry,
- redeemService: redeemService,
- userRepo: userRepo,
- providersLoaded: true,
- }
- got, err := svc.VerifyOrderByOutTradeNo(ctx, order.OutTradeNo, user.ID)
- require.NoError(t, err)
- require.Equal(t, 2, provider.queryCalls)
- require.Equal(t, OrderStatusCompleted, got.Status)
- require.Equal(t, "upstream-trade-retry", got.PaymentTradeNo)
- }
- func TestVerifyOrderByOutTradeNoRejectsPaidQueryWithZeroAmount(t *testing.T) {
- ctx := context.Background()
- client := newPaymentOrderLifecycleTestClient(t)
- user, err := client.User.Create().
- SetEmail("checkpaid-zero-amount@example.com").
- SetPasswordHash("hash").
- SetUsername("checkpaid-zero-amount-user").
- 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("CHECKPAID-ZERO-AMOUNT").
- SetOutTradeNo("sub2_checkpaid_zero_amount").
- SetPaymentType(payment.TypeAlipay).
- SetPaymentTradeNo("").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusPending).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- Save(ctx)
- require.NoError(t, err)
- userRepo := &mockUserRepo{
- getByIDUser: &User{
- ID: user.ID,
- Email: user.Email,
- Username: user.Username,
- Balance: 0,
- },
- }
- redeemRepo := &paymentOrderLifecycleRedeemRepo{
- codesByCode: map[string]*RedeemCode{
- order.RechargeCode: {
- ID: 1,
- Code: order.RechargeCode,
- Type: RedeemTypeBalance,
- Value: order.Amount,
- Status: StatusUnused,
- },
- },
- }
- redeemService := NewRedeemService(
- redeemRepo,
- userRepo,
- nil,
- nil,
- nil,
- client,
- nil,
- nil,
- )
- registry := payment.NewRegistry()
- provider := &paymentOrderLifecycleQueryProvider{
- resp: &payment.QueryOrderResponse{
- TradeNo: "upstream-trade-zero",
- Status: payment.ProviderStatusPaid,
- Amount: 0,
- },
- }
- registry.Register(provider)
- svc := &PaymentService{
- entClient: client,
- registry: registry,
- redeemService: redeemService,
- userRepo: userRepo,
- providersLoaded: true,
- }
- got, err := svc.VerifyOrderByOutTradeNo(ctx, order.OutTradeNo, user.ID)
- require.NoError(t, err)
- require.Equal(t, order.OutTradeNo, provider.lastQueryTradeNo)
- require.Equal(t, OrderStatusPending, got.Status)
- require.Empty(t, got.PaymentTradeNo)
- reloaded, err := client.PaymentOrder.Get(ctx, order.ID)
- require.NoError(t, err)
- require.Equal(t, OrderStatusPending, reloaded.Status)
- require.Empty(t, reloaded.PaymentTradeNo)
- require.Equal(t, 0.0, userRepo.getByIDUser.Balance)
- require.Empty(t, redeemRepo.useCalls)
- }
- func TestVerifyOrderByOutTradeNoDoesNotCancelUnpaidUpstreamOrder(t *testing.T) {
- ctx := context.Background()
- client := newPaymentOrderLifecycleTestClient(t)
- user, err := client.User.Create().
- SetEmail("checkpaid-pending@example.com").
- SetPasswordHash("hash").
- SetUsername("checkpaid-pending-user").
- 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("CHECKPAID-PENDING").
- SetOutTradeNo("sub2_checkpaid_pending").
- SetPaymentType(payment.TypeAlipay).
- SetPaymentTradeNo("").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusPending).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- Save(ctx)
- require.NoError(t, err)
- registry := payment.NewRegistry()
- provider := &paymentOrderLifecycleQueryProvider{
- resp: &payment.QueryOrderResponse{
- TradeNo: order.OutTradeNo,
- Status: payment.ProviderStatusPending,
- Amount: 0,
- },
- }
- registry.Register(provider)
- svc := &PaymentService{
- entClient: client,
- registry: registry,
- providersLoaded: true,
- }
- got, err := svc.VerifyOrderByOutTradeNo(ctx, order.OutTradeNo, user.ID)
- require.NoError(t, err)
- require.Equal(t, OrderStatusPending, got.Status)
- require.Equal(t, order.OutTradeNo, provider.lastQueryTradeNo)
- require.Zero(t, provider.cancelCalls)
- reloaded, err := client.PaymentOrder.Get(ctx, order.ID)
- require.NoError(t, err)
- require.Equal(t, OrderStatusPending, reloaded.Status)
- }
- func TestCancelOrderStillClosesUnpaidUpstreamOrder(t *testing.T) {
- ctx := context.Background()
- client := newPaymentOrderLifecycleTestClient(t)
- user, err := client.User.Create().
- SetEmail("cancel-pending@example.com").
- SetPasswordHash("hash").
- SetUsername("cancel-pending-user").
- 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("CANCEL-PENDING").
- SetOutTradeNo("sub2_cancel_pending").
- SetPaymentType(payment.TypeAlipay).
- SetPaymentTradeNo("").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusPending).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- Save(ctx)
- require.NoError(t, err)
- registry := payment.NewRegistry()
- provider := &paymentOrderLifecycleQueryProvider{
- resp: &payment.QueryOrderResponse{
- TradeNo: order.OutTradeNo,
- Status: payment.ProviderStatusPending,
- Amount: 0,
- },
- }
- registry.Register(provider)
- svc := &PaymentService{
- entClient: client,
- registry: registry,
- providersLoaded: true,
- }
- outcome, err := svc.CancelOrder(ctx, order.ID, user.ID)
- require.NoError(t, err)
- require.Equal(t, checkPaidResultCancelled, outcome)
- require.Equal(t, order.OutTradeNo, provider.lastCancelTradeNo)
- require.Equal(t, 1, provider.cancelCalls)
- reloaded, err := client.PaymentOrder.Get(ctx, order.ID)
- require.NoError(t, err)
- require.Equal(t, OrderStatusCancelled, reloaded.Status)
- }
- func TestReconcilePendingPaymentOrdersBackfillsPaidOrder(t *testing.T) {
- ctx := context.Background()
- client := newPaymentOrderLifecycleTestClient(t)
- user, err := client.User.Create().
- SetEmail("wxpay-reconcile@example.com").
- SetPasswordHash("hash").
- SetUsername("wxpay-reconcile-user").
- Save(ctx)
- require.NoError(t, err)
- order, err := client.PaymentOrder.Create().
- SetUserID(user.ID).
- SetUserEmail(user.Email).
- SetUserName(user.Username).
- SetAmount(50).
- SetPayAmount(50).
- SetFeeRate(0).
- SetRechargeCode("WXPAY-RECONCILE").
- SetOutTradeNo("sub2_wxpay_reconcile").
- SetPaymentType(payment.TypeWxpay).
- SetPaymentTradeNo("").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusPending).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- Save(ctx)
- require.NoError(t, err)
- userRepo := &mockUserRepo{
- getByIDUser: &User{
- ID: user.ID,
- Email: user.Email,
- Username: user.Username,
- Balance: 0,
- },
- }
- userRepo.updateBalanceFn = func(ctx context.Context, id int64, amount float64) error {
- require.Equal(t, user.ID, id)
- if userRepo.getByIDUser != nil {
- userRepo.getByIDUser.Balance += amount
- }
- return nil
- }
- redeemRepo := &paymentOrderLifecycleRedeemRepo{
- codesByCode: map[string]*RedeemCode{
- order.RechargeCode: {
- ID: 1,
- Code: order.RechargeCode,
- Type: RedeemTypeBalance,
- Value: order.Amount,
- Status: StatusUnused,
- },
- },
- }
- redeemService := NewRedeemService(
- redeemRepo,
- userRepo,
- nil,
- nil,
- nil,
- client,
- nil,
- nil,
- )
- registry := payment.NewRegistry()
- provider := &paymentOrderLifecycleQueryProvider{
- key: payment.TypeWxpay,
- resp: &payment.QueryOrderResponse{
- TradeNo: "wxpay-upstream-trade-123",
- Status: payment.ProviderStatusPaid,
- Amount: 50,
- Metadata: map[string]string{
- "trade_state": "SUCCESS",
- },
- },
- }
- registry.Register(provider)
- svc := &PaymentService{
- entClient: client,
- registry: registry,
- redeemService: redeemService,
- userRepo: userRepo,
- providersLoaded: true,
- }
- recovered, err := svc.ReconcilePendingPaymentOrders(ctx)
- require.NoError(t, err)
- require.Equal(t, 1, recovered)
- require.Equal(t, order.OutTradeNo, provider.lastQueryTradeNo)
- require.Zero(t, provider.cancelCalls)
- reloaded, err := client.PaymentOrder.Get(ctx, order.ID)
- require.NoError(t, err)
- require.Equal(t, OrderStatusCompleted, reloaded.Status)
- require.Equal(t, "wxpay-upstream-trade-123", reloaded.PaymentTradeNo)
- require.Equal(t, 50.0, userRepo.getByIDUser.Balance)
- require.Len(t, redeemRepo.useCalls, 1)
- }
- func TestReconcilePendingPaymentOrdersQueriesAlipayOrder(t *testing.T) {
- ctx := context.Background()
- client := newPaymentOrderLifecycleTestClient(t)
- user, err := client.User.Create().
- SetEmail("alipay-reconcile@example.com").
- SetPasswordHash("hash").
- SetUsername("alipay-reconcile-user").
- Save(ctx)
- require.NoError(t, err)
- order, err := client.PaymentOrder.Create().
- SetUserID(user.ID).
- SetUserEmail(user.Email).
- SetUserName(user.Username).
- SetAmount(50).
- SetPayAmount(50).
- SetFeeRate(0).
- SetRechargeCode("ALIPAY-RECONCILE").
- SetOutTradeNo("sub2_alipay_reconcile").
- SetPaymentType(payment.TypeAlipay).
- SetPaymentTradeNo("").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusPending).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- Save(ctx)
- require.NoError(t, err)
- registry := payment.NewRegistry()
- provider := &paymentOrderLifecycleQueryProvider{
- key: payment.TypeAlipay,
- resp: &payment.QueryOrderResponse{
- TradeNo: order.OutTradeNo,
- Status: payment.ProviderStatusPending,
- },
- }
- registry.Register(provider)
- svc := &PaymentService{
- entClient: client,
- registry: registry,
- providersLoaded: true,
- }
- recovered, err := svc.ReconcilePendingPaymentOrders(ctx)
- require.NoError(t, err)
- require.Zero(t, recovered)
- require.Equal(t, 1, provider.queryCalls)
- require.Equal(t, order.OutTradeNo, provider.lastQueryTradeNo)
- }
- func TestVerifyOrderByOutTradeNoUsesOutTradeNoWhenPaymentTradeNoAlreadyExistsForAlipay(t *testing.T) {
- ctx := context.Background()
- client := newPaymentOrderLifecycleTestClient(t)
- user, err := client.User.Create().
- SetEmail("checkpaid-existing-trade@example.com").
- SetPasswordHash("hash").
- SetUsername("checkpaid-existing-trade-user").
- 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("CHECKPAID-EXISTING-TRADE-NO").
- SetOutTradeNo("sub2_checkpaid_use_out_trade_no").
- SetPaymentType(payment.TypeAlipay).
- SetPaymentTradeNo("upstream-trade-existing").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusPending).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- Save(ctx)
- require.NoError(t, err)
- userRepo := &mockUserRepo{
- getByIDUser: &User{
- ID: user.ID,
- Email: user.Email,
- Username: user.Username,
- Balance: 0,
- },
- }
- userRepo.updateBalanceFn = func(ctx context.Context, id int64, amount float64) error {
- require.Equal(t, user.ID, id)
- if userRepo.getByIDUser != nil {
- userRepo.getByIDUser.Balance += amount
- }
- return nil
- }
- redeemRepo := &paymentOrderLifecycleRedeemRepo{
- codesByCode: map[string]*RedeemCode{
- order.RechargeCode: {
- ID: 1,
- Code: order.RechargeCode,
- Type: RedeemTypeBalance,
- Value: order.Amount,
- Status: StatusUnused,
- },
- },
- }
- redeemService := NewRedeemService(
- redeemRepo,
- userRepo,
- nil,
- nil,
- nil,
- client,
- nil,
- nil,
- )
- registry := payment.NewRegistry()
- provider := &paymentOrderLifecycleQueryProvider{
- resp: &payment.QueryOrderResponse{
- TradeNo: "upstream-trade-existing",
- Status: payment.ProviderStatusPaid,
- Amount: 88,
- },
- }
- registry.Register(provider)
- svc := &PaymentService{
- entClient: client,
- registry: registry,
- redeemService: redeemService,
- userRepo: userRepo,
- providersLoaded: true,
- }
- got, err := svc.VerifyOrderByOutTradeNo(ctx, order.OutTradeNo, user.ID)
- require.NoError(t, err)
- require.Equal(t, order.OutTradeNo, provider.lastQueryTradeNo)
- require.Equal(t, "upstream-trade-existing", got.PaymentTradeNo)
- }
- func TestPaymentOrderAllowsRegistryFallbackOnlyForLegacyOrdersWithoutPinnedProviderState(t *testing.T) {
- t.Parallel()
- require.True(t, paymentOrderAllowsRegistryFallback(&dbent.PaymentOrder{
- PaymentType: payment.TypeAlipay,
- }))
- instanceID := "12"
- require.False(t, paymentOrderAllowsRegistryFallback(&dbent.PaymentOrder{
- PaymentType: payment.TypeAlipay,
- ProviderInstanceID: &instanceID,
- }))
- require.False(t, paymentOrderAllowsRegistryFallback(&dbent.PaymentOrder{
- PaymentType: payment.TypeAlipay,
- ProviderSnapshot: map[string]any{
- "schema_version": 2,
- "provider_instance_id": "12",
- },
- }))
- }
- func TestPaymentOrderQueryReferenceUsesOutTradeNoForOfficialProviders(t *testing.T) {
- t.Parallel()
- order := &dbent.PaymentOrder{
- PaymentType: payment.TypeWxpay,
- OutTradeNo: "sub2_out_trade_no",
- PaymentTradeNo: "wx-transaction-id",
- }
- require.Equal(t, "sub2_out_trade_no", paymentOrderQueryReference(order, &paymentOrderLifecycleQueryProvider{}))
- require.Equal(t, "sub2_out_trade_no", paymentOrderQueryReference(order, paymentFulfillmentTestProvider{
- key: payment.TypeWxpay,
- }))
- }
- func newPaymentOrderLifecycleTestClient(t *testing.T) *dbent.Client {
- t.Helper()
- db, err := sql.Open("sqlite", "file:payment_order_lifecycle?mode=memory&cache=shared&_fk=1")
- require.NoError(t, err)
- t.Cleanup(func() { _ = db.Close() })
- _, err = db.Exec("PRAGMA foreign_keys = ON")
- require.NoError(t, err)
- drv := entsql.OpenDB(dialect.SQLite, db)
- client := enttest.NewClient(t, enttest.WithOptions(dbent.Driver(drv)))
- t.Cleanup(func() { _ = client.Close() })
- return client
- }
|