85 lines
3.3 KiB
PHP
85 lines
3.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\MailFlow;
|
|
|
|
use App\Models\MailMessage;
|
|
use App\Models\MailQuotaBucket;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class QuotaService
|
|
{
|
|
public function __construct(private ResendClient $client) {}
|
|
|
|
public function reserve(MailMessage $message): bool
|
|
{
|
|
if ($message->reservation) {
|
|
return true;
|
|
}
|
|
// One account on this website. Changing the API key does not grant extra quota.
|
|
$usage = Cache::remember('mailflow:resend:usage', 15, fn () => $this->client->usage());
|
|
foreach (['daily', 'monthly'] as $period) {
|
|
if (!isset($usage[$period]['used'], $usage[$period]['resets_at']) || !is_numeric($usage[$period]['used'])) {
|
|
throw new \RuntimeException('resend_usage_invalid');
|
|
}
|
|
}
|
|
return DB::transaction(function () use ($usage, $message) {
|
|
$buckets = [];
|
|
foreach (['daily', 'monthly'] as $period) {
|
|
$reset = Carbon::parse($usage[$period]['resets_at'])->setTimezone(config('app.timezone'));
|
|
if ($reset->isPast()) {
|
|
Cache::forget('mailflow:resend:usage');
|
|
throw new \RuntimeException('resend_usage_stale');
|
|
}
|
|
$limit = min((int) Settings::get($period . '_limit'), (int) ($usage[$period]['limit'] ?? Settings::get($period . '_limit')));
|
|
$key = $period . ':' . $reset->timestamp;
|
|
DB::table('v2_mail_quota_buckets')->insertOrIgnore([
|
|
'period_key' => $key, 'period' => $period, 'resets_at' => $reset,
|
|
'limit' => $limit, 'used' => 0, 'reserved' => 0,
|
|
'created_at' => now(), 'updated_at' => now(),
|
|
]);
|
|
$bucket = MailQuotaBucket::where('period_key', $key)->lockForUpdate()->firstOrFail();
|
|
$bucket->used = max($bucket->used, (int) $usage[$period]['used']);
|
|
$bucket->limit = $limit;
|
|
$bucket->synced_at = now();
|
|
$bucket->save();
|
|
$buckets[] = $bucket;
|
|
}
|
|
foreach ($buckets as $bucket) {
|
|
if ($bucket->used + $bucket->reserved >= $bucket->limit) {
|
|
return false;
|
|
}
|
|
}
|
|
foreach ($buckets as $bucket) {
|
|
$bucket->increment('reserved');
|
|
}
|
|
$message->reservation = array_map(fn ($bucket) => $bucket->id, $buckets);
|
|
$message->save();
|
|
return true;
|
|
}, 3);
|
|
}
|
|
|
|
// Call inside the same transaction that records the message result.
|
|
public function settle(MailMessage $message, bool $accepted): void
|
|
{
|
|
foreach ($message->reservation ?? [] as $id) {
|
|
$bucket = MailQuotaBucket::whereKey($id)->lockForUpdate()->firstOrFail();
|
|
$bucket->reserved = max(0, $bucket->reserved - 1);
|
|
if ($accepted) {
|
|
$bucket->used++;
|
|
}
|
|
$bucket->save();
|
|
}
|
|
$message->reservation = null;
|
|
$message->save();
|
|
}
|
|
|
|
public function exhausted(string $period): void
|
|
{
|
|
Cache::forget('mailflow:resend:usage');
|
|
MailQuotaBucket::where('period', $period)->where('resets_at', '>', now())
|
|
->update(['used' => DB::raw('`limit`')]);
|
|
}
|
|
}
|