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:
parent
23f9787b69
commit
3512db5ca4
35
migrations/20230219_add_subscription_renewal.go
Normal file
35
migrations/20230219_add_subscription_renewal.go
Normal 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)
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user