//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" ) type paymentResumeLookupProvider struct { queryCount int } func (p *paymentResumeLookupProvider) Name() string { return "resume-lookup-provider" } func (p *paymentResumeLookupProvider) ProviderKey() string { return payment.TypeAlipay } func (p *paymentResumeLookupProvider) SupportedTypes() []payment.PaymentType { return []payment.PaymentType{payment.TypeAlipay} } func (p *paymentResumeLookupProvider) CreatePayment(context.Context, payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) { panic("unexpected call") } func (p *paymentResumeLookupProvider) QueryOrder(context.Context, string) (*payment.QueryOrderResponse, error) { p.queryCount++ return &payment.QueryOrderResponse{Status: payment.ProviderStatusPending}, nil } func (p *paymentResumeLookupProvider) VerifyNotification(context.Context, string, map[string]string) (*payment.PaymentNotification, error) { panic("unexpected call") } func (p *paymentResumeLookupProvider) Refund(context.Context, payment.RefundRequest) (*payment.RefundResponse, error) { panic("unexpected call") } func TestGetPublicOrderByResumeTokenReturnsMatchingOrder(t *testing.T) { ctx := context.Background() client := newPaymentConfigServiceTestClient(t) user, err := client.User.Create(). SetEmail("resume@example.com"). SetPasswordHash("hash"). SetUsername("resume-user"). Save(ctx) require.NoError(t, err) instanceID := "12" providerKey := payment.TypeEasyPay order, err := client.PaymentOrder.Create(). SetUserID(user.ID). SetUserEmail(user.Email). SetUserName(user.Username). SetAmount(88). SetPayAmount(88). SetFeeRate(0). SetRechargeCode("RESUME-ORDER"). SetOutTradeNo("sub2_resume_lookup"). SetPaymentType(payment.TypeAlipay). SetPaymentTradeNo("trade-1"). SetOrderType(payment.OrderTypeBalance). SetStatus(OrderStatusPending). SetExpiresAt(time.Now().Add(time.Hour)). SetClientIP("127.0.0.1"). SetSrcHost("api.example.com"). SetProviderInstanceID(instanceID). SetProviderKey(providerKey). Save(ctx) require.NoError(t, err) resumeSvc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef")) token, err := resumeSvc.CreateToken(ResumeTokenClaims{ OrderID: order.ID, UserID: user.ID, ProviderInstanceID: instanceID, ProviderKey: providerKey, PaymentType: payment.TypeAlipay, CanonicalReturnURL: "https://app.example.com/payment/result", }) require.NoError(t, err) svc := &PaymentService{ entClient: client, resumeService: resumeSvc, } got, err := svc.GetPublicOrderByResumeToken(ctx, token) require.NoError(t, err) require.Equal(t, order.ID, got.ID) } func TestGetPublicOrderByResumeTokenRejectsSnapshotMismatch(t *testing.T) { ctx := context.Background() client := newPaymentConfigServiceTestClient(t) user, err := client.User.Create(). SetEmail("resume-mismatch@example.com"). SetPasswordHash("hash"). SetUsername("resume-mismatch-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("RESUME-MISMATCH"). SetOutTradeNo("sub2_resume_lookup_mismatch"). SetPaymentType(payment.TypeAlipay). SetPaymentTradeNo("trade-2"). SetOrderType(payment.OrderTypeBalance). SetStatus(OrderStatusPending). SetExpiresAt(time.Now().Add(time.Hour)). SetClientIP("127.0.0.1"). SetSrcHost("api.example.com"). SetProviderInstanceID("12"). SetProviderKey(payment.TypeEasyPay). Save(ctx) require.NoError(t, err) resumeSvc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef")) token, err := resumeSvc.CreateToken(ResumeTokenClaims{ OrderID: order.ID, UserID: user.ID, ProviderInstanceID: "99", ProviderKey: payment.TypeEasyPay, PaymentType: payment.TypeAlipay, CanonicalReturnURL: "https://app.example.com/payment/result", }) require.NoError(t, err) svc := &PaymentService{ entClient: client, resumeService: resumeSvc, } _, err = svc.GetPublicOrderByResumeToken(ctx, token) require.Error(t, err) require.Equal(t, "INVALID_RESUME_TOKEN", infraerrors.Reason(err)) } func TestGetPublicOrderByResumeTokenUsesSnapshotAuthorityWhenColumnsDiffer(t *testing.T) { ctx := context.Background() client := newPaymentConfigServiceTestClient(t) user, err := client.User.Create(). SetEmail("resume-snapshot-authority@example.com"). SetPasswordHash("hash"). SetUsername("resume-snapshot-authority-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("RESUME-SNAPSHOT-AUTHORITY"). SetOutTradeNo("sub2_resume_snapshot_authority"). SetPaymentType(payment.TypeAlipay). SetPaymentTradeNo("trade-snapshot-authority"). SetOrderType(payment.OrderTypeBalance). SetStatus(OrderStatusPending). SetExpiresAt(time.Now().Add(time.Hour)). SetClientIP("127.0.0.1"). SetSrcHost("api.example.com"). SetProviderInstanceID("legacy-column-instance"). SetProviderKey(payment.TypeAlipay). SetProviderSnapshot(map[string]any{ "schema_version": 2, "provider_instance_id": "snapshot-instance", "provider_key": payment.TypeEasyPay, }). Save(ctx) require.NoError(t, err) resumeSvc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef")) token, err := resumeSvc.CreateToken(ResumeTokenClaims{ OrderID: order.ID, UserID: user.ID, ProviderInstanceID: "snapshot-instance", ProviderKey: payment.TypeEasyPay, PaymentType: payment.TypeAlipay, CanonicalReturnURL: "https://app.example.com/payment/result", }) require.NoError(t, err) svc := &PaymentService{ entClient: client, resumeService: resumeSvc, } got, err := svc.GetPublicOrderByResumeToken(ctx, token) require.NoError(t, err) require.Equal(t, order.ID, got.ID) } func TestGetPublicOrderByResumeTokenChecksUpstreamForPendingOrder(t *testing.T) { ctx := context.Background() client := newPaymentConfigServiceTestClient(t) user, err := client.User.Create(). SetEmail("resume-refresh@example.com"). SetPasswordHash("hash"). SetUsername("resume-refresh-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("RESUME-PENDING"). SetOutTradeNo("sub2_resume_lookup_pending"). SetPaymentType(payment.TypeAlipay). SetPaymentTradeNo("trade-pending"). 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) resumeSvc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef")) token, err := resumeSvc.CreateToken(ResumeTokenClaims{ OrderID: order.ID, UserID: user.ID, PaymentType: payment.TypeAlipay, CanonicalReturnURL: "https://app.example.com/payment/result", }) require.NoError(t, err) registry := payment.NewRegistry() provider := &paymentResumeLookupProvider{} registry.Register(provider) svc := &PaymentService{ entClient: client, registry: registry, resumeService: resumeSvc, providersLoaded: true, } got, err := svc.GetPublicOrderByResumeToken(ctx, token) require.NoError(t, err) require.Equal(t, order.ID, got.ID) require.Equal(t, 1, provider.queryCount) } func TestVerifyOrderPublicDoesNotCheckUpstreamForPendingOrder(t *testing.T) { ctx := context.Background() client := newPaymentConfigServiceTestClient(t) user, err := client.User.Create(). SetEmail("public-verify@example.com"). SetPasswordHash("hash"). SetUsername("public-verify-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("PUBLIC-VERIFY"). SetOutTradeNo("sub2_public_verify_pending"). SetPaymentType(payment.TypeAlipay). SetPaymentTradeNo("trade-public-verify"). 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 := &paymentResumeLookupProvider{} registry.Register(provider) svc := &PaymentService{ entClient: client, registry: registry, providersLoaded: true, } got, err := svc.VerifyOrderPublic(ctx, order.OutTradeNo) require.NoError(t, err) require.Equal(t, order.ID, got.ID) require.Equal(t, 0, provider.queryCount) } func TestVerifyOrderPublicRejectsBlankOutTradeNo(t *testing.T) { svc := &PaymentService{ entClient: newPaymentConfigServiceTestClient(t), } _, err := svc.VerifyOrderPublic(context.Background(), " ") require.Error(t, err) require.Equal(t, "INVALID_OUT_TRADE_NO", infraerrors.Reason(err)) }