package store import ( "errors" "fmt" "time" "gorm.io/gorm" "gorm.io/gorm/clause" "github.com/wangjia/pay/internal/model" ) var ErrSubNotFound = errors.New("store: subscription not found") type SubscriptionStore struct{ db *gorm.DB } func NewSubscriptionStore(db *gorm.DB) *SubscriptionStore { return &SubscriptionStore{db: db} } // Create 幂等插入:重复 (channel,provider_sub_ref) → no-op(created=false)。 // Stripe 会重投 checkout.session.completed;诞生订阅必须幂等。 func (s *SubscriptionStore) Create(sub *model.Subscription) (bool, error) { res := s.db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "channel"}, {Name: "provider_sub_ref"}}, DoNothing: true, }).Create(sub) if res.Error != nil { return false, fmt.Errorf("store.SubscriptionStore.Create: %w", res.Error) } return res.RowsAffected > 0, nil } func (s *SubscriptionStore) GetBySubID(subID string) (*model.Subscription, error) { var sub model.Subscription if err := s.db.Where("sub_id = ?", subID).First(&sub).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrSubNotFound } return nil, fmt.Errorf("store.GetBySubID: %w", err) } return &sub, nil } func (s *SubscriptionStore) GetByProviderRef(channel, ref string) (*model.Subscription, error) { var sub model.Subscription if err := s.db.Where("channel = ? AND provider_sub_ref = ?", channel, ref).First(&sub).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrSubNotFound } return nil, fmt.Errorf("store.GetByProviderRef: %w", err) } return &sub, nil } // Activate 置 active + 刷新续费锚点(past_due 续费成功恢复也走它);canceled 终态不复活。 func (s *SubscriptionStore) Activate(subID string, periodEnd *time.Time) (bool, error) { upd := map[string]any{"status": model.SubActive} if periodEnd != nil { upd["current_period_end"] = periodEnd } res := s.db.Model(&model.Subscription{}). Where("sub_id = ? AND status <> ?", subID, model.SubCanceled).Updates(upd) if res.Error != nil { return false, fmt.Errorf("store.Activate: %w", res.Error) } return res.RowsAffected > 0, nil } func (s *SubscriptionStore) MarkPastDue(channel, providerRef string) (bool, error) { res := s.db.Model(&model.Subscription{}). Where("channel = ? AND provider_sub_ref = ? AND status = ?", channel, providerRef, model.SubActive). Update("status", model.SubPastDue) if res.Error != nil { return false, fmt.Errorf("store.MarkPastDue: %w", res.Error) } return res.RowsAffected > 0, nil } func (s *SubscriptionStore) MarkCanceled(subID string) (bool, error) { res := s.db.Model(&model.Subscription{}). Where("sub_id = ? AND status <> ?", subID, model.SubCanceled). Updates(map[string]any{"status": model.SubCanceled, "canceled_at": time.Now()}) if res.Error != nil { return false, fmt.Errorf("store.MarkCanceled: %w", res.Error) } return res.RowsAffected > 0, nil }