diff --git a/src/app/Console/Commands/Data/ImportCommand.php b/src/app/Console/Commands/Data/ImportCommand.php index 3c24ee9f..76e6ed80 100644 --- a/src/app/Console/Commands/Data/ImportCommand.php +++ b/src/app/Console/Commands/Data/ImportCommand.php @@ -1,45 +1,45 @@ output = $this->output; $execution->handle(); } return 0; } } diff --git a/src/app/Console/Commands/Data/Stats/CollectorCommand.php b/src/app/Console/Commands/Data/Stats/CollectorCommand.php index d65ddca1..c269e768 100644 --- a/src/app/Console/Commands/Data/Stats/CollectorCommand.php +++ b/src/app/Console/Commands/Data/Stats/CollectorCommand.php @@ -1,83 +1,76 @@ collectPayersCount(); } /** * Collect current payers count */ protected function collectPayersCount(): void { - $tenant_id = \config('app.tenant_id'); - // A subquery to get the all wallets with a successful payment $payments = DB::table('payments') ->selectRaw('distinct wallet_id') ->where('status', Payment::STATUS_PAID); // A subquery to get users' wallets (by entitlement) - one record per user $wallets = DB::table('entitlements') ->selectRaw("min(wallet_id) as id, entitleable_id as user_id") ->where('entitleable_type', User::class) ->groupBy('entitleable_id'); // Count all non-degraded and non-deleted users with any successful payment - $count = DB::table('users') + $counts = DB::table('users') + ->selectRaw('count(*) as total, users.tenant_id') ->joinSub($wallets, 'wallets', function ($join) { $join->on('users.id', '=', 'wallets.user_id'); }) ->joinSub($payments, 'payments', function ($join) { $join->on('wallets.id', '=', 'payments.wallet_id'); }) ->whereNull('users.deleted_at') ->whereNot('users.status', '&', User::STATUS_DEGRADED) - ->whereNot('users.status', '&', User::STATUS_SUSPENDED); - - if ($tenant_id) { - $count->where('users.tenant_id', $tenant_id); - } else { - $count->whereNull('users.tenant_id'); - } - - $count = $count->count(); - - if ($count) { - DB::table('stats')->insert([ - 'tenant_id' => $tenant_id, + ->whereNot('users.status', '&', User::STATUS_SUSPENDED) + ->groupBy('users.tenant_id') + ->havingRaw('count(*) > 0') + ->get() + ->each(function ($record) { + DB::table('stats')->insert([ + 'tenant_id' => $record->tenant_id, 'type' => StatsController::TYPE_PAYERS, - 'value' => $count, - ]); - } + 'value' => $record->total, + ]); + }); } } diff --git a/src/app/Console/Commands/PasswordRetentionCommand.php b/src/app/Console/Commands/PasswordRetentionCommand.php index 3c2f3ca7..dc5d5740 100644 --- a/src/app/Console/Commands/PasswordRetentionCommand.php +++ b/src/app/Console/Commands/PasswordRetentionCommand.php @@ -1,84 +1,86 @@ join('user_settings', 'users.id', '=', 'user_settings.user_id') - ->withEnvTenantContext('users') ->where('user_settings.key', 'max_password_age') ->cursor(); foreach ($accounts as $account) { // For all users in the account (get the password update date)... $account->users(false) ->addSelect( DB::raw("(select value from user_settings" . " where users.id = user_settings.user_id and user_settings.key = 'password_update'" . ") as password_update") ) ->get() ->each(function ($user) use ($account) { + /** @var User $user */ // Skip incomplete or suspended users if (!$user->isImapReady() || $user->isSuspended()) { return; } // If the password was never updated use the user creation time if (!empty($user->password_update)) { $lastUpdate = new Carbon($user->password_update); } else { $lastUpdate = $user->created_at; } + // @phpstan-ignore-next-line $nextUpdate = $lastUpdate->copy()->addMonthsWithoutOverflow($account->max_age); $diff = Carbon::now()->diffInDays($nextUpdate, false); // The password already expired, do nothing if ($diff <= 0) { return; } if ($warnedOn = $user->getSetting('password_expiration_warning')) { $warnedOn = new Carbon($warnedOn); } // The password expires in 14 days or less if ($diff <= 14) { // Send a warning if it wasn't sent yet or 7 days passed since the last warning. // Which means that we send the email 14 and 7 days before the password expires. if (empty($warnedOn) || $warnedOn->diffInDays(Carbon::now(), false) > 7) { \App\Jobs\Password\RetentionEmailJob::dispatch($user, $nextUpdate->toDateString()); } } }); } } } diff --git a/src/app/Console/Commands/Wallet/ChargeCommand.php b/src/app/Console/Commands/Wallet/ChargeCommand.php index 50e55714..b2ff9964 100644 --- a/src/app/Console/Commands/Wallet/ChargeCommand.php +++ b/src/app/Console/Commands/Wallet/ChargeCommand.php @@ -1,81 +1,80 @@ argument('wallet')) { // Find specified wallet by ID $wallet = $this->getWallet($wallet); if (!$wallet) { $this->error("Wallet not found."); return 1; } if (!$wallet->owner) { $this->error("Wallet's owner is deleted."); return 1; } $wallets = [$wallet]; } else { // Get all wallets, excluding deleted accounts $wallets = \App\Wallet::select('wallets.id') ->join('users', 'users.id', '=', 'wallets.user_id') - ->withEnvTenantContext('users') ->whereNull('users.deleted_at') ->cursor(); } foreach ($wallets as $wallet) { // This is a long-running process. Because another process might have modified // the wallet balance in meantime we have to refresh it. // Note: This is needed despite the use of cursor() above. $wallet->refresh(); // Sanity check after refresh (owner deleted in meantime) if (!$wallet->owner) { continue; } $charge = $wallet->chargeEntitlements(); if ($charge > 0) { $this->info("Charged wallet {$wallet->id} for user {$wallet->owner->email} with {$charge}"); // Top-up the wallet if auto-payment enabled for the wallet \App\Jobs\WalletCharge::dispatch($wallet); } if ($wallet->balance < 0) { // Check the account balance, send notifications, (suspend, delete,) degrade // Also sends reminders to the degraded account owners \App\Jobs\WalletCheck::dispatch($wallet); } } } } diff --git a/src/app/Console/Commands/Wallet/TrialEndCommand.php b/src/app/Console/Commands/Wallet/TrialEndCommand.php index 2e4e363f..9c49d5ea 100644 --- a/src/app/Console/Commands/Wallet/TrialEndCommand.php +++ b/src/app/Console/Commands/Wallet/TrialEndCommand.php @@ -1,70 +1,69 @@ join('users', 'users.id', '=', 'wallets.user_id') - ->withEnvTenantContext('users') // exclude deleted accounts ->whereNull('users.deleted_at') // exclude "inactive" accounts ->where('users.status', '&', \App\User::STATUS_IMAP_READY) // consider only these created 1 to 2 months ago ->where('users.created_at', '>', \now()->subMonthsNoOverflow(2)) ->where('users.created_at', '<=', \now()->subMonthsNoOverflow(1)) // skip wallets with the notification already sent ->whereNotExists(function ($query) { $query->from('wallet_settings') ->where('wallet_settings.key', 'trial_end_notice') ->whereColumn('wallet_settings.wallet_id', 'wallets.id'); }) // skip users that aren't account owners ->whereExists(function ($query) { $query->from('entitlements') ->where('entitlements.entitleable_type', \App\User::class) ->whereColumn('entitlements.entitleable_id', 'wallets.user_id') ->whereColumn('entitlements.wallet_id', 'wallets.id'); }) ->cursor(); foreach ($wallets as $wallet) { // Skip accounts with no trial period, or a period longer than a month $plan = $wallet->plan(); if (!$plan || $plan->free_months != 1) { continue; } // Send the email asynchronously \App\Jobs\TrialEndEmail::dispatch($wallet->owner); // Store the timestamp $wallet->setSetting('trial_end_notice', (string) \now()); } } }