diff --git a/migrations/20230219_add_subscription_renewal.go b/migrations/20230219_add_subscription_renewal.go new file mode 100644 index 0000000..6bb26ae --- /dev/null +++ b/migrations/20230219_add_subscription_renewal.go @@ -0,0 +1,35 @@ +package migrations + +import ( + "github.com/emvi/logbuch" + "github.com/muety/wakapi/config" + "github.com/muety/wakapi/models" + "gorm.io/gorm" +) + +func init() { + const name = "20230219-add_subscription_renewal" + f := migrationFunc{ + name: name, + f: func(db *gorm.DB, cfg *config.Config) error { + if hasRun(name, db) { + return nil + } + + migrator := db.Migrator() + + if migrator.HasColumn(&models.User{}, "subscription_renewal") { + logbuch.Info("running migration '%s'", name) + + if err := db.Exec("UPDATE users SET subscription_renewal = subscribed_until WHERE subscribed_until is not null").Error; err != nil { + return err + } + } + + setHasRun(name, db) + return nil + }, + } + + registerPostMigration(f) +} diff --git a/models/user.go b/models/user.go index 43e2bb3..c46c88d 100644 --- a/models/user.go +++ b/models/user.go @@ -15,29 +15,30 @@ func init() { } type User struct { - ID string `json:"id" gorm:"primary_key"` - ApiKey string `json:"api_key" gorm:"unique; default:NULL"` - Email string `json:"email" gorm:"index:idx_user_email; size:255"` - Location string `json:"location"` - Password string `json:"-"` - CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"` - LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"` - ShareDataMaxDays int `json:"-"` - ShareEditors bool `json:"-" gorm:"default:false; type:bool"` - ShareLanguages bool `json:"-" gorm:"default:false; type:bool"` - ShareProjects bool `json:"-" gorm:"default:false; type:bool"` - ShareOSs bool `json:"-" gorm:"default:false; type:bool; column:share_oss"` - ShareMachines bool `json:"-" gorm:"default:false; type:bool"` - ShareLabels bool `json:"-" gorm:"default:false; type:bool"` - IsAdmin bool `json:"-" gorm:"default:false; type:bool"` - HasData bool `json:"-" gorm:"default:false; type:bool"` - WakatimeApiKey string `json:"-"` // for relay middleware and imports - WakatimeApiUrl string `json:"-"` // for relay middleware and imports - ResetToken string `json:"-"` - ReportsWeekly bool `json:"-" gorm:"default:false; type:bool"` - PublicLeaderboard bool `json:"-" gorm:"default:false; type:bool"` - SubscribedUntil *CustomTime `json:"-" gorm:"type:timestamp" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"` - StripeCustomerId string `json:"-"` + ID string `json:"id" gorm:"primary_key"` + ApiKey string `json:"api_key" gorm:"unique; default:NULL"` + Email string `json:"email" gorm:"index:idx_user_email; size:255"` + Location string `json:"location"` + Password string `json:"-"` + CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"` + LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"` + ShareDataMaxDays int `json:"-"` + ShareEditors bool `json:"-" gorm:"default:false; type:bool"` + ShareLanguages bool `json:"-" gorm:"default:false; type:bool"` + ShareProjects bool `json:"-" gorm:"default:false; type:bool"` + ShareOSs bool `json:"-" gorm:"default:false; type:bool; column:share_oss"` + ShareMachines bool `json:"-" gorm:"default:false; type:bool"` + ShareLabels bool `json:"-" gorm:"default:false; type:bool"` + IsAdmin bool `json:"-" gorm:"default:false; type:bool"` + HasData bool `json:"-" gorm:"default:false; type:bool"` + WakatimeApiKey string `json:"-"` // for relay middleware and imports + WakatimeApiUrl string `json:"-"` // for relay middleware and imports + ResetToken string `json:"-"` + ReportsWeekly bool `json:"-" gorm:"default:false; type:bool"` + PublicLeaderboard bool `json:"-" gorm:"default:false; type:bool"` + SubscribedUntil *CustomTime `json:"-" gorm:"type:timestamp" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"` + SubscriptionRenewal *CustomTime `json:"-" gorm:"type:timestamp" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"` + StripeCustomerId string `json:"-"` } type Login struct { diff --git a/routes/subscription.go b/routes/subscription.go index 5aa4fc4..f9e9f51 100644 --- a/routes/subscription.go +++ b/routes/subscription.go @@ -278,14 +278,17 @@ func (h *SubscriptionHandler) handleSubscriptionEvent(subscription *stripe.Subsc if user.SubscribedUntil == nil || !user.SubscribedUntil.T().Equal(until.T()) { hasSubscribed = true user.SubscribedUntil = &until + user.SubscriptionRenewal = &until logbuch.Info("user %s got active subscription %s until %v", user.ID, subscription.ID, user.SubscribedUntil) } if cancelAt := time.Unix(subscription.CancelAt, 0); !cancelAt.IsZero() && cancelAt.After(time.Now()) { + user.SubscriptionRenewal = nil logbuch.Info("user %s chose to cancel subscription %s by %v", user.ID, subscription.ID, cancelAt) } - case "canceled", "unpaid": + case "canceled", "unpaid", "incomplete_expired": user.SubscribedUntil = nil + user.SubscriptionRenewal = nil logbuch.Info("user %s's subscription %s got canceled, because of status update to '%s'", user.ID, subscription.ID, subscription.Status) default: logbuch.Info("got subscription (%s) status update to '%s' for user '%s'", subscription.ID, subscription.Status, user.ID) diff --git a/services/misc.go b/services/misc.go index a1c628d..90a9bf9 100644 --- a/services/misc.go +++ b/services/misc.go @@ -180,6 +180,7 @@ func (srv *MiscService) ComputeOldestHeartbeats() { // - Subscriptions are enabled on the server (aka. users can do something about their old data getting cleaned up) // - User has an e-mail address configured // - User's subscription has expired or is about to expire soon +// - User doesn't have upcoming auto-renewal (i.e. chose to cancel at some date in the future) // - The user has gotten no such e-mail before recently // Note: only one mail will be sent for either "expired" or "about to expire" state. func (srv *MiscService) NotifyExpiringSubscription() { @@ -187,6 +188,7 @@ func (srv *MiscService) NotifyExpiringSubscription() { return } + now := time.Now() logbuch.Info("notifying users about soon to expire subscriptions") users, err := srv.userService.GetAll() @@ -210,10 +212,22 @@ func (srv *MiscService) NotifyExpiringSubscription() { config.Log().Warn("invalid state: user '%s' has active subscription but no e-mail address set", u.ID) } + var alreadySent bool + if kvs, ok := subscriptionReminders[u.ID]; ok && len(kvs) > 0 { + // don't send again while subscription hasn't had chance to have been renewed + if sendDate, err := time.Parse(time.RFC822Z, kvs[0].Value); err == nil && now.Sub(sendDate) <= notifyBeforeSubscriptionExpiry { + alreadySent = true + } else if err != nil { + config.Log().Error("failed to parse date for last sent subscription notification mail for user '%s', %v", u.ID, err) + alreadySent = true + } + } + // skip users without e-mail address // skip users who already received a notification before // skip users who either never had a subscription before or intentionally deleted it - if _, ok := subscriptionReminders[u.ID]; ok || u.Email == "" || u.SubscribedUntil == nil { + // skip users who have upcoming auto-renewal (everyone except users who chose to cancel subscription at later date) + if alreadySent || u.Email == "" || u.SubscribedUntil == nil || (u.SubscriptionRenewal != nil && u.SubscriptionRenewal.T().After(now)) { continue } diff --git a/views/settings.tpl.html b/views/settings.tpl.html index afa7d58..b2df66c 100644 --- a/views/settings.tpl.html +++ b/views/settings.tpl.html @@ -729,7 +729,12 @@ Subscription status: {{ if .User.HasActiveSubscription }} - Active (until {{ .User.SubscribedUntil.T | date }}) + Active + {{ if .User.SubscriptionRenewal }} + (automatically renews at {{ .User.SubscriptionRenewal.T | date }}) + {{ else }} + (until {{ .User.SubscribedUntil.T | date }}) + {{ end }} {{ else }} Inactive {{ end }} @@ -746,7 +751,7 @@ {{ else }}
- +
{{ end }}