Update: 2026-07-30 02:27:45
This commit is contained in:
@@ -21,14 +21,16 @@ final class QuotaMiddleware
|
||||
*
|
||||
* @return array The current subscription data (for UI display)
|
||||
*/
|
||||
public static function checkInvoiceQuota(string $tenantId): array
|
||||
public static function checkInvoiceQuota(string $tenantId, int $needed = 1): array
|
||||
{
|
||||
$cacheKey = "quota_sub_{$tenantId}";
|
||||
$sub = Cache::get($cacheKey);
|
||||
|
||||
if ($sub === false || $sub === null) {
|
||||
$db = Database::getInstance();
|
||||
// $db is needed by the auto-reset branch below regardless of whether the
|
||||
// subscription came from the cache, so resolve it unconditionally.
|
||||
$db = Database::getInstance();
|
||||
|
||||
if ($sub === false || $sub === null) {
|
||||
// Fetch subscription with plan info
|
||||
$stmt = $db->prepare("
|
||||
SELECT s.*, sp.name_ar as plan_name, sp.ai_features, sp.jofotara_enabled, sp.price_monthly_jod, sp.price_annual_jod
|
||||
@@ -78,18 +80,30 @@ final class QuotaMiddleware
|
||||
$sub['current_period_start'] = $newStart;
|
||||
$sub['current_period_end'] = $newEnd;
|
||||
|
||||
// The cached copy still holds the pre-reset counters.
|
||||
Cache::delete($cacheKey);
|
||||
|
||||
error_log("QuotaMiddleware: Auto-reset annual counter for tenant {$tenantId}");
|
||||
}
|
||||
|
||||
// Check invoice quota
|
||||
// Check invoice quota. $needed lets callers reserve a whole batch at once
|
||||
// instead of only asking "is there room for one more?".
|
||||
$used = (int)$sub['invoices_used_this_month'];
|
||||
$limit = (int)$sub['max_invoices_per_month']; // Keeping the DB column name the same for compatibility
|
||||
$needed = max(1, $needed);
|
||||
|
||||
if ($used >= $limit) {
|
||||
json_error('لقد وصلت للحد الأقصى من الفواتير المسموحة في باقتك الحالية (' . $limit . ' فاتورة). يرجى ترقية باقتك للاستمرار.', 429, [
|
||||
if (($used + $needed) > $limit) {
|
||||
$remaining = max(0, $limit - $used);
|
||||
$message = $remaining === 0
|
||||
? 'لقد وصلت للحد الأقصى من الفواتير المسموحة في باقتك الحالية (' . $limit . ' فاتورة). يرجى ترقية باقتك للاستمرار.'
|
||||
: 'رصيدك المتبقي ' . $remaining . ' فاتورة فقط، وقد طلبت ' . $needed . '. يرجى تقليل عدد الفواتير أو ترقية باقتك.';
|
||||
|
||||
json_error($message, 429, [
|
||||
'quota_type' => 'invoices',
|
||||
'used' => $used,
|
||||
'limit' => $limit,
|
||||
'requested' => $needed,
|
||||
'remaining' => $remaining,
|
||||
'plan' => $sub['plan_id'] ?? 'free',
|
||||
'plan_name' => $sub['plan_name'] ?? 'مجانية',
|
||||
'period_end' => $sub['current_period_end'],
|
||||
@@ -99,6 +113,34 @@ final class QuotaMiddleware
|
||||
return $sub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-fatal variant of checkInvoiceQuota() for background workers.
|
||||
*
|
||||
* Workers must never emit an HTTP response, so this returns a boolean
|
||||
* instead of calling json_error().
|
||||
*/
|
||||
public static function hasInvoiceQuota(string $tenantId, int $needed = 1): bool
|
||||
{
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$stmt = $db->prepare("
|
||||
SELECT invoices_used_this_month, max_invoices_per_month, status
|
||||
FROM subscriptions WHERE tenant_id = ?
|
||||
");
|
||||
$stmt->execute([$tenantId]);
|
||||
$sub = $stmt->fetch();
|
||||
|
||||
if (!$sub) return false;
|
||||
if (in_array($sub['status'], ['cancelled', 'past_due'], true)) return false;
|
||||
|
||||
return ((int)$sub['invoices_used_this_month'] + max(1, $needed))
|
||||
<= (int)$sub['max_invoices_per_month'];
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[QuotaMiddleware] hasInvoiceQuota failed: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the monthly invoice counter after a successful upload.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user