Update: 2026-07-30 02:27:45

This commit is contained in:
Hamza-Ayed
2026-07-30 02:27:45 +03:00
parent 5f62455113
commit ca4a7c2e70
56 changed files with 3391 additions and 709 deletions
+48 -6
View File
@@ -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.
*/