Files
NianAIGC/backend/internal/postgres/payments_test.go
T
2026-10-12 13:57:23 +08:00

178 lines
8.2 KiB
Go

package postgres
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"time"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/payments"
"github.com/jackc/pgx/v5/pgconn"
)
func paymentTestOrder(status, transactionID string) payments.Order {
now := time.Now().UTC()
order := payments.Order{
ID: "pmt_123", OrganizationID: "org_1", AccountID: "account_1", RequestKey: "request_1",
MchID: "mch_1", AppID: "wx_app_1", AmountFen: 5200, Currency: "CNY",
Status: status, CodeURL: "weixin://qr", TransactionID: transactionID,
CreatedAt: now, UpdatedAt: now, ExpiresAt: now.Add(15 * time.Minute),
}
if status == payments.StatusPaid {
order.PaidAt = &now
}
return order
}
func paymentTestRow(order payments.Order) []any {
var paidAt any
if order.PaidAt != nil {
paidAt = *order.PaidAt
}
return []any{order.ID, order.OrganizationID, order.AccountID, order.RequestKey,
order.MchID, order.AppID, order.AmountFen, order.Currency, order.Status,
order.CodeURL, order.TransactionID, order.CreatedAt, order.UpdatedAt, order.ExpiresAt, paidAt}
}
func paymentTestTransaction() payments.Transaction {
return payments.Transaction{
OrderID: "pmt_123", MchID: "mch_1", AppID: "wx_app_1",
TransactionID: "wx_transaction_1", TradeState: "SUCCESS", Currency: "CNY",
AmountFen: 5200, SuccessTime: time.Now().UTC(),
}
}
func TestPaymentSettlementAtomicallyPostsExactAmountAndMarksPaid(t *testing.T) {
order := paymentTestOrder(payments.StatusPending, "")
paid := paymentTestOrder(payments.StatusPaid, "wx_transaction_1")
now := time.Now().UTC()
tx := &generationStateTransaction{results: []*jobRows{
{rows: [][]any{paymentTestRow(order)}},
{rows: [][]any{paymentTestRow(paid)}},
{rows: [][]any{{"wechat-native-entry:pmt_123", int64(5200), int64(5200), int64(5200), int64(0), now, now, int64(5200)}}},
}}
db := NewDatabase(Config{Backend: BackendPostgres}, &generationStatePool{tx: tx})
got, err := db.SettlePaymentOrder(context.Background(), paymentTestTransaction())
if err != nil || got.Status != payments.StatusPaid || tx.commits != 1 || tx.rollbacks != 0 {
t.Fatalf("got=%+v err=%v commits=%d rollbacks=%d", got, err, tx.commits, tx.rollbacks)
}
if len(tx.queries) != 3 || tx.queries[0].sql != lockPaymentOrderSQL ||
tx.queries[1].sql != paidPaymentOrderSQL || tx.queries[2].sql != PostWalletEntrySQL {
t.Fatalf("queries=%+v", tx.queries)
}
args := tx.queries[2].args
if args[0] != "wechat-native-entry:pmt_123" || args[1] != "org_1" || args[2] != nil || args[3] != nil ||
args[4] != "recharge" || args[5] != int64(5200) || args[7] != "wechat-native:mch_1:pmt_123" {
t.Fatalf("wallet args=%#v", args)
}
var metadata map[string]string
if err := json.Unmarshal(args[9].(json.RawMessage), &metadata); err != nil || metadata["initiatedByAccountId"] != "account_1" {
t.Fatalf("metadata=%#v err=%v", metadata, err)
}
}
func TestPaymentSettlementRejectsMismatchedAmountBeforeWalletWrite(t *testing.T) {
tx := &generationStateTransaction{results: []*jobRows{{rows: [][]any{paymentTestRow(paymentTestOrder(payments.StatusPending, ""))}}}}
db := NewDatabase(Config{Backend: BackendPostgres}, &generationStatePool{tx: tx})
transaction := paymentTestTransaction()
transaction.AmountFen--
_, err := db.SettlePaymentOrder(context.Background(), transaction)
if !errors.Is(err, payments.ErrVerification) || tx.commits != 0 || tx.rollbacks != 1 || len(tx.queries) != 1 {
t.Fatalf("err=%v commits=%d rollbacks=%d queries=%d", err, tx.commits, tx.rollbacks, len(tx.queries))
}
}
func TestPaymentSettlementReplaysSamePaidTransactionWithoutRecredit(t *testing.T) {
tx := &generationStateTransaction{results: []*jobRows{{rows: [][]any{paymentTestRow(paymentTestOrder(payments.StatusPaid, "wx_transaction_1"))}}}}
db := NewDatabase(Config{Backend: BackendPostgres}, &generationStatePool{tx: tx})
_, err := db.SettlePaymentOrder(context.Background(), paymentTestTransaction())
if err != nil || tx.commits != 1 || len(tx.queries) != 1 {
t.Fatalf("err=%v commits=%d queries=%d", err, tx.commits, len(tx.queries))
}
}
func TestPaymentSettlementAllowsVerifiedLateSuccessAfterClose(t *testing.T) {
closed := paymentTestOrder(payments.StatusClosed, "")
paid := paymentTestOrder(payments.StatusPaid, "wx_transaction_1")
now := time.Now().UTC()
tx := &generationStateTransaction{results: []*jobRows{
{rows: [][]any{paymentTestRow(closed)}},
{rows: [][]any{paymentTestRow(paid)}},
{rows: [][]any{{"wechat-native-entry:pmt_123", int64(5200), int64(5200), int64(5200), int64(0), now, now, int64(5200)}}},
}}
db := NewDatabase(Config{Backend: BackendPostgres}, &generationStatePool{tx: tx})
got, err := db.SettlePaymentOrder(context.Background(), paymentTestTransaction())
if err != nil || got.Status != payments.StatusPaid || tx.commits != 1 {
t.Fatalf("got=%+v err=%v commits=%d", got, err, tx.commits)
}
}
func TestPaymentSettlementRollsBackOrderWhenWalletFails(t *testing.T) {
tx := &generationStateTransaction{results: []*jobRows{
{rows: [][]any{paymentTestRow(paymentTestOrder(payments.StatusPending, ""))}},
{rows: [][]any{paymentTestRow(paymentTestOrder(payments.StatusPaid, "wx_transaction_1"))}},
{err: &pgconn.PgError{Code: "P0001", Message: "wallet failed"}},
}}
db := NewDatabase(Config{Backend: BackendPostgres}, &generationStatePool{tx: tx})
_, err := db.SettlePaymentOrder(context.Background(), paymentTestTransaction())
if err == nil || tx.commits != 0 || tx.rollbacks != 1 {
t.Fatalf("err=%v commits=%d rollbacks=%d", err, tx.commits, tx.rollbacks)
}
}
func TestPaymentSettlementRejectsReusedProviderTransactionAndRollsBack(t *testing.T) {
tx := &generationStateTransaction{results: []*jobRows{
{rows: [][]any{paymentTestRow(paymentTestOrder(payments.StatusPending, ""))}},
{err: &pgconn.PgError{Code: "23505", ConstraintName: "payment_orders_provider_transaction_unique"}},
}}
db := NewDatabase(Config{Backend: BackendPostgres}, &generationStatePool{tx: tx})
_, err := db.SettlePaymentOrder(context.Background(), paymentTestTransaction())
if !errors.Is(err, payments.ErrConflict) || tx.commits != 0 || tx.rollbacks != 1 || len(tx.queries) != 2 {
t.Fatalf("err=%v commits=%d rollbacks=%d queries=%d", err, tx.commits, tx.rollbacks, len(tx.queries))
}
}
func TestPaymentSettlementRejectsDifferentTransactionForPaidOrder(t *testing.T) {
tx := &generationStateTransaction{results: []*jobRows{{rows: [][]any{paymentTestRow(paymentTestOrder(payments.StatusPaid, "other_transaction"))}}}}
db := NewDatabase(Config{Backend: BackendPostgres}, &generationStatePool{tx: tx})
_, err := db.SettlePaymentOrder(context.Background(), paymentTestTransaction())
if !errors.Is(err, payments.ErrConflict) || tx.commits != 0 || tx.rollbacks != 1 || len(tx.queries) != 1 {
t.Fatalf("err=%v commits=%d rollbacks=%d queries=%d", err, tx.commits, tx.rollbacks, len(tx.queries))
}
}
func TestPaymentCreateRejectsInvalidMoneyAndIdentifiersBeforeWrite(t *testing.T) {
for _, mutate := range []func(*payments.Order){
func(o *payments.Order) { o.AmountFen = 0 },
func(o *payments.Order) { o.AmountFen = 1_000_001 },
func(o *payments.Order) { o.Currency = "USD" },
func(o *payments.Order) { o.RequestKey = "request:key" },
func(o *payments.Order) { o.ID = strings.Repeat("a", 33) },
} {
q := &fakeQuerier{}
db := NewDatabase(Config{Backend: BackendPostgres}, q)
order := paymentTestOrder(payments.StatusPending, "")
mutate(&order)
if _, err := db.CreatePaymentOrder(context.Background(), order); err == nil || q.sql != "" {
t.Fatalf("order=%+v err=%v sql=%s", order, err, q.sql)
}
}
}
func TestPaymentCreateUsesAccountOrganizationAndRequestUniqueness(t *testing.T) {
if !strings.Contains(createPaymentOrderSQL, "account.organization_id = organization.id") ||
!strings.Contains(createPaymentOrderSQL, "account.role IN ('organization_admin', 'super_admin')") ||
!strings.Contains(createPaymentOrderSQL, "ON CONFLICT DO NOTHING") {
t.Fatalf("create SQL lacks account relation or conflict control: %s", createPaymentOrderSQL)
}
order := paymentTestOrder(payments.StatusPending, "")
q := &fakeQuerier{rows: [][]any{paymentTestRow(order)}}
db := NewDatabase(Config{Backend: BackendPostgres}, q)
created, err := db.CreatePaymentOrder(context.Background(), order)
if err != nil || created.ID != order.ID || q.sql != createPaymentOrderSQL {
t.Fatalf("created=%+v err=%v sql=%s", created, err, q.sql)
}
}