88 lines
2.9 KiB
Go
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
|
|
}
|