merge: P8 订阅/recurring + 拒付 chargeback 并入(订阅生命周期/续费/取消/past_due/chargeback/事件集收口)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u # Conflicts: # internal/model/testdb.go # internal/provider/provider.go # internal/router/router.go # internal/store/order_query_test.go # main.go
This commit is contained in:
@@ -32,7 +32,7 @@ func OpenTestDB(t *testing.T) *gorm.DB {
|
||||
t.Fatalf("open test db: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&OrderV2{}, &Attempt{}, &Account{}, &Refund{}, &WebhookDelivery{},
|
||||
&Product{}, &ProductPrice{}, &OrphanPayment{}); err != nil {
|
||||
&Product{}, &ProductPrice{}, &OrphanPayment{}, &Subscription{}, &Chargeback{}); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
if err := UpgradeWebhookDeliveryIndex(db); err != nil {
|
||||
|
||||
@@ -72,6 +72,9 @@ type OrderV2 struct {
|
||||
DiscountReason string `gorm:"size:128"`
|
||||
PaidAt *time.Time
|
||||
ExpiresAt *time.Time // 整体购买窗口
|
||||
// Disputed 打标位(P8 Task6):渠道拒付(charge.dispute.created)命中该单时置 true,
|
||||
// 仅留痕、不改状态机(§6 决策:不自动回收权益,业务方按 chargeback.received 自行冲正)。
|
||||
Disputed bool `gorm:"default:false"`
|
||||
}
|
||||
|
||||
// ---- 支付尝试(收款账本)----
|
||||
@@ -121,3 +124,53 @@ type Refund struct {
|
||||
InitiatedBy string `gorm:"size:16"` // business/platform
|
||||
CompletedAt *time.Time
|
||||
}
|
||||
|
||||
// ---- 订阅(recurring)----
|
||||
|
||||
type SubStatus string
|
||||
|
||||
const (
|
||||
SubActive SubStatus = "active" // 已激活,正常续费
|
||||
SubPastDue SubStatus = "past_due" // 某期扣款失败,待恢复(invoice.payment_failed)
|
||||
SubCanceled SubStatus = "canceled" // 已取消(主动/网关删除),终态
|
||||
)
|
||||
|
||||
// Subscription 是"同一 entitlement 的跨期账本"(设计 §5.1)。单笔层(OrderV2/Attempt)
|
||||
// 与订阅层解耦:每期扣款仍落一张 renewal OrderV2,Subscription 只维护状态机 + 续费锚点。
|
||||
type Subscription struct {
|
||||
Base
|
||||
SubID string `gorm:"uniqueIndex;size:64;not null"` // pay 生成的逻辑订阅号
|
||||
OutTradeNo string `gorm:"index;size:64;not null"` // 首购 order(诞生订阅那笔)
|
||||
MerchantID uint64 `gorm:"index"`
|
||||
BizSystem string `gorm:"index;size:32"`
|
||||
BizRef string `gorm:"size:128"`
|
||||
BizCode string `gorm:"index;size:64"` // 套餐码副本,续费事件带回
|
||||
Channel string `gorm:"index;size:32;not null;uniqueIndex:uq_sub_provider"`
|
||||
ProviderSubRef string `gorm:"size:128;not null;uniqueIndex:uq_sub_provider"` // 渠道订阅号(stripe sub id)
|
||||
RecurringKind string `gorm:"size:24"` // gateway_scheduled/token_offsession/...
|
||||
AmountMinor int64 `gorm:"not null"`
|
||||
Currency string `gorm:"size:16;not null"`
|
||||
Status SubStatus `gorm:"index;size:16;not null"`
|
||||
CurrentPeriodEnd *time.Time
|
||||
CanceledAt *time.Time
|
||||
}
|
||||
|
||||
// ---- 拒付(chargeback,P8 Task6)----
|
||||
|
||||
// Chargeback 记录渠道拒付(设计 §6「钱到账不可逆」的例外形态:卡组织裁定退单)。只有
|
||||
// Stripe(卡)有此语义;alipay/微信本轮无拒付流,crypto 收款不可逆、永无 chargeback——
|
||||
// 三者均不实现 Capabilities 里任何 chargeback 相关声明(P8 未新增该 capability 位,
|
||||
// 靠 EventKind 是否产出 EventChargeback 天然区分)。
|
||||
// 每次拒付(含重投)按 DisputeRef 幂等落一行留痕;是否成功定位/转发给业务方是另一回事
|
||||
// (见 gateway.recordChargeback),Chargeback 表本身对"能否定位业务单"保持中立、全记录。
|
||||
type Chargeback struct {
|
||||
Base
|
||||
DisputeRef string `gorm:"uniqueIndex;size:128;not null"` // 渠道拒付号(stripe dp_...)
|
||||
OutTradeNo string `gorm:"index;size:64"` // 解析出的原单号(订阅拒付可能为空)
|
||||
Channel string `gorm:"index;size:32;not null"`
|
||||
ProviderPaymentRef string `gorm:"size:128"` // 关联 PaymentIntent id
|
||||
AmountMinor int64 `gorm:"not null"`
|
||||
Currency string `gorm:"size:16;not null"`
|
||||
Reason string `gorm:"size:64"`
|
||||
Status string `gorm:"size:24"` // 渠道拒付状态快照(needs_response/...)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user