Files
pay/internal/store/subscription.go
T

88 lines
2.9 KiB
Go

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
}