1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00

fix: track subscription renewal date

This commit is contained in:
Ferdinand Mütsch 2023-02-19 19:37:03 +01:00
parent 23f9787b69
commit 3512db5ca4
5 changed files with 85 additions and 27 deletions

View File

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

View File

@ -15,29 +15,30 @@ func init() {
} }
type User struct { type User struct {
ID string `json:"id" gorm:"primary_key"` ID string `json:"id" gorm:"primary_key"`
ApiKey string `json:"api_key" gorm:"unique; default:NULL"` ApiKey string `json:"api_key" gorm:"unique; default:NULL"`
Email string `json:"email" gorm:"index:idx_user_email; size:255"` Email string `json:"email" gorm:"index:idx_user_email; size:255"`
Location string `json:"location"` Location string `json:"location"`
Password string `json:"-"` Password string `json:"-"`
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"` 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"` LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
ShareDataMaxDays int `json:"-"` ShareDataMaxDays int `json:"-"`
ShareEditors bool `json:"-" gorm:"default:false; type:bool"` ShareEditors bool `json:"-" gorm:"default:false; type:bool"`
ShareLanguages bool `json:"-" gorm:"default:false; type:bool"` ShareLanguages bool `json:"-" gorm:"default:false; type:bool"`
ShareProjects 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"` ShareOSs bool `json:"-" gorm:"default:false; type:bool; column:share_oss"`
ShareMachines bool `json:"-" gorm:"default:false; type:bool"` ShareMachines bool `json:"-" gorm:"default:false; type:bool"`
ShareLabels bool `json:"-" gorm:"default:false; type:bool"` ShareLabels bool `json:"-" gorm:"default:false; type:bool"`
IsAdmin bool `json:"-" gorm:"default:false; type:bool"` IsAdmin bool `json:"-" gorm:"default:false; type:bool"`
HasData bool `json:"-" gorm:"default:false; type:bool"` HasData bool `json:"-" gorm:"default:false; type:bool"`
WakatimeApiKey string `json:"-"` // for relay middleware and imports WakatimeApiKey string `json:"-"` // for relay middleware and imports
WakatimeApiUrl string `json:"-"` // for relay middleware and imports WakatimeApiUrl string `json:"-"` // for relay middleware and imports
ResetToken string `json:"-"` ResetToken string `json:"-"`
ReportsWeekly bool `json:"-" gorm:"default:false; type:bool"` ReportsWeekly bool `json:"-" gorm:"default:false; type:bool"`
PublicLeaderboard 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"` SubscribedUntil *CustomTime `json:"-" gorm:"type:timestamp" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
StripeCustomerId string `json:"-"` SubscriptionRenewal *CustomTime `json:"-" gorm:"type:timestamp" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
StripeCustomerId string `json:"-"`
} }
type Login struct { type Login struct {

View File

@ -278,14 +278,17 @@ func (h *SubscriptionHandler) handleSubscriptionEvent(subscription *stripe.Subsc
if user.SubscribedUntil == nil || !user.SubscribedUntil.T().Equal(until.T()) { if user.SubscribedUntil == nil || !user.SubscribedUntil.T().Equal(until.T()) {
hasSubscribed = true hasSubscribed = true
user.SubscribedUntil = &until user.SubscribedUntil = &until
user.SubscriptionRenewal = &until
logbuch.Info("user %s got active subscription %s until %v", user.ID, subscription.ID, user.SubscribedUntil) 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()) { 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) 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.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) logbuch.Info("user %s's subscription %s got canceled, because of status update to '%s'", user.ID, subscription.ID, subscription.Status)
default: default:
logbuch.Info("got subscription (%s) status update to '%s' for user '%s'", subscription.ID, subscription.Status, user.ID) logbuch.Info("got subscription (%s) status update to '%s' for user '%s'", subscription.ID, subscription.Status, user.ID)

View File

@ -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) // - 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 has an e-mail address configured
// - User's subscription has expired or is about to expire soon // - 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 // - 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. // Note: only one mail will be sent for either "expired" or "about to expire" state.
func (srv *MiscService) NotifyExpiringSubscription() { func (srv *MiscService) NotifyExpiringSubscription() {
@ -187,6 +188,7 @@ func (srv *MiscService) NotifyExpiringSubscription() {
return return
} }
now := time.Now()
logbuch.Info("notifying users about soon to expire subscriptions") logbuch.Info("notifying users about soon to expire subscriptions")
users, err := srv.userService.GetAll() 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) 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 without e-mail address
// skip users who already received a notification before // skip users who already received a notification before
// skip users who either never had a subscription before or intentionally deleted it // 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 continue
} }

View File

@ -729,7 +729,12 @@
<span class="font-semibold text-gray-300">Subscription status:</span> <span class="font-semibold text-gray-300">Subscription status:</span>
<span class="text-gray-600 ml-1 text-sm"> <span class="text-gray-600 ml-1 text-sm">
{{ if .User.HasActiveSubscription }} {{ if .User.HasActiveSubscription }}
<span class="font-semibold text-green-500 text-base">Active</span> (until {{ .User.SubscribedUntil.T | date }}) <span class="font-semibold text-green-500 text-base">Active</span>
{{ if .User.SubscriptionRenewal }}
(automatically renews at {{ .User.SubscriptionRenewal.T | date }})
{{ else }}
(until {{ .User.SubscribedUntil.T | date }})
{{ end }}
{{ else }} {{ else }}
<span class="font-semibold text-red-500 text-base">Inactive</span> <span class="font-semibold text-red-500 text-base">Inactive</span>
{{ end }} {{ end }}
@ -746,7 +751,7 @@
</form> </form>
{{ else }} {{ else }}
<form action="subscription/portal" method="post" class="mt-8 mb-8" id="form-subscription-portal"> <form action="subscription/portal" method="post" class="mt-8 mb-8" id="form-subscription-portal">
<button type="submit" class="btn-danger">Cancel subscription</button> <button type="submit" class="btn-primary">Manage subscription</button>
</form> </form>
{{ end }} {{ end }}
</div> </div>