diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index abdd9e58ce5..434d0dc67f3 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -5,9 +5,6 @@ import ( "database/sql" b64 "encoding/base64" "fmt" - "github.com/ente-io/museum/pkg/controller/collections" - publicCtrl "github.com/ente-io/museum/pkg/controller/public" - "github.com/ente-io/museum/pkg/repo/public" "net/http" "os" "os/signal" @@ -17,6 +14,10 @@ import ( "syscall" "time" + "github.com/ente-io/museum/pkg/controller/collections" + publicCtrl "github.com/ente-io/museum/pkg/controller/public" + "github.com/ente-io/museum/pkg/repo/public" + "github.com/ente-io/museum/ente/base" "github.com/ente-io/museum/pkg/controller/emergency" "github.com/ente-io/museum/pkg/controller/file_copy" @@ -213,16 +214,17 @@ func main() { stripeClients := billing.GetStripeClients() commonBillController := commonbilling.NewController(emailNotificationCtrl, storagBonusRepo, userRepo, usageRepo, billingRepo) appStoreController := controller.NewAppStoreController(defaultPlan, - billingRepo, fileRepo, userRepo, commonBillController) + billingRepo, fileRepo, userRepo, notificationHistoryRepo, commonBillController) + remoteStoreController := &remoteStoreCtrl.Controller{Repo: remoteStoreRepository} playStoreController := controller.NewPlayStoreController(defaultPlan, - billingRepo, fileRepo, userRepo, storagBonusRepo, commonBillController) + billingRepo, fileRepo, userRepo, notificationHistoryRepo, storagBonusRepo, commonBillController) stripeController := controller.NewStripeController(plans, stripeClients, - billingRepo, fileRepo, userRepo, storagBonusRepo, discordController, emailNotificationCtrl, offerController, commonBillController) + billingRepo, fileRepo, userRepo, storagBonusRepo, notificationHistoryRepo, discordController, emailNotificationCtrl, offerController, commonBillController) billingController := controller.NewBillingController(plans, appStoreController, playStoreController, stripeController, discordController, emailNotificationCtrl, billingRepo, userRepo, usageRepo, storagBonusRepo, commonBillController) - remoteStoreController := &remoteStoreCtrl.Controller{Repo: remoteStoreRepository, BillingCtrl: billingController} + remoteStoreController = &remoteStoreCtrl.Controller{Repo: remoteStoreRepository, BillingCtrl: billingController} pushController := controller.NewPushController(pushRepo, taskLockingRepo, hostName) mailingListsController := controller.NewMailingListsController() @@ -1038,6 +1040,10 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, collectionLinkRep emailNotificationCtrl.NudgePaidSubscriberForFamily() }) + scheduleAndRun(c, "@every 6h", func() { + emailNotificationCtrl.SendStorageLimitExceedingMails() + }) + schedule(c, "@every 1m", func() { pushController.SendPushes() }) diff --git a/server/compose.yaml b/server/compose.yaml index 1def7f528eb..e5e7984a2d0 100644 --- a/server/compose.yaml +++ b/server/compose.yaml @@ -58,7 +58,7 @@ services: sh -c ' #!/bin/sh - while ! mc config host add h0 http://minio:3200 changeme changeme1234 2>/dev/null + while ! mc alias set h0 http://minio:3200 changeme changeme1234 2>/dev/null do echo "Waiting for minio..." sleep 0.5 diff --git a/server/docs/quickstart.md b/server/docs/quickstart.md index c43749b539d..6ff3646273f 100644 --- a/server/docs/quickstart.md +++ b/server/docs/quickstart.md @@ -23,6 +23,8 @@ sh -c "$(curl -fsSL https://raw.githubusercontent.com/ente-io/ente/main/server/q > chmod +x quickstart.sh > ./quickstart.sh + +Which will prompt to start the Docker compose cluster. After the Docker compose cluster starts, you can open Ente web app at http://localhost:3000. diff --git a/server/mail-templates/storage_limit_exceeding_free.html b/server/mail-templates/storage_limit_exceeding_free.html new file mode 100644 index 00000000000..2fb2fbb4a58 --- /dev/null +++ b/server/mail-templates/storage_limit_exceeding_free.html @@ -0,0 +1,141 @@ + + + + + + + +
 
+
+

Hi there,

+ +

You have used 95% of your available storage on Ente Photos. All your photos on Ente will continue to remain safe and accessible. However, new photos will not be backed up after you get to 100%.

+ +

Please upgrade your plan so that all the new photos you take can be backed up. Once you upgrade, you will be able to access family plans, photo and album sharing, and all your photos will have triple backups. + You can also use our referral program to get more storage at no additional cost.

+ +

For any help, please reach out to support@ente.io or reply to this email.

+
+
+ + + diff --git a/server/mail-templates/storage_limit_exceeding_paid.html b/server/mail-templates/storage_limit_exceeding_paid.html new file mode 100644 index 00000000000..3a43f0e2500 --- /dev/null +++ b/server/mail-templates/storage_limit_exceeding_paid.html @@ -0,0 +1,140 @@ + + + + + + + +
 
+
+

Hi there,

+ +

You have used 95% of your available storage on Ente Photos. All your photos on Ente will continue to remain safe and accessible. However, new photos will not be backed up after you get to 100%.

+ +

Please upgrade your plan so that all the new photos you take can be backed up. You can also use our referral program to get more storage at no additional cost.

+ +

For any help, please reach out to support@ente.io or reply to this email.

+
+
+ + + diff --git a/server/pkg/controller/appstore.go b/server/pkg/controller/appstore.go index 6325aa28436..e96392c18cb 100644 --- a/server/pkg/controller/appstore.go +++ b/server/pkg/controller/appstore.go @@ -7,6 +7,8 @@ import ( "strings" "github.com/ente-io/museum/pkg/controller/commonbilling" + emailCtrl "github.com/ente-io/museum/pkg/controller/email" + "github.com/prometheus/common/log" "github.com/ente-io/stacktrace" @@ -23,12 +25,13 @@ import ( // AppStoreController provides abstractions for handling billing on AppStore type AppStoreController struct { - AppStoreClient appstore.Client - BillingRepo *repo.BillingRepository - FileRepo *repo.FileRepository - UserRepo *repo.UserRepository - BillingPlansPerCountry ente.BillingPlansPerCountry - CommonBillCtrl *commonbilling.Controller + AppStoreClient appstore.Client + BillingRepo *repo.BillingRepository + FileRepo *repo.FileRepository + UserRepo *repo.UserRepository + NotificationHistoryRepo *repo.NotificationHistoryRepository + BillingPlansPerCountry ente.BillingPlansPerCountry + CommonBillCtrl *commonbilling.Controller // appStoreSharedPassword is the password to be used to access AppStore APIs appStoreSharedPassword string } @@ -39,17 +42,19 @@ func NewAppStoreController( billingRepo *repo.BillingRepository, fileRepo *repo.FileRepository, userRepo *repo.UserRepository, + notificationHistoryRepo *repo.NotificationHistoryRepository, commonBillCtrl *commonbilling.Controller, ) *AppStoreController { appleSharedSecret := viper.GetString("apple.shared-secret") return &AppStoreController{ - AppStoreClient: *appstore.New(), - BillingRepo: billingRepo, - FileRepo: fileRepo, - UserRepo: userRepo, - BillingPlansPerCountry: plans, - appStoreSharedPassword: appleSharedSecret, - CommonBillCtrl: commonBillCtrl, + AppStoreClient: *appstore.New(), + BillingRepo: billingRepo, + FileRepo: fileRepo, + UserRepo: userRepo, + NotificationHistoryRepo: notificationHistoryRepo, + BillingPlansPerCountry: plans, + appStoreSharedPassword: appleSharedSecret, + CommonBillCtrl: commonBillCtrl, } } @@ -118,6 +123,10 @@ func (c *AppStoreController) HandleNotification(ctx *gin.Context, notification a if err != nil { return stacktrace.Propagate(err, "") } + + c.NotificationHistoryRepo.DeleteLastNotification(subscription.UserID, emailCtrl.StorageLimitExceededTemplateID) + c.NotificationHistoryRepo.DeleteLastNotification(subscription.UserID, emailCtrl.StorageLimitExceedingTemplateID) + } else { if notification.NotificationType == appstore.NotificationTypeDidChangeRenewalStatus { err := c.BillingRepo.UpdateSubscriptionCancellationStatus(subscription.UserID, notification.AutoRenewStatus == "false") diff --git a/server/pkg/controller/commonbilling/controller.go b/server/pkg/controller/commonbilling/controller.go index 0e816bcf26b..8af19515572 100644 --- a/server/pkg/controller/commonbilling/controller.go +++ b/server/pkg/controller/commonbilling/controller.go @@ -16,6 +16,7 @@ type Controller struct { UserRepo *repo.UserRepository UsageRepo *repo.UsageRepository BillingRepo *repo.BillingRepository + NotificationHistoryRepo *repo.NotificationHistoryRepository } func NewController( @@ -77,6 +78,7 @@ func (c *Controller) OnSubscriptionCancelled(userID int64) error { if err != nil { return stacktrace.Propagate(err, "") } + go c.EmailNotificationController.OnSubscriptionCancelled(userID) return nil } diff --git a/server/pkg/controller/email/email_notification.go b/server/pkg/controller/email/email_notification.go index 3f6ab25d86d..6a2b074e5e1 100644 --- a/server/pkg/controller/email/email_notification.go +++ b/server/pkg/controller/email/email_notification.go @@ -49,13 +49,20 @@ const ( FamilyNudgeTemplate = "family_nudge.html" FamilyNudgeSubject = "Share your Ente Subscription with your Family!" FamilyNudgeTemplateID = "family_nudge" + + StorageLimitExceedingMailLock = "storage_limit_exceeding" + StorageLimitExceedingPaidTemplate = "storage_limit_exceeding_paid.html" + StorageLimitExceedingFreeTemplate = "storage_limit_exceeding_free.html" + StorageLimitExceedingSubject = "Your Ente storage is 95% full" + StorageLimitExceedingTemplateID = "storage_limit_exceeding" ) type EmailNotificationController struct { - UserRepo *repo.UserRepository - LockController *lock.LockController - NotificationHistoryRepo *repo.NotificationHistoryRepository - isSendingStorageLimitExceededMails bool + UserRepo *repo.UserRepository + LockController *lock.LockController + NotificationHistoryRepo *repo.NotificationHistoryRepository + isSendingStorageLimitExceededMails bool + isSendingStorageLimitExceedingMails bool } func (c *EmailNotificationController) OnFirstFileUpload(userID int64, userAgent string) { @@ -161,17 +168,24 @@ func (c *EmailNotificationController) OnSubscriptionCancelled(userID int64) { log.Error("Could not find user to email", err) return } + log.Info(fmt.Sprintf("Emailing on subscription cancellation %d", user.ID)) err = email.SendTemplatedEmail([]string{user.Email}, "vishnu@ente.io", "vishnu@ente.io", SubscriptionCancelledSubject, SubscriptionCancelledTemplate, nil, nil) if err != nil { log.Error("Error sending email", err) } + + // deletion for storage limit exceeding email notification + c.NotificationHistoryRepo.DeleteLastNotification(userID, StorageLimitExceedingTemplateID) + + // deletion for storage limit exceeded email notification + c.NotificationHistoryRepo.DeleteLastNotification(userID, StorageLimitExceededTemplateID) } // SayHelloToCustomers sends an email to check in with customers who upgraded 7 // days ago. func (c *EmailNotificationController) SayHelloToCustomers() { - log.Info("Running SayHelloToCustomers") + log.Info("Sending hello mail to paid users") lockStatus := c.LockController.TryLock(CustomerHelloMailLock, time.MicrosecondsAfterHours(24)) if !lockStatus { log.Error("Could not acquire lock to send customer hellos") @@ -251,8 +265,12 @@ func (c *EmailNotificationController) setStorageLimitExceededMailerJobStatus(isS c.isSendingStorageLimitExceededMails = isSending } +func (c *EmailNotificationController) setStorageLimitExceedingMailerJobStatus(isSending bool) { + c.isSendingStorageLimitExceedingMails = isSending +} + func (c *EmailNotificationController) NudgePaidSubscriberForFamily() { - log.Info("Running NudgePaidSubscriberForFamily") + log.Info("Running family nudge for family") lockStatus := c.LockController.TryLock(FamilyNudgeMailLock, time.MicrosecondsAfterHours(24)) if !lockStatus { log.Error("Could not acquire lock to send family nudge mails") @@ -294,3 +312,66 @@ func (c *EmailNotificationController) NudgePaidSubscriberForFamily() { c.NotificationHistoryRepo.SetLastNotificationTimeToNow(u.ID, FamilyNudgeTemplateID) } } + +func (c *EmailNotificationController) SendStorageLimitExceedingMails() { + if c.isSendingStorageLimitExceedingMails { + log.Info("Skipping sending storage limit exceeding mails as another instance is still running") + return + } + c.setStorageLimitExceedingMailerJobStatus(true) + defer c.setStorageLimitExceedingMailerJobStatus(false) + lockStatus := c.LockController.TryLock(StorageLimitExceedingMailLock, time.MicrosecondsAfterHours(24)) + if !lockStatus { + log.Error("Could not acquire lock to send storage limit exceeding mails") + return + } + defer c.LockController.ReleaseLock(StorageLimitExceedingMailLock) + + notifications := []struct { + getSubscribers func() ([]ente.User, error) + template string + }{ + { + getSubscribers: func() ([]ente.User, error) { + return c.UserRepo.GetFreeUsersWhoAreExceedingStorageQuota() + }, + template: StorageLimitExceedingPaidTemplate, + }, + { + getSubscribers: func() ([]ente.User, error) { + return c.UserRepo.GetPaidUsersWhoAreExceedingStorageQuota() + }, + template: StorageLimitExceedingFreeTemplate, + }, + } + + for _, notification := range notifications { + users, err := notification.getSubscribers() + if err != nil { + log.Error("Error while fetching user list", err) + return + } + + for _, u := range users { + lastNotificationTime, err := c.NotificationHistoryRepo.GetLastNotificationTime(u.ID, StorageLimitExceedingTemplateID) + logger := log.WithFields(log.Fields{ + "user_id": u.ID, + }) + if err != nil { + logger.Error("Could not fetch last notification time", err) + continue + } + if lastNotificationTime > 0 { + continue + } + logger.Info("Alerting about storage limit exceeding") + err = email.SendTemplatedEmail([]string{u.Email}, "team@ente.io", "team@ente.io", StorageLimitExceedingSubject, notification.template, nil, nil) + if err != nil { + logger.Info("Error notifying", err) + continue + } + c.NotificationHistoryRepo.SetLastNotificationTimeToNow(u.ID, StorageLimitExceedingTemplateID) + } + + } +} diff --git a/server/pkg/controller/playstore.go b/server/pkg/controller/playstore.go index 4c155792c86..44f2d9941fe 100644 --- a/server/pkg/controller/playstore.go +++ b/server/pkg/controller/playstore.go @@ -6,6 +6,7 @@ import ( "os" "github.com/ente-io/museum/pkg/controller/commonbilling" + emailCtrl "github.com/ente-io/museum/pkg/controller/email" "github.com/ente-io/museum/pkg/repo/storagebonus" "github.com/ente-io/stacktrace" @@ -22,13 +23,14 @@ import ( // PlayStoreController provides abstractions for handling billing on AppStore type PlayStoreController struct { - PlayStoreClient *playstore.Client - BillingRepo *repo.BillingRepository - FileRepo *repo.FileRepository - UserRepo *repo.UserRepository - StorageBonusRepo *storagebonus.Repository - BillingPlansPerCountry ente.BillingPlansPerCountry - CommonBillCtrl *commonbilling.Controller + PlayStoreClient *playstore.Client + BillingRepo *repo.BillingRepository + FileRepo *repo.FileRepository + UserRepo *repo.UserRepository + NotificationHistoryRepo *repo.NotificationHistoryRepository + StorageBonusRepo *storagebonus.Repository + BillingPlansPerCountry ente.BillingPlansPerCountry + CommonBillCtrl *commonbilling.Controller } // PlayStorePackageName is the package name of the PlayStore item @@ -40,6 +42,7 @@ func NewPlayStoreController( billingRepo *repo.BillingRepository, fileRepo *repo.FileRepository, userRepo *repo.UserRepository, + notificationHistoryRepo *repo.NotificationHistoryRepository, storageBonusRepo *storagebonus.Repository, commonBillCtrl *commonbilling.Controller, ) *PlayStoreController { @@ -52,13 +55,14 @@ func NewPlayStoreController( // environment and so playStoreClient really should've been there. return &PlayStoreController{ - PlayStoreClient: playStoreClient, - BillingRepo: billingRepo, - FileRepo: fileRepo, - UserRepo: userRepo, - BillingPlansPerCountry: plans, - StorageBonusRepo: storageBonusRepo, - CommonBillCtrl: commonBillCtrl, + PlayStoreClient: playStoreClient, + BillingRepo: billingRepo, + FileRepo: fileRepo, + UserRepo: userRepo, + NotificationHistoryRepo: notificationHistoryRepo, + BillingPlansPerCountry: plans, + StorageBonusRepo: storageBonusRepo, + CommonBillCtrl: commonBillCtrl, } } @@ -188,6 +192,9 @@ func (c *PlayStoreController) HandleNotification(notification playstore.Develope if err != nil { return stacktrace.Propagate(err, "") } + c.NotificationHistoryRepo.DeleteLastNotification(subscription.UserID, emailCtrl.StorageLimitExceededTemplateID) + c.NotificationHistoryRepo.DeleteLastNotification(subscription.UserID, emailCtrl.StorageLimitExceedingTemplateID) + } else { err = c.BillingRepo.UpdateSubscriptionExpiryTime( subscription.ID, purchase.ExpiryTimeMillis*1000) diff --git a/server/pkg/controller/stripe.go b/server/pkg/controller/stripe.go index 7e8b6ca837d..bae76c2aef1 100644 --- a/server/pkg/controller/stripe.go +++ b/server/pkg/controller/stripe.go @@ -17,10 +17,10 @@ import ( "github.com/ente-io/museum/ente" emailCtrl "github.com/ente-io/museum/pkg/controller/email" - timeUtil "github.com/ente-io/museum/pkg/utils/time" "github.com/ente-io/museum/pkg/repo" "github.com/ente-io/museum/pkg/utils/billing" "github.com/ente-io/museum/pkg/utils/email" + timeUtil "github.com/ente-io/museum/pkg/utils/time" "github.com/ente-io/stacktrace" log "github.com/sirupsen/logrus" "github.com/spf13/viper" @@ -32,33 +32,35 @@ import ( // StripeController provides abstractions for handling billing on Stripe type StripeController struct { - StripeClients ente.StripeClientPerAccount - BillingPlansPerAccount ente.BillingPlansPerAccount - BillingRepo *repo.BillingRepository - FileRepo *repo.FileRepository - UserRepo *repo.UserRepository - StorageBonusRepo *storagebonus.Repository - DiscordController *discord.DiscordController - EmailNotificationCtrl *emailCtrl.EmailNotificationController - OfferController *offer.OfferController - CommonBillCtrl *commonbilling.Controller + StripeClients ente.StripeClientPerAccount + BillingPlansPerAccount ente.BillingPlansPerAccount + BillingRepo *repo.BillingRepository + FileRepo *repo.FileRepository + UserRepo *repo.UserRepository + StorageBonusRepo *storagebonus.Repository + NotificationHistoryRepo *repo.NotificationHistoryRepository + DiscordController *discord.DiscordController + EmailNotificationCtrl *emailCtrl.EmailNotificationController + OfferController *offer.OfferController + CommonBillCtrl *commonbilling.Controller } const BufferPeriodOnPaymentFailureInDays = 7 // Return a new instance of StripeController -func NewStripeController(plans ente.BillingPlansPerAccount, stripeClients ente.StripeClientPerAccount, billingRepo *repo.BillingRepository, fileRepo *repo.FileRepository, userRepo *repo.UserRepository, storageBonusRepo *storagebonus.Repository, discordController *discord.DiscordController, emailNotificationController *emailCtrl.EmailNotificationController, offerController *offer.OfferController, commonBillCtrl *commonbilling.Controller) *StripeController { +func NewStripeController(plans ente.BillingPlansPerAccount, stripeClients ente.StripeClientPerAccount, billingRepo *repo.BillingRepository, fileRepo *repo.FileRepository, userRepo *repo.UserRepository, storageBonusRepo *storagebonus.Repository, notificationHistoryRepo *repo.NotificationHistoryRepository, discordController *discord.DiscordController, emailNotificationController *emailCtrl.EmailNotificationController, offerController *offer.OfferController, commonBillCtrl *commonbilling.Controller) *StripeController { return &StripeController{ - StripeClients: stripeClients, - BillingRepo: billingRepo, - FileRepo: fileRepo, - UserRepo: userRepo, - BillingPlansPerAccount: plans, - StorageBonusRepo: storageBonusRepo, - DiscordController: discordController, - EmailNotificationCtrl: emailNotificationController, - OfferController: offerController, - CommonBillCtrl: commonBillCtrl, + StripeClients: stripeClients, + BillingRepo: billingRepo, + FileRepo: fileRepo, + UserRepo: userRepo, + BillingPlansPerAccount: plans, + StorageBonusRepo: storageBonusRepo, + NotificationHistoryRepo: notificationHistoryRepo, + DiscordController: discordController, + EmailNotificationCtrl: emailNotificationController, + OfferController: offerController, + CommonBillCtrl: commonBillCtrl, } } @@ -436,7 +438,9 @@ func (c *StripeController) UpdateSubscription(stripeID string, userID int64) (en if subscription.PaymentProvider != ente.Stripe || subscription.ProductID == stripeID || subscription.Attributes.StripeAccountCountry != newStripeAccountCountry { return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(ente.ErrBadRequest, "") } - if newPlan.Storage < subscription.Storage { // Downgrade + + // Downgrade + if newPlan.Storage < subscription.Storage { canDowngrade, canDowngradeErr := c.CommonBillCtrl.CanDowngradeToGivenStorage(newPlan.Storage, userID) if canDowngradeErr != nil { return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(canDowngradeErr, "") @@ -447,6 +451,7 @@ func (c *StripeController) UpdateSubscription(stripeID string, userID int64) (en log.Info("Usage is good") } + stripeSubscription, err := c.getStripeSubscriptionWithPaymentMethod(subscription) if err != nil { return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(err, "") @@ -504,6 +509,8 @@ func (c *StripeController) UpdateSubscription(stripeID string, userID int64) (en return ente.SubscriptionUpdateResponse{}, stacktrace.Propagate(ente.ErrBadRequest, "") } } + c.NotificationHistoryRepo.DeleteLastNotification(userID, emailCtrl.StorageLimitExceededTemplateID) + c.NotificationHistoryRepo.DeleteLastNotification(userID, emailCtrl.StorageLimitExceedingTemplateID) return ente.SubscriptionUpdateResponse{Status: "success"}, nil } diff --git a/server/pkg/repo/notificationhistory.go b/server/pkg/repo/notificationhistory.go index 3e1baece296..4b53e880cfb 100644 --- a/server/pkg/repo/notificationhistory.go +++ b/server/pkg/repo/notificationhistory.go @@ -2,6 +2,7 @@ package repo import ( "database/sql" + "github.com/ente-io/stacktrace" "github.com/ente-io/museum/pkg/utils/time" @@ -24,6 +25,17 @@ func (repo *NotificationHistoryRepository) GetLastNotificationTime(userID int64, return 0, nil } +func (repo *NotificationHistoryRepository) DeleteLastNotification(userID int64, templateID string) error { + _, err := repo.DB.Exec(` + DELETE FROM notification_history + WHERE user_id = $1 AND template_id = $2 + `, userID, templateID) + if err != nil { + return stacktrace.Propagate(err, "failed to delete last notification") + } + return nil +} + func (repo *NotificationHistoryRepository) SetLastNotificationTimeToNow(userID int64, templateID string) error { _, err := repo.DB.Exec(`INSERT INTO notification_history(user_id, template_id, sent_time) VALUES($1, $2, $3)`, userID, templateID, time.Microseconds()) diff --git a/server/pkg/repo/user.go b/server/pkg/repo/user.go index bac3aaf2f1b..cac03101be4 100644 --- a/server/pkg/repo/user.go +++ b/server/pkg/repo/user.go @@ -332,6 +332,119 @@ func (repo *UserRepository) GetUsersWithIndividualPlanWhoHaveExceededStorageQuot return users, nil } +// GetPaidUsersWhoAreExceedingStorageQuota returns list of paid users who +// have consumed more than 95% of their storage quota and they are not part of any family plan +func (repo *UserRepository) GetPaidUsersWhoAreExceedingStorageQuota() ([]ente.User, error) { + rows, err := repo.DB.Query(` + SELECT users.user_id, users.encrypted_email, users.email_decryption_nonce, users.email_hash, usage.storage_consumed, subscriptions.storage + FROM users + INNER JOIN usage + ON users.user_id = usage.user_id + INNER JOIN subscriptions + ON users.user_id = subscriptions.user_id + AND subscriptions.product_id != 'free' + AND usage.storage_consumed > 0.95 * subscriptions.storage + AND users.encrypted_email IS NOT NULL + AND users.family_admin_id IS NULL; + `) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + refBonus, addOnBonus, bonusErr := repo.StorageBonusRepo.GetAllUsersSurplusBonus(context.Background()) + if bonusErr != nil { + return nil, stacktrace.Propagate(bonusErr, "failed to fetch bonusInfo") + } + defer rows.Close() + users := make([]ente.User, 0) + for rows.Next() { + var user ente.User + var encryptedEmail, nonce []byte + var storageConsumed, subStorage int64 + err := rows.Scan(&user.ID, &encryptedEmail, &nonce, &user.Hash, &storageConsumed, &subStorage) + if err != nil { + return users, stacktrace.Propagate(err, "") + } + // ignore deleted users + if strings.EqualFold(user.Hash, fmt.Sprintf(DELETED_EMAIL_HASH_FORMAT, &user.ID)) || len(encryptedEmail) == 0 { + continue + } + if refBonusStorage, ok := refBonus[user.ID]; ok { + addOnBonusStorage := addOnBonus[user.ID] + // cap usable ref bonus to the subscription storage + addOnBonus + if refBonusStorage > (subStorage + addOnBonusStorage) { + refBonusStorage = subStorage + addOnBonusStorage + } + totalStorageQuota := subStorage + refBonusStorage + addOnBonusStorage + if storageConsumed <= (95*totalStorageQuota/100) || storageConsumed >= totalStorageQuota { + continue + } + } + email, err := crypto.Decrypt(encryptedEmail, repo.SecretEncryptionKey, nonce) + if err != nil { + return users, stacktrace.Propagate(err, "") + } + user.Email = email + users = append(users, user) + } + return users, nil +} + +// GetFreeUsersWhoAreExceedingStorageQuota returns list of free users who +// have consumed more than 95% of their storage quota and they are not part of any family plan +func (repo *UserRepository) GetFreeUsersWhoAreExceedingStorageQuota() ([]ente.User, error) { + rows, err := repo.DB.Query(` + SELECT users.user_id, users.encrypted_email, users.email_decryption_nonce, users.email_hash, usage.storage_consumed, subscriptions.storage + FROM users + INNER JOIN usage + ON users.user_id = usage.user_id + INNER JOIN subscriptions + ON users.user_id = subscriptions.user_id + AND subscriptions.product_id = 'free' + AND usage.storage_consumed > 0.95 * subscriptions.storage + AND users.encrypted_email IS NOT NULL + AND users.family_admin_id IS NULL; + `) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + refBonus, addOnBonus, bonusErr := repo.StorageBonusRepo.GetAllUsersSurplusBonus(context.Background()) + if bonusErr != nil { + return nil, stacktrace.Propagate(bonusErr, "failed to fetch bonusInfo") + } + defer rows.Close() + users := make([]ente.User, 0) + for rows.Next() { + var user ente.User + var encryptedEmail, nonce []byte + var storageConsumed, subStorage int64 + err := rows.Scan(&user.ID, &encryptedEmail, &nonce, &user.Hash, &storageConsumed, &subStorage) + if err != nil { + return users, stacktrace.Propagate(err, "") + } + // ignore deleted users + if strings.EqualFold(user.Hash, fmt.Sprintf(DELETED_EMAIL_HASH_FORMAT, &user.ID)) || len(encryptedEmail) == 0 { + continue + } + if refBonusStorage, ok := refBonus[user.ID]; ok { + addOnBonusStorage := addOnBonus[user.ID] + // cap usable ref bonus to the subscription storage + addOnBonus + if refBonusStorage > (subStorage + addOnBonusStorage) { + refBonusStorage = subStorage + addOnBonusStorage + } + if (storageConsumed) <= (95 * (subStorage + refBonusStorage + addOnBonusStorage) / 100) { + continue + } + } + email, err := crypto.Decrypt(encryptedEmail, repo.SecretEncryptionKey, nonce) + if err != nil { + return users, stacktrace.Propagate(err, "") + } + user.Email = email + users = append(users, user) + } + return users, nil +} + func (repo *UserRepository) GetUsersWhoUpgradedNDaysAgo(days int) ([]ente.User, error) { rows, err := repo.DB.Query(` SELECT u.user_id, u.encrypted_email, u.email_decryption_nonce, u.email_hash, u.creation_time