c5949a595a
从"每单唯一 HD 地址"改为"单个固定收款地址 + 每单唯一金额",归集成本 O(订单数)→O(1)。 - store: pay_orders 加 user_ref/expect_amount(唯一金额)/matched_tx_id;新 orphan_payments 表; ActiveOrderByUser(同用户单订单)、AmountRecentlyUsed(迟到窗口内金额不复用)、TxHandled(幂等)、 RecordOrphan。去掉每单派生游标。 - pay: CreateOrder(userRef,sku,priceMicro)——同用户单订单校验 + 分配唯一金额(base+随机微尾数[1,9999]、 cooldown 内不复用),address 恒为收款地址。 - tron: Transfer 加 BlockTs(区块时间秒),取 block_timestamp。 - watcher: 单地址取到账,按"金额==expect && block_ts>建单"匹配 → paid;不匹配的到账 → orphan;幂等。 - httpapi: POST /order 加 user_ref,同用户重复 → 409;main 收款地址=PAY_RECEIVE_ADDRESS 或 xpub index0。 - 测试:唯一金额/同地址、同用户单订单、精确匹配、付错成孤儿、迟到不误配新单、付款早于建单不匹配、 超时、幂等、409,全绿。README 更新为单地址模型+API(user_ref/精确金额/orphan)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
2.0 KiB
Go
78 lines
2.0 KiB
Go
package pay
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/wangjia/pangolin/pay/internal/store"
|
|
)
|
|
|
|
const recvAddr = "TRecv00000000000000000000000000000A"
|
|
|
|
func newSvc(t *testing.T) *Service {
|
|
t.Helper()
|
|
st, err := store.Open(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("store: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = st.Close() })
|
|
return New(st, Config{ReceiveAddress: recvAddr})
|
|
}
|
|
|
|
func TestCreateOrderUniqueAmountSameAddress(t *testing.T) {
|
|
svc := newSvc(t)
|
|
ctx := context.Background()
|
|
|
|
o1, err := svc.CreateOrder(ctx, "u1", "pro-year", 5_000000)
|
|
if err != nil {
|
|
t.Fatalf("order1: %v", err)
|
|
}
|
|
if o1.Address != recvAddr {
|
|
t.Fatalf("addr %s, want single receive address", o1.Address)
|
|
}
|
|
if o1.ExpectAmount <= 5_000000 || o1.ExpectAmount > 5_000000+9999 {
|
|
t.Fatalf("amount %d not base+tail(<=9999)", o1.ExpectAmount)
|
|
}
|
|
|
|
o2, err := svc.CreateOrder(ctx, "u2", "pro-year", 5_000000)
|
|
if err != nil {
|
|
t.Fatalf("order2: %v", err)
|
|
}
|
|
if o2.ExpectAmount == o1.ExpectAmount {
|
|
t.Fatal("amounts must be unique across concurrent orders")
|
|
}
|
|
if o2.Address != o1.Address {
|
|
t.Fatal("single-address model: both orders share the receiving address")
|
|
}
|
|
}
|
|
|
|
func TestCreateOrderOneActivePerUser(t *testing.T) {
|
|
svc := newSvc(t)
|
|
ctx := context.Background()
|
|
if _, err := svc.CreateOrder(ctx, "u1", "pro", 100); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := svc.CreateOrder(ctx, "u1", "pro", 100)
|
|
if !errors.Is(err, ErrUserHasActiveOrder) {
|
|
t.Fatalf("want ErrUserHasActiveOrder, got %v", err)
|
|
}
|
|
if _, err := svc.CreateOrder(ctx, "u2", "pro", 100); err != nil {
|
|
t.Fatalf("different user should be allowed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCreateOrderRejectsBadInput(t *testing.T) {
|
|
svc := newSvc(t)
|
|
ctx := context.Background()
|
|
if _, err := svc.CreateOrder(ctx, "", "pro", 100); err == nil {
|
|
t.Fatal("empty userRef should error")
|
|
}
|
|
if _, err := svc.CreateOrder(ctx, "u1", "", 100); err == nil {
|
|
t.Fatal("empty sku should error")
|
|
}
|
|
if _, err := svc.CreateOrder(ctx, "u1", "pro", 0); err == nil {
|
|
t.Fatal("non-positive price should error")
|
|
}
|
|
}
|