177 lines
7.5 KiB
Go
177 lines
7.5 KiB
Go
package payment
|
|
|
|
import (
|
|
"context"
|
|
bizpayment "kra/internal/biz/payment"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func newPaymentOrderRepoForTest(t *testing.T) *paymentOrderRepo {
|
|
t.Helper()
|
|
db, err := openWithDriver("sqlite", "file:"+t.Name()+"-"+uuid.NewString()+"?mode=memory&cache=shared")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.AutoMigrate(&paymentOrderPO{}, &paymentEventPO{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return &paymentOrderRepo{data: &Data{gormDB: newReloadableDB(db, nil)}}
|
|
}
|
|
|
|
func testPaymentOrder() *bizpayment.PaymentOrder {
|
|
return &bizpayment.PaymentOrder{
|
|
TradeNo: "order-1", Provider: bizpayment.PaymentAlipay, BusinessType: "game_item", BusinessID: "item-1",
|
|
Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: bizpayment.PaymentStatusInitialized,
|
|
FulfillmentStatus: bizpayment.FulfillmentStatusPending, RefundStatus: bizpayment.RefundStatusNone,
|
|
ConfirmationID: "11111111-1111-1111-1111-111111111111", RequestFingerprint: "fingerprint",
|
|
}
|
|
}
|
|
|
|
func TestPaymentOrderRepositoryPersistsPaymentFulfillmentAndRefundState(t *testing.T) {
|
|
repo := newPaymentOrderRepoForTest(t)
|
|
ctx := context.Background()
|
|
order, created, err := repo.CreatePaymentOrder(ctx, testPaymentOrder())
|
|
if err != nil || !created {
|
|
t.Fatalf("create order = %#v created=%v err=%v", order, created, err)
|
|
}
|
|
update := &bizpayment.PaymentProviderUpdate{Status: "success", ProviderStatus: "TRADE_SUCCESS", ProviderTradeNo: "provider-1", Amount: 100, Currency: "CNY", EventID: "event-1"}
|
|
order, err = repo.ApplyPaymentResult(ctx, bizpayment.PaymentAlipay, "order-1", update)
|
|
if err != nil || order.PaymentStatus != bizpayment.PaymentStatusPaid || order.PaidAmount != 100 {
|
|
t.Fatalf("apply payment = %#v err=%v", order, err)
|
|
}
|
|
order, token, duplicate, err := repo.BeginPaymentFulfillment(ctx, bizpayment.PaymentAlipay, "order-1", time.Minute)
|
|
if err != nil || duplicate || token == "" || order.FulfillmentStatus != bizpayment.FulfillmentStatusProcessing {
|
|
t.Fatalf("begin fulfillment = %#v token=%q duplicate=%v err=%v", order, token, duplicate, err)
|
|
}
|
|
if _, _, _, err = repo.BeginPaymentFulfillment(ctx, bizpayment.PaymentAlipay, "order-1", time.Minute); err == nil {
|
|
t.Fatal("concurrent fulfillment was accepted")
|
|
}
|
|
order, err = repo.CompletePaymentFulfillment(ctx, bizpayment.PaymentAlipay, "order-1", token, true, "")
|
|
if err != nil || order.FulfillmentStatus != bizpayment.FulfillmentStatusSucceeded {
|
|
t.Fatalf("complete fulfillment = %#v err=%v", order, err)
|
|
}
|
|
_, _, duplicate, err = repo.BeginPaymentFulfillment(ctx, bizpayment.PaymentAlipay, "order-1", time.Minute)
|
|
if err != nil || !duplicate {
|
|
t.Fatalf("duplicate fulfillment = duplicate=%v err=%v", duplicate, err)
|
|
}
|
|
order, token, err = repo.BeginPaymentRefund(ctx, bizpayment.PaymentAlipay, "order-1", 40, time.Minute)
|
|
if err != nil || token == "" || order.RefundStatus != bizpayment.RefundStatusProcessing {
|
|
t.Fatalf("begin refund = %#v token=%q err=%v", order, token, err)
|
|
}
|
|
order, err = repo.CompletePaymentRefundRequest(ctx, bizpayment.PaymentAlipay, "order-1", token, true, "")
|
|
if err != nil || order.RefundStatus != bizpayment.RefundStatusPending || order.RefundRequestedAmount != 40 {
|
|
t.Fatalf("accept refund = %#v err=%v", order, err)
|
|
}
|
|
order, err = repo.ConfirmPaymentRefund(ctx, bizpayment.PaymentAlipay, "order-1", order.RefundNo, 40, true, "")
|
|
if err != nil || order.RefundedAmount != 40 || order.PaymentStatus != bizpayment.PaymentStatusPartiallyRefunded {
|
|
t.Fatalf("confirm refund = %#v err=%v", order, err)
|
|
}
|
|
}
|
|
|
|
func TestPaymentOrderRepositoryRejectsProviderTradeReuse(t *testing.T) {
|
|
repo := newPaymentOrderRepoForTest(t)
|
|
ctx := context.Background()
|
|
first := testPaymentOrder()
|
|
if _, _, err := repo.CreatePaymentOrder(ctx, first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second := testPaymentOrder()
|
|
second.TradeNo = "order-2"
|
|
second.ConfirmationID = "22222222-2222-2222-2222-222222222222"
|
|
if _, _, err := repo.CreatePaymentOrder(ctx, second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
update := &bizpayment.PaymentProviderUpdate{Status: "success", ProviderTradeNo: "provider-1", Amount: 100, Currency: "CNY"}
|
|
if _, err := repo.ApplyPaymentResult(ctx, bizpayment.PaymentAlipay, "order-1", update); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := repo.ApplyPaymentResult(ctx, bizpayment.PaymentAlipay, "order-2", update); err != bizpayment.ErrPaymentProviderConflict {
|
|
t.Fatalf("provider trade reuse err = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPaymentOrderRepositoryListsWithFilters(t *testing.T) {
|
|
repo := newPaymentOrderRepoForTest(t)
|
|
ctx := context.Background()
|
|
first := testPaymentOrder()
|
|
if _, _, err := repo.CreatePaymentOrder(ctx, first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second := testPaymentOrder()
|
|
second.TradeNo = "wechat-order-2"
|
|
second.Provider = bizpayment.PaymentWechatV3
|
|
second.BusinessID = "item-2"
|
|
second.ConfirmationID = "33333333-3333-3333-3333-333333333333"
|
|
if _, _, err := repo.CreatePaymentOrder(ctx, second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
items, total, err := repo.ListPaymentOrders(ctx, 1, 10, bizpayment.PaymentOrderFilter{Provider: bizpayment.PaymentWechatV3, TradeNo: "wechat", BusinessID: "item-2"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if total != 1 || len(items) != 1 || items[0].TradeNo != second.TradeNo {
|
|
t.Fatalf("filtered orders = %#v total=%d", items, total)
|
|
}
|
|
}
|
|
|
|
func TestPaymentOrderRepositorySummarizesAndListsEvents(t *testing.T) {
|
|
repo := newPaymentOrderRepoForTest(t)
|
|
ctx := bizpayment.WithPaymentOperation(context.Background(), bizpayment.PaymentOperation{
|
|
Source: "admin", Reason: "客户重复购买", OperatorID: 7, OperatorName: "operator",
|
|
ClientIP: "127.0.0.1", UserAgent: "payment-test/1.0", DeviceID: "device-1",
|
|
})
|
|
order := testPaymentOrder()
|
|
if _, _, err := repo.CreatePaymentOrder(ctx, order); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := repo.ApplyPaymentResult(ctx, bizpayment.PaymentAlipay, order.TradeNo, &bizpayment.PaymentProviderUpdate{
|
|
Status: "success", ProviderStatus: "TRADE_SUCCESS", ProviderTradeNo: "provider-summary-1",
|
|
Amount: 100, Currency: "CNY", EventType: bizpayment.PaymentEventPaymentSynchronized,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
current, token, err := repo.BeginPaymentRefund(ctx, bizpayment.PaymentAlipay, order.TradeNo, 40, time.Minute)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if current, err = repo.CompletePaymentRefundRequest(ctx, bizpayment.PaymentAlipay, order.TradeNo, token, true, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = repo.ConfirmPaymentRefund(ctx, bizpayment.PaymentAlipay, order.TradeNo, current.RefundNo, 40, true, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
summary, err := repo.SummarizePaymentOrders(ctx, bizpayment.PaymentOrderFilter{ProviderTradeNo: "summary-1"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if summary.OrderCount != 1 || summary.PaidCount != 1 || summary.RefundedCount != 1 || len(summary.Currencies) != 1 || summary.Currencies[0].NetAmount != 60 {
|
|
t.Fatalf("unexpected summary: %#v", summary)
|
|
}
|
|
events, err := repo.ListPaymentEvents(ctx, bizpayment.PaymentAlipay, order.TradeNo, 100)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(events) < 5 {
|
|
t.Fatalf("event count = %d, want at least 5", len(events))
|
|
}
|
|
var foundRefund bool
|
|
for _, event := range events {
|
|
if event.Type == bizpayment.PaymentEventRefundStarted {
|
|
foundRefund = event.Message == "客户重复购买" && event.OperatorID == 7 && event.DeviceID == "device-1"
|
|
}
|
|
}
|
|
if !foundRefund {
|
|
t.Fatalf("refund audit event missing operation context: %#v", events)
|
|
}
|
|
}
|
|
|
|
func TestTrimToPreservesUTF8(t *testing.T) {
|
|
if got := trimTo(" 退款处理失败 ", 4); got != "退款处理" {
|
|
t.Fatalf("trimTo() = %q, want %q", got, "退款处理")
|
|
}
|
|
}
|