mirror of
https://github.com/PrivateCaptcha/PrivateCaptcha.git
synced 2026-02-11 16:29:00 -06:00
Add maintenance job to delete trial accounts
This commit is contained in:
@@ -342,7 +342,7 @@ func run(ctx context.Context, cfg common.ConfigStore, stderr io.Writer, listener
|
||||
Store: businessDB,
|
||||
Templates: email.Templates(),
|
||||
Sender: sender,
|
||||
ChunkSize: 100,
|
||||
ChunkSize: 50,
|
||||
EmailFrom: cfg.Get(common.EmailFromKey),
|
||||
ReplyToEmail: cfg.Get(common.ReplyToEmailKey),
|
||||
CDNURL: mailer.CDNURL,
|
||||
@@ -351,6 +351,12 @@ func run(ctx context.Context, cfg common.ConfigStore, stderr io.Writer, listener
|
||||
jobs.AddLocked(24*time.Hour, &maintenance.CleanupUserNotificationsJob{
|
||||
Store: businessDB,
|
||||
})
|
||||
jobs.AddLocked(24*time.Hour, &maintenance.CleanupExpiredTrialUsersJob{
|
||||
Age: 30 * 24 * time.Hour,
|
||||
BusinessDB: businessDB,
|
||||
PlanService: planService,
|
||||
ChunkSize: 20,
|
||||
})
|
||||
jobs.Run()
|
||||
|
||||
var localServer *http.Server
|
||||
|
||||
@@ -260,10 +260,10 @@ func (impl *BusinessStoreImpl) SoftDeleteUser(ctx context.Context, userID int32)
|
||||
}
|
||||
|
||||
if err := impl.querier.DeleteUserAPIKeys(ctx, Int(userID)); err != nil {
|
||||
slog.ErrorContext(ctx, "Failed to soft-delete user API keys", "userID", userID, common.ErrAttr(err))
|
||||
slog.ErrorContext(ctx, "Failed to delete user API keys", "userID", userID, common.ErrAttr(err))
|
||||
return err
|
||||
} else {
|
||||
slog.DebugContext(ctx, "Disabled user API keys", "userID", userID)
|
||||
slog.DebugContext(ctx, "Deleted user API keys", "userID", userID)
|
||||
}
|
||||
|
||||
// TODO: Delete user API keys from cache
|
||||
@@ -1755,3 +1755,28 @@ func (s *BusinessStoreImpl) DeletePendingUserNotification(ctx context.Context, u
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BusinessStoreImpl) RetrieveUsersWithExpiredTrials(ctx context.Context, before time.Time, trialStatus string, maxUsers int32) ([]*dbgen.User, error) {
|
||||
if s.querier == nil {
|
||||
return nil, ErrMaintenance
|
||||
}
|
||||
|
||||
users, err := s.querier.GetUsersWithExpiredTrials(ctx, &dbgen.GetUsersWithExpiredTrialsParams{
|
||||
TrialEndsAt: Timestampz(before),
|
||||
Status: trialStatus,
|
||||
Limit: maxUsers,
|
||||
})
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return []*dbgen.User{}, nil
|
||||
}
|
||||
|
||||
slog.ErrorContext(ctx, "Failed to retrieve users with expired trials", "before", before, "status", trialStatus, common.ErrAttr(err))
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slog.DebugContext(ctx, "Fetched users with expired trials", "count", len(users), "before", before, "status", trialStatus)
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ type Querier interface {
|
||||
GetUserBySubscriptionID(ctx context.Context, subscriptionID pgtype.Int4) (*User, error)
|
||||
GetUserOrganizations(ctx context.Context, userID pgtype.Int4) ([]*GetUserOrganizationsRow, error)
|
||||
GetUserPropertiesCount(ctx context.Context, orgOwnerID pgtype.Int4) (int64, error)
|
||||
GetUsersWithExpiredTrials(ctx context.Context, arg *GetUsersWithExpiredTrialsParams) ([]*User, error)
|
||||
GetUsersWithoutSubscription(ctx context.Context, dollar_1 []int32) ([]*User, error)
|
||||
InsertLock(ctx context.Context, arg *InsertLockParams) (*Lock, error)
|
||||
InviteUserToOrg(ctx context.Context, arg *InviteUserToOrgParams) (*OrganizationUser, error)
|
||||
|
||||
@@ -147,6 +147,56 @@ func (q *Queries) GetUserBySubscriptionID(ctx context.Context, subscriptionID pg
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const getUsersWithExpiredTrials = `-- name: GetUsersWithExpiredTrials :many
|
||||
SELECT u.id, u.name, u.email, u.subscription_id, u.created_at, u.updated_at, u.deleted_at
|
||||
FROM backend.users u
|
||||
JOIN backend.subscriptions s ON u.subscription_id = s.id
|
||||
WHERE
|
||||
s.source = 'internal' AND
|
||||
s.trial_ends_at IS NOT NULL AND
|
||||
s.trial_ends_at < $1 AND
|
||||
s.status = $2 AND
|
||||
(s.external_customer_id IS NULL OR s.external_customer_id = '') AND
|
||||
(s.external_subscription_id IS NULL OR s.external_subscription_id = '') AND
|
||||
s.next_billed_at IS NULL AND
|
||||
u.deleted_at IS NULL
|
||||
LIMIT $3
|
||||
`
|
||||
|
||||
type GetUsersWithExpiredTrialsParams struct {
|
||||
TrialEndsAt pgtype.Timestamptz `db:"trial_ends_at" json:"trial_ends_at"`
|
||||
Status string `db:"status" json:"status"`
|
||||
Limit int32 `db:"limit" json:"limit"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUsersWithExpiredTrials(ctx context.Context, arg *GetUsersWithExpiredTrialsParams) ([]*User, error) {
|
||||
rows, err := q.db.Query(ctx, getUsersWithExpiredTrials, arg.TrialEndsAt, arg.Status, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []*User
|
||||
for rows.Next() {
|
||||
var i User
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Email,
|
||||
&i.SubscriptionID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.DeletedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getUsersWithoutSubscription = `-- name: GetUsersWithoutSubscription :many
|
||||
SELECT id, name, email, subscription_id, created_at, updated_at, deleted_at FROM backend.users where id = ANY($1::INT[]) AND (subscription_id IS NULL OR deleted_at IS NOT NULL)
|
||||
`
|
||||
|
||||
@@ -31,3 +31,18 @@ DELETE FROM backend.users WHERE id = ANY($1::INT[]);
|
||||
|
||||
-- name: GetUsersWithoutSubscription :many
|
||||
SELECT * FROM backend.users where id = ANY($1::INT[]) AND (subscription_id IS NULL OR deleted_at IS NOT NULL);
|
||||
|
||||
-- name: GetUsersWithExpiredTrials :many
|
||||
SELECT u.*
|
||||
FROM backend.users u
|
||||
JOIN backend.subscriptions s ON u.subscription_id = s.id
|
||||
WHERE
|
||||
s.source = 'internal' AND
|
||||
s.trial_ends_at IS NOT NULL AND
|
||||
s.trial_ends_at < $1 AND
|
||||
s.status = $2 AND
|
||||
(s.external_customer_id IS NULL OR s.external_customer_id = '') AND
|
||||
(s.external_subscription_id IS NULL OR s.external_subscription_id = '') AND
|
||||
s.next_billed_at IS NULL AND
|
||||
u.deleted_at IS NULL
|
||||
LIMIT $3;
|
||||
|
||||
@@ -2,10 +2,13 @@ package maintenance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/billing"
|
||||
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/common"
|
||||
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/db"
|
||||
"github.com/jpillora/backoff"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -99,3 +102,51 @@ func (j *GarbageCollectDataJob) RunOnce(ctx context.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type CleanupExpiredTrialUsersJob struct {
|
||||
Age time.Duration
|
||||
BusinessDB db.Implementor
|
||||
PlanService billing.PlanService
|
||||
ChunkSize int
|
||||
}
|
||||
|
||||
func (j *CleanupExpiredTrialUsersJob) Interval() time.Duration {
|
||||
return 12 * time.Hour
|
||||
}
|
||||
|
||||
func (j *CleanupExpiredTrialUsersJob) Jitter() time.Duration {
|
||||
return 6 * time.Hour
|
||||
}
|
||||
|
||||
func (CleanupExpiredTrialUsersJob) Name() string {
|
||||
return "cleanup_expired_trial_users_job"
|
||||
}
|
||||
|
||||
func (j *CleanupExpiredTrialUsersJob) RunOnce(ctx context.Context) error {
|
||||
expiredBefore := time.Now().Add(-j.Age)
|
||||
users, err := j.BusinessDB.Impl().RetrieveUsersWithExpiredTrials(ctx, expiredBefore, j.PlanService.TrialStatus(), int32(j.ChunkSize))
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "Failed to retrieve users with expired trials", common.ErrAttr(err))
|
||||
return err
|
||||
}
|
||||
|
||||
b := &backoff.Backoff{
|
||||
Min: 200 * time.Millisecond,
|
||||
Max: 1 * time.Second,
|
||||
Factor: 2,
|
||||
Jitter: true,
|
||||
}
|
||||
|
||||
for i, u := range users {
|
||||
if (i > 0) && (err == nil) {
|
||||
time.Sleep(b.Duration())
|
||||
}
|
||||
|
||||
err = j.BusinessDB.Impl().SoftDeleteUser(ctx, u.ID)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "Failed to soft-delete user", "userID", u.ID, common.ErrAttr(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user