Compare commits

...

36 Commits

Author SHA1 Message Date
Neeraj Gupta
c93f9adb02 rename 2025-05-01 00:22:24 +05:30
Neeraj Gupta
aa3d852280 Avoid spinning go routines for individual emails 2025-04-29 12:14:49 +05:30
Neeraj Gupta
6b92bace0c Merge branch 'main' into retention-emails 2025-04-29 11:30:50 +05:30
Neeraj Gupta
70c857641c Fix bug in GetUsersWithExceedingStorages 2025-04-25 18:03:52 +05:30
Neeraj Gupta
d5b137ea82 clean up 2025-04-25 17:33:36 +05:30
Neeraj Gupta
8b02edb19f Fix query & add doc 2025-04-25 17:26:59 +05:30
Neeraj Gupta
daf1d632e7 Fix typo & minor refactor 2025-04-25 17:19:00 +05:30
Neeraj Gupta
0e435b6bcf simplify family nudge email 2025-04-25 17:13:23 +05:30
mngshm
a2643f2cd0 turn off the cron for SendFamilyNudge temporarily 2025-04-25 17:04:39 +05:30
mngshm
abe0ef0a03 fix query, add filter for deleted users, switch temporarily to u.creation_time 2025-04-25 16:31:23 +05:30
mngshm
0f24ba01f5 refactor: dedup code 2025-04-25 15:10:05 +05:30
mngshm
5d5e418676 remove Dependency injection and functionality for DeleteLastNotificationTime 2025-04-24 15:38:39 +05:30
mngshm
ccfd7abf83 remove batching 2025-04-24 15:27:58 +05:30
mngshm
f609cef79e modify code to pass timeDuration as parameter to get list of users
previously all users without families were fetched and later 30 days check was being done.
With this modification, in the future custom duration can be passed and depending on that one could get the list of users. The compiler decides if more time has passed dependeing on what is the underlying nanoseconds value of time.Time value. Older dates are less in size compared to Newer Dates. Hence, s.created_at >=  is an appropriate calculation
2025-04-23 17:41:54 +05:30
mngshm
ab5c02d792 delete last notification time from table after successful execution of ReplaceSubscription 2025-04-22 15:45:37 +05:30
mngshm
20a26eac3b Delete entry from table whenever there is a change in subscription 2025-04-22 15:19:37 +05:30
mngshm
e7b5815039 minor fix 2025-04-21 15:47:02 +05:30
mngshm
8e313840fd remove unnecessary log statement 2025-04-21 11:05:39 +05:30
mngshm
df8ca468db turn off OnFirstFIleUpload email trigger 2025-04-19 16:15:39 +05:30
mngshm
4623e05eb5 update subject, notification ID's 2025-04-18 19:42:40 +05:30
mngshm
edf6baef6e 90% storage exceeded email template 2025-04-18 19:30:25 +05:30
mngshm
611d2684c4 Family nudge email template 2025-04-18 18:49:14 +05:30
mngshm
8bab350624 Comment 2025-04-03 19:26:08 +05:30
mngshm
6474ff25a7 Update Comment 2025-03-28 17:39:01 +05:30
mngshm
7e5a2c4377 [server](StorageExceedingAlerts) Minor tweaks in multiple files 2025-03-17 18:57:59 +05:30
mngshm
f27a2c68ec [server](StorageExceedingAlerts) Delete user's record from notification_history on Subscription Update 2025-03-17 18:57:59 +05:30
mngshm
709a3756f0 [server](StorageExceedingAlerts) repo func to delete record from notification_history table 2025-03-17 18:57:59 +05:30
mngshm
e1a0c1c847 [server](StorageExceedingAlerts) Better struct name 2025-03-17 18:57:59 +05:30
mngshm
763217c6df [server](ServerExceedingAlerts) fix improper SetlastNotificationTimeToNow calls 2025-03-17 18:57:59 +05:30
mngshm
66d9c100ca [server](StorageExceedingAlerts) handle bonus storage separately 2025-03-17 18:57:59 +05:30
mngshm
ba2ae29e3a [server](StorageExceedingAlerts) struct to loop through slices of set of users depending on storage consumed 2025-03-17 18:57:59 +05:30
mngshm
a72694116a [server](StorageExceedingAlerts) refactor/modify existing method to return storage exceeded/exceeding users on the param basis 2025-03-17 18:57:59 +05:30
mngshm
3fc24d139b [server](SendFamilyNudge) record email nudge time in notification_history table 2025-03-17 18:57:49 +05:30
mngshm
50ab944579 [server](SendFamilyNudge) controller method to send emails to user without families after 30 days of sub 2025-03-17 18:57:30 +05:30
mngshm
2b28661f89 [server](SendFamilyNudge) Modify repo func to get subscribed users without families 2025-03-17 18:57:14 +05:30
mngshm
cfa02f631c [server](SendFamilyNudge) method to get list of all subscribed users 2025-03-17 18:56:55 +05:30
9 changed files with 468 additions and 45 deletions

View File

@@ -5,7 +5,6 @@ import (
"database/sql"
b64 "encoding/base64"
"fmt"
"github.com/ente-io/museum/pkg/controller/collections"
"net/http"
"os"
"os/signal"
@@ -15,6 +14,8 @@ import (
"syscall"
"time"
"github.com/ente-io/museum/pkg/controller/collections"
"github.com/ente-io/museum/ente/base"
"github.com/ente-io/museum/pkg/controller/emergency"
"github.com/ente-io/museum/pkg/controller/file_copy"
@@ -983,9 +984,8 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR
_ = castDb.DeleteUnclaimedCodes(context.Background(), timeUtil.MicrosecondsBeforeMinutes(60))
dataCleanupCtrl.DeleteDataCron()
})
schedule(c, "@every 24h", func() {
emailNotificationCtrl.SendStorageLimitExceededMails()
emailNotificationCtrl.SendStorageAlerts()
})
schedule(c, "@every 1m", func() {
@@ -996,6 +996,10 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR
pushController.ClearExpiredTokens()
})
// schedule(c, "@every 3s", func() {
// emailNotificationCtrl.SendFamilyNudgeEmail()
// })
scheduleAndRun(c, "@every 60m", func() {
emergencyCtrl.SendRecoveryReminder()
kexCtrl.DeleteOldKeys()

View File

@@ -0,0 +1,144 @@
<!DOCTYPE html>
<html>
<meta content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1,
minimum-scale=1" />
<style>
body {
background-color: #f0f1f3;
font-family: "Helvetica Neue", "Segoe UI", Helvetica, sans-serif;
font-size: 16px;
line-height: 27px;
margin: 0;
color: #444;
}
pre {
background: #f4f4f4f4;
padding: 2px;
}
table {
width: 100%;
border: 1px solid #ddd;
}
table td {
border-color: #ddd;
padding: 5px;
}
.wrap {
background-color: #fff;
padding: 30px;
max-width: 525px;
margin: 0 auto;
border-radius: 5px;
}
.button {
background: #0055d4;
border-radius: 3px;
text-decoration: none !important;
color: #fff !important;
font-weight: bold;
padding: 10px 30px;
display: inline-block;
}
.button:hover {
background: #111;
}
.footer {
text-align: center;
font-size: 12px;
color: #888;
}
.footer a {
color: #888;
margin-right: 5px;
}
.gutter {
padding: 30px;
}
img {
max-width: 100%;
height: auto;
}
a {
color: #0055d4;
}
a:hover {
color: #111;
}
@media screen and (max-width: 600px) {
.wrap {
max-width: auto;
}
.gutter {
padding: 10px;
}
}
.footer-icons {
padding: 4px !important;
width: 24px !important;
}
</style>
<body>
<div class="gutter" style="padding: 4px">&nbsp;</div>
<div class="wrap" style=" background-color: rgb(255, 255, 255); padding: 2px
30px 30px 30px; max-width: 525px; margin: 0 auto; border-radius: 5px;
font-size: 16px; ">
<p>
<p>
You have used 90% 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%.
</p>
<p>
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.
</p>
<p>
For any help, please reach out to support@ente.io or reply to this email.
</p>
</p>
</div>
<br />
<div class="footer" style="text-align: center; font-size: 12px; color:
rgb(136, 136, 136)" >
<div>
<a href="https://ente.io" target="_blank" ><img
src="https://email-assets.ente.io/ente-green.png" style="width: 100px;
padding: 24px" title="Ente" alt="Ente" /></a>
</div>
<div>
<a href="https://fosstodon.org/@ente" target="_blank" ><img
src="https://email-assets.ente.io/mastodon-icon.png"
class="footer-icons" style="width: 24px; padding: 4px" title="Mastodon"
alt="Mastodon" /></a>
<a href="https://twitter.com/enteio" target="_blank" ><img
src="https://email-assets.ente.io/twitter-icon.png" class="footer-icons"
style="width: 24px; padding: 4px" title="Twitter" alt="Twitter" /></a>
<a href="https://discord.ente.io" target="_blank" ><img
src="https://email-assets.ente.io/discord-icon.png" class="footer-icons"
style="width: 24px; padding: 4px" title="Discord" alt="Discord" /></a>
<a href="https://github.com/ente-io" target="_blank" ><img
src="https://email-assets.ente.io/github-icon.png" class="footer-icons"
style="width: 24px; padding: 4px" title="GitHub" alt="GitHub" /></a>
</div>
<p>
Ente Technologies, Inc.
<br /> 1111B S Governors Ave 6032 Dover, DE 19904
</p>
<br />
</div>
</body>
</html>

View File

@@ -0,0 +1,139 @@
<!DOCTYPE html>
<html>
<meta content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1,
minimum-scale=1" />
<style>
body {
background-color: #f0f1f3;
font-family: "Helvetica Neue", "Segoe UI", Helvetica, sans-serif;
font-size: 16px;
line-height: 27px;
margin: 0;
color: #444;
}
pre {
background: #f4f4f4f4;
padding: 2px;
}
table {
width: 100%;
border: 1px solid #ddd;
}
table td {
border-color: #ddd;
padding: 5px;
}
.wrap {
background-color: #fff;
padding: 30px;
max-width: 525px;
margin: 0 auto;
border-radius: 5px;
}
.button {
background: #0055d4;
border-radius: 3px;
text-decoration: none !important;
color: #fff !important;
font-weight: bold;
padding: 10px 30px;
display: inline-block;
}
.button:hover {
background: #111;
}
.footer {
text-align: center;
font-size: 12px;
color: #888;
}
.footer a {
color: #888;
margin-right: 5px;
}
.gutter {
padding: 30px;
}
img {
max-width: 100%;
height: auto;
}
a {
color: #0055d4;
}
a:hover {
color: #111;
}
@media screen and (max-width: 600px) {
.wrap {
max-width: auto;
}
.gutter {
padding: 10px;
}
}
.footer-icons {
padding: 4px !important;
width: 24px !important;
}
</style>
<body>
<div class="gutter" style="padding: 4px">&nbsp;</div>
<div class="wrap" style=" background-color: rgb(255, 255, 255); padding: 2px
30px 30px 30px; max-width: 525px; margin: 0 auto; border-radius: 5px;
font-size: 16px; " >
<p>
Your subscription includes our family plan where you can share your subscription with upto 5 additional members at no extra cost. <br>
<br>
You can add family members to your account by going to Settings -> Manage Subscriptions -> Manage Family. For more information, you can also visit
<a href="https://help.ente.io/photos/features/family-plans">our help page</a> or reach out to support@ente.io
</p>
</div>
<br />
<div class="footer" style="text-align: center; font-size: 12px; color:
rgb(136, 136, 136)" >
<div>
<a href="https://ente.io" target="_blank" ><img
src="https://email-assets.ente.io/ente-green.png" style="width: 100px;
padding: 24px" title="Ente" alt="Ente" /></a>
</div>
<div>
<a href="https://fosstodon.org/@ente" target="_blank" ><img
src="https://email-assets.ente.io/mastodon-icon.png"
class="footer-icons" style="width: 24px; padding: 4px" title="Mastodon"
alt="Mastodon" /></a>
<a href="https://twitter.com/enteio" target="_blank" ><img
src="https://email-assets.ente.io/twitter-icon.png" class="footer-icons"
style="width: 24px; padding: 4px" title="Twitter" alt="Twitter" /></a>
<a href="https://discord.ente.io" target="_blank" ><img
src="https://email-assets.ente.io/discord-icon.png" class="footer-icons"
style="width: 24px; padding: 4px" title="Discord" alt="Discord" /></a>
<a href="https://github.com/ente-io" target="_blank" ><img
src="https://email-assets.ente.io/github-icon.png" class="footer-icons"
style="width: 24px; padding: 4px" title="GitHub" alt="GitHub" /></a>
</div>
<p>
Ente Technologies, Inc.
<br /> 1111B S Governors Ave 6032 Dover, DE 19904
</p>
<br />
</div>
</body>
</html>

View File

@@ -5,9 +5,10 @@ import (
"database/sql"
"errors"
"fmt"
"github.com/ente-io/museum/pkg/controller/commonbilling"
"strconv"
"github.com/ente-io/museum/pkg/controller/commonbilling"
"github.com/ente-io/museum/pkg/repo/storagebonus"
"github.com/ente-io/museum/pkg/controller/discord"
@@ -292,6 +293,7 @@ func (c *BillingController) VerifySubscription(
if err != nil {
return ente.Subscription{}, stacktrace.Propagate(err, "")
}
log.Info("Replaced subscription")
newSubscription.ID = currentSubscription.ID
if paymentProvider == ente.PlayStore &&

View File

@@ -2,15 +2,15 @@ package email
import (
"fmt"
"strconv"
"github.com/avct/uasurfer"
"github.com/ente-io/museum/ente"
"github.com/ente-io/museum/pkg/controller/lock"
"github.com/ente-io/museum/pkg/repo"
"github.com/ente-io/museum/pkg/utils/email"
"github.com/ente-io/museum/pkg/utils/time"
"github.com/ente-io/stacktrace"
log "github.com/sirupsen/logrus"
"strconv"
)
const (
@@ -21,6 +21,7 @@ const (
StorageLimitExceededMailLock = "storage_limit_exceeded_mail_lock"
StorageLimitExceededTemplateID = "storage_limit_exceeded"
StorageLimitExceededTemplate = "storage_limit_exceeded.html"
StorageLimitExceededSubject = "[Alert] You have exceeded your storage limit"
FilesCollectedTemplate = "files_collected.html"
FilesCollectedTemplateID = "files_collected"
@@ -33,12 +34,19 @@ const (
SubscriptionCancelledTemplate = "subscription_cancelled.html"
FilesCollectedMuteDurationInMinutes = 10
StorageLimitExceededSubject = "[Alert] You have exceeded your storage limit"
ReferralSuccessfulTemplate = "successful_referral.html"
ReferralSuccessfulSubject = "You've earned 10 GB on Ente! 🎁"
ReferralSuccessfulTemplate = "successful_referral.html"
ReferralSuccessfulSubject = "You've earned 10 GB on Ente! 🎁"
StorageLimitExceedingID = "90_percent_consumed"
StorageLimitExceedingTemplate = "90_percent_storage_consumed.html"
StorageLimitExceedingSubject = "Your Ente storage is at 90% capacity"
LoginSuccessSubject = "New login to your Ente account"
LoginSuccessTemplate = "on_login.html"
FamilyNudgeEmailTemplate = "nudge_for_family.html"
FamilyNudgeSubject = "Share your Ente Subscription with your Family!"
FamilyNudgeTemplateID = "family_nudge"
)
type EmailNotificationController struct {
@@ -60,7 +68,7 @@ func (c *EmailNotificationController) OnFirstFileUpload(userID int64, userAgent
}
err = email.SendTemplatedEmail([]string{user.Email}, "team@ente.io", "team@ente.io", FirstUploadEmailSubject, template, nil, nil)
if err != nil {
log.Error("Error sending first upload email ", err)
log.Error("Error sending first upload email", err)
}
}
@@ -158,7 +166,7 @@ func (c *EmailNotificationController) OnSubscriptionCancelled(userID int64) {
}
}
func (c *EmailNotificationController) SendStorageLimitExceededMails() {
func (c *EmailNotificationController) SendStorageAlerts() {
if c.isSendingStorageLimitExceededMails {
log.Info("Skipping sending storage limit exceeded mails as another instance is still running")
return
@@ -171,33 +179,92 @@ func (c *EmailNotificationController) SendStorageLimitExceededMails() {
return
}
defer c.LockController.ReleaseLock(StorageLimitExceededMailLock)
users, err := c.UserRepo.GetUsersWithIndividualPlanWhoHaveExceededStorageQuota()
if err != nil {
log.Error("Error while fetching user list", err)
return
// storageAlertGroups struct gets the list of both the users who have consumed
// 90% storage and 100% of their subcriptions. Then, it ranges through
// the slices of the both the users and inside this for loop, users from
// both the slices are separately looped. This is done to avoid
// duplication of a lot of code if both the users were ranged inside a loop
// separately.
storageAlertGroups := []struct {
getListofSubscribers func() ([]ente.User, error)
template string
subject string
notifID string
}{
{
getListofSubscribers: func() ([]ente.User, error) {
return c.UserRepo.GetUsersWithExceedingStorage(90)
},
template: StorageLimitExceedingTemplate,
subject: StorageLimitExceedingSubject,
notifID: StorageLimitExceedingID,
},
{
getListofSubscribers: func() ([]ente.User, error) {
return c.UserRepo.GetUsersWithExceedingStorage(100)
},
template: StorageLimitExceededTemplate,
subject: StorageLimitExceededSubject,
notifID: StorageLimitExceededTemplateID,
},
}
for _, u := range users {
lastNotificationTime, err := c.NotificationHistoryRepo.GetLastNotificationTime(u.ID, StorageLimitExceededTemplateID)
logger := log.WithFields(log.Fields{
"user_id": u.ID,
})
if err != nil {
logger.Error("Could not fetch last notification time", err)
for _, alertGroup := range storageAlertGroups {
users, usersErr := alertGroup.getListofSubscribers()
if usersErr != nil {
log.WithError(usersErr).Error("Failed to get list of users")
continue
}
if lastNotificationTime > 0 {
continue
for _, u := range users {
lastNotificationTime, err := c.NotificationHistoryRepo.GetLastNotificationTime(u.ID, alertGroup.notifID)
logger := log.WithFields(log.Fields{
"user_id": u.ID,
"group_id": alertGroup.notifID,
})
if err != nil {
logger.Error("Could not fetch last notification time", err)
continue
}
if lastNotificationTime == 0 {
logger.Info("Alerting about storage limit exceeded")
err = email.SendTemplatedEmail([]string{u.Email}, "team@ente.io", "team@ente.io", alertGroup.subject, alertGroup.template, nil, nil)
if err != nil {
logger.Info("Error notifying", err)
continue
}
c.NotificationHistoryRepo.SetLastNotificationTimeToNow(u.ID, alertGroup.notifID)
}
}
logger.Info("Alerting about storage limit exceeded")
err = email.SendTemplatedEmail([]string{u.Email}, "team@ente.io", "team@ente.io", StorageLimitExceededSubject, StorageLimitExceededTemplate, nil, nil)
if err != nil {
logger.Info("Error notifying", err)
continue
}
c.NotificationHistoryRepo.SetLastNotificationTimeToNow(u.ID, StorageLimitExceededTemplateID)
}
}
func (c *EmailNotificationController) setStorageLimitExceededMailerJobStatus(isSending bool) {
c.isSendingStorageLimitExceededMails = isSending
}
func (c *EmailNotificationController) SendFamilyNudgeEmail() error {
subscribedUsers, subUsersErr := c.UserRepo.GetSubscribedUsersWithoutFamily(30)
if subUsersErr != nil {
return stacktrace.Propagate(subUsersErr, "Failed to get subscribers")
}
log.Infof("Found %d subscribers to nudge for family", len(subscribedUsers))
for _, user := range subscribedUsers {
lastNudgeSent, lastNudgeErr := c.NotificationHistoryRepo.GetLastNotificationTime(user.ID, FamilyNudgeTemplateID)
if lastNudgeErr != nil {
log.WithError(lastNudgeErr).Error("Failed to set Notification History")
continue
}
if lastNudgeSent == 0 {
err := email.SendTemplatedEmail([]string{user.Email}, "team@ente.io", "team@ente.io", FamilyNudgeSubject, FamilyNudgeEmailTemplate, nil, nil)
if err != nil {
log.Error("Failed to send family nudge email: ", err)
continue
}
err = c.NotificationHistoryRepo.SetLastNotificationTimeToNow(user.ID, FamilyNudgeTemplateID)
if err != nil {
log.Error("Failed to set Notification History")
}
}
}
return nil
}

View File

@@ -46,7 +46,18 @@ type StripeController struct {
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,
discordController *discord.DiscordController,
emailNotificationController *emailCtrl.EmailNotificationController,
offerController *offer.OfferController,
commonBillCtrl *commonbilling.Controller,
) *StripeController {
return &StripeController{
StripeClients: stripeClients,
BillingRepo: billingRepo,
@@ -249,7 +260,7 @@ func (c *StripeController) handleCheckoutSessionCompleted(event stripe.Event, co
}()
}
if err != nil {
return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
return ente.StripeEventLog{}, stacktrace.Propagate(err, "Failed to change subscription")
}
return ente.StripeEventLog{UserID: userID, StripeSubscription: stripeSubscription, Event: event}, nil
} else {

View File

@@ -155,7 +155,7 @@ func (repo *BillingRepository) LogStripePush(eventLog ente.StripeEventLog) error
return stacktrace.Propagate(err, "")
}
// LogStripePush logs a subscription modification by an admin
// LogAdminTriggeredSubscriptionUpdate logs a subscription modification by an admin
func (repo *BillingRepository) LogAdminTriggeredSubscriptionUpdate(r ente.UpdateSubscriptionRequest) error {
requestJSON, _ := json.Marshal(r)
_, err := repo.DB.Exec(`INSERT INTO subscription_logs(user_id, payment_provider, notification, verification_response) VALUES($1, $2, $3, '{}'::json)`,

View File

@@ -29,3 +29,13 @@ func (repo *NotificationHistoryRepository) SetLastNotificationTimeToNow(userID i
userID, templateID, time.Microseconds())
return stacktrace.Propagate(err, "")
}
// DeleteLastNotificationTime deletes the last notification time for a user and template.
// This will be used in the upcoming PR for resetting storage alerts notifications.
func (repo *NotificationHistoryRepository) DeleteLastNotificationTime(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, "")
}
return nil
}

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"strings"
t "time"
"github.com/ente-io/museum/pkg/repo/passkey"
storageBonusRepo "github.com/ente-io/museum/pkg/repo/storagebonus"
@@ -128,6 +129,39 @@ func (repo *UserRepository) GetAll(sinceTime int64, tillTime int64) ([]ente.User
return users, nil
}
// GetSubscribedUsersWithoutFamily returns notification candidates who are on paid plan
// but not part of any family plan.
func (repo *UserRepository) GetSubscribedUsersWithoutFamily(accountAgeInDays int64) ([]ente.User, error) {
rows, err := repo.DB.Query(`SELECT u.user_id, u.encrypted_email, u.email_decryption_nonce, u.email_hash, s.created_at
FROM subscriptions s
INNER JOIN users u ON s.user_id = u.user_id
WHERE u.creation_time <= $1 AND u.family_admin_id IS NULL
AND s.product_id != 'free'
AND u.encrypted_email IS NOT NULL`, time.MicrosecondBeforeDays(int(accountAgeInDays)))
if err != nil {
return nil, stacktrace.Propagate(err, "")
}
defer rows.Close()
subscribedUsers := make([]ente.User, 0)
for rows.Next() {
var user ente.User
var encryptedEmail, nonce []byte
var createdAt t.Time
err := rows.Scan(&user.ID, &encryptedEmail, &nonce, &user.Hash, &createdAt)
if err != nil {
return subscribedUsers, stacktrace.Propagate(err, "user scan failed")
}
email, err := crypto.Decrypt(encryptedEmail, repo.SecretEncryptionKey, nonce)
if err != nil {
return nil, stacktrace.Propagate(err, "")
}
user.Email = email
subscribedUsers = append(subscribedUsers, user)
}
return subscribedUsers, nil
}
// GetUserUsageWithSubData will return current storage usage & basic information about subscription for given list
// of users. It's primarily used for fetching storage utilisation for a family/group of users
func (repo *UserRepository) GetUserUsageWithSubData(ctx context.Context, userIds []int64) ([]ente.UserUsageWithSubData, error) {
@@ -280,17 +314,17 @@ func (repo *UserRepository) GetPublicKey(userID int64) (string, error) {
return publicKey, stacktrace.Propagate(err, "")
}
// GetUsersWithIndividualPlanWhoHaveExceededStorageQuota returns list of users who have consumed their storage quota
// and they are not part of any family plan
func (repo *UserRepository) GetUsersWithIndividualPlanWhoHaveExceededStorageQuota() ([]ente.User, error) {
// GetUsersWithExceedingStorage returns list of users who have consumed 90% or 100% of their total storage quota
// depending on the percentageThreshold (90% or 100%) and they are not part of any family plan
func (repo *UserRepository) GetUsersWithExceedingStorage(percentageThreshold int64) ([]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 usage.storage_consumed > subscriptions.storage AND users.encrypted_email IS NOT NULL AND users.family_admin_id IS NULL;
`)
ON users.user_id = subscriptions.user_id AND usage.storage_consumed >= (subscriptions.storage * ($1 / 100.0) AND users.encrypted_email IS NOT NULL AND users.family_admin_id is NULL
`, percentageThreshold)
if err != nil {
return nil, stacktrace.Propagate(err, "")
}
@@ -304,7 +338,7 @@ func (repo *UserRepository) GetUsersWithIndividualPlanWhoHaveExceededStorageQuot
var user ente.User
var encryptedEmail, nonce []byte
var storageConsumed, subStorage int64
err := rows.Scan(&user.ID, &encryptedEmail, &nonce, &user.Hash, &storageConsumed, &subStorage)
err = rows.Scan(&user.ID, &encryptedEmail, &nonce, &user.Hash, &storageConsumed, &subStorage)
if err != nil {
return users, stacktrace.Propagate(err, "")
}
@@ -312,15 +346,26 @@ func (repo *UserRepository) GetUsersWithIndividualPlanWhoHaveExceededStorageQuot
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]
refBonusStorage := int64(0)
addOnBonusStorage := int64(0)
if bonus, ok := refBonus[user.ID]; ok {
refBonusStorage = bonus
addOnBonusStorage = addOnBonus[user.ID]
// cap usable ref bonus to the subscription storage + addOnBonus
if refBonusStorage > (subStorage + addOnBonusStorage) {
refBonusStorage = subStorage + addOnBonusStorage
}
if (storageConsumed) <= (subStorage + refBonusStorage + addOnBonusStorage) {
continue
}
}
totalStorage := refBonusStorage + subStorage + addOnBonusStorage
usagePercentage := (storageConsumed * 100.0) / totalStorage
if percentageThreshold >= 100 && usagePercentage < 100.0 {
continue
}
// when required percentage is below 100, skip email if the usage is below
// percentageThreshold or usage above 100% (which means user has used all the storage)
if percentageThreshold < 100 && (usagePercentage < percentageThreshold || usagePercentage >= 100.0) {
continue
}
email, err := crypto.Decrypt(encryptedEmail, repo.SecretEncryptionKey, nonce)
if err != nil {
@@ -328,6 +373,7 @@ func (repo *UserRepository) GetUsersWithIndividualPlanWhoHaveExceededStorageQuot
}
user.Email = email
users = append(users, user)
}
return users, nil
}