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 }}