178 lines
8.2 KiB
Go
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)
|
|
}
|
|
}
|