Update: 2026-05-16 01:36:22

This commit is contained in:
Hamza-Ayed
2026-05-16 01:36:22 +03:00
parent 24a9f064a1
commit aceb7d324f
8 changed files with 145 additions and 43 deletions

View File

@@ -31,7 +31,7 @@ final class QuotaMiddleware
// Fetch subscription with plan info // Fetch subscription with plan info
$stmt = $db->prepare(" $stmt = $db->prepare("
SELECT s.*, sp.name_ar as plan_name, sp.ai_features, sp.jofotara_enabled SELECT s.*, sp.name_ar as plan_name, sp.ai_features, sp.jofotara_enabled, sp.price_monthly_jod, sp.price_annual_jod
FROM subscriptions s FROM subscriptions s
LEFT JOIN subscription_plans sp ON s.plan_id = sp.id LEFT JOIN subscription_plans sp ON s.plan_id = sp.id
WHERE s.tenant_id = ? WHERE s.tenant_id = ?
@@ -60,7 +60,9 @@ final class QuotaMiddleware
// Auto-reset period counter if billing period has ended // Auto-reset period counter if billing period has ended
if (!empty($sub['current_period_end']) && strtotime($sub['current_period_end']) < time()) { if (!empty($sub['current_period_end']) && strtotime($sub['current_period_end']) < time()) {
$newStart = date('Y-m-d H:i:s'); $newStart = date('Y-m-d H:i:s');
$newEnd = date('Y-m-d H:i:s', strtotime('+1 year')); // Changed to annual $cycle = $sub['billing_cycle'] ?? 'annual';
$interval = ($cycle === 'monthly') ? '+1 month' : '+1 year';
$newEnd = date('Y-m-d H:i:s', strtotime($interval));
$resetStmt = $db->prepare(" $resetStmt = $db->prepare("
UPDATE subscriptions UPDATE subscriptions

View File

@@ -15,6 +15,8 @@ return [
'max_invoices_month' => 15, 'max_invoices_month' => 15,
'max_users' => 1, 'max_users' => 1,
'price_jod' => 0.00, 'price_jod' => 0.00,
'price_monthly_jod' => 0.00,
'price_annual_jod' => 0.00,
'ai_features' => true, 'ai_features' => true,
'jofotara_enabled' => true, 'jofotara_enabled' => true,
'badge_color' => 'gray', 'badge_color' => 'gray',
@@ -29,43 +31,47 @@ return [
], ],
'basic' => [ 'basic' => [
'id' => 'basic', 'id' => 'basic',
'name_ar' => 'الباقة الأساسية (سنوي)', 'name_ar' => 'الباقة الأساسية',
'name_en' => 'Basic Plan (Annual)', 'name_en' => 'Basic Plan',
'max_companies' => 1, 'max_companies' => 3,
'max_invoices_month' => 12000, 'max_invoices_month' => 500,
'max_users' => 1, 'max_users' => 2,
'price_jod' => 120.00, 'price_jod' => 15.00, // Default legacy price
'price_monthly_jod' => 15.00,
'price_annual_jod' => 120.00,
'ai_features' => true, 'ai_features' => true,
'jofotara_enabled' => true, 'jofotara_enabled' => true,
'badge_color' => 'blue', 'badge_color' => 'blue',
'description_ar' => 'للمحاسبين المستقلين والشركات الصغيرة — 12,000 فاتورة سنوياً', 'description_ar' => 'للمحاسبين المستقلين والشركات الصغيرة — 3 شركات',
'features' => [ 'features' => [
'استخراج الفواتير بالذكاء الاصطناعي', 'استخراج الفواتير بالذكاء الاصطناعي',
'الربط المباشر مع جوفوترة', 'الربط المباشر مع جوفوترة',
'شركة واحدة فقط', 'حتى 3 شركات (بدلاً من واحدة)',
'12,000 فاتورة سنوياً (سخية جداً)', '500 فاتورة شهرياً (سخية جداً)',
'مستخدم واحد', 'مستخدمين اثنين',
'دعم فني عبر الواتساب', 'دعم فني عبر الواتساب',
], ],
], ],
'pro' => [ 'pro' => [
'id' => 'pro', 'id' => 'pro',
'name_ar' => 'الباقة الاحترافية (سنوي)', 'name_ar' => 'الباقة الاحترافية',
'name_en' => 'Pro Plan (Annual)', 'name_en' => 'Pro Plan',
'max_companies' => 9999, 'max_companies' => 9999,
'max_invoices_month' => 50000, 'max_invoices_month' => 3000,
'max_users' => 5, 'max_users' => 5,
'price_jod' => 250.00, 'price_jod' => 35.00, // Default legacy price
'price_monthly_jod' => 35.00,
'price_annual_jod' => 290.00,
'ai_features' => true, 'ai_features' => true,
'jofotara_enabled' => true, 'jofotara_enabled' => true,
'badge_color' => 'gold', 'badge_color' => 'gold',
'is_popular' => true, 'is_popular' => true,
'description_ar' => 'للمكاتب الكبيرة والموزعين — 50,000 فاتورة سنوياً', 'description_ar' => 'للمكاتب الكبيرة والموزعين — حجم عمل ضخم',
'features' => [ 'features' => [
'استخراج الفواتير بالذكاء الاصطناعي', 'استخراج الفواتير بالذكاء الاصطناعي',
'الربط المباشر مع جوفوترة', 'الربط المباشر مع جوفوترة',
'عدد شركات غير محدود', 'عدد شركات غير محدود',
'50,000 فاتورة سنوياً', '3,000 فاتورة شهرياً',
'5 مستخدمين', '5 مستخدمين',
'API كامل لتطبيق الهاتف', 'API كامل لتطبيق الهاتف',
'مدير حساب مخصص', 'مدير حساب مخصص',

View File

@@ -31,7 +31,7 @@ final class QuotaMiddleware
// Fetch subscription with plan info // Fetch subscription with plan info
$stmt = $db->prepare(" $stmt = $db->prepare("
SELECT s.*, sp.name_ar as plan_name, sp.ai_features, sp.jofotara_enabled SELECT s.*, sp.name_ar as plan_name, sp.ai_features, sp.jofotara_enabled, sp.price_monthly_jod, sp.price_annual_jod
FROM subscriptions s FROM subscriptions s
LEFT JOIN subscription_plans sp ON s.plan_id = sp.id LEFT JOIN subscription_plans sp ON s.plan_id = sp.id
WHERE s.tenant_id = ? WHERE s.tenant_id = ?
@@ -60,7 +60,9 @@ final class QuotaMiddleware
// Auto-reset period counter if billing period has ended // Auto-reset period counter if billing period has ended
if (!empty($sub['current_period_end']) && strtotime($sub['current_period_end']) < time()) { if (!empty($sub['current_period_end']) && strtotime($sub['current_period_end']) < time()) {
$newStart = date('Y-m-d H:i:s'); $newStart = date('Y-m-d H:i:s');
$newEnd = date('Y-m-d H:i:s', strtotime('+1 year')); // Changed to annual $cycle = $sub['billing_cycle'] ?? 'annual';
$interval = ($cycle === 'monthly') ? '+1 month' : '+1 year';
$newEnd = date('Y-m-d H:i:s', strtotime($interval));
$resetStmt = $db->prepare(" $resetStmt = $db->prepare("
UPDATE subscriptions UPDATE subscriptions

View File

@@ -34,6 +34,11 @@ $db = Database::getInstance();
$tenantId = $decoded['tenant_id']; $tenantId = $decoded['tenant_id'];
$userId = $decoded['user_id']; $userId = $decoded['user_id'];
$planId = $data['plan_id']; $planId = $data['plan_id'];
$cycle = $data['billing_cycle'] ?? 'annual'; // Default to annual
if (!in_array($cycle, ['monthly', 'annual'])) {
json_error('دورة الفوترة غير صالحة.', 422);
}
try { try {
// 1. Get plan details // 1. Get plan details
@@ -45,6 +50,9 @@ try {
json_error('الباقة المختارة غير صالحة أو غير نشطة.', 422); json_error('الباقة المختارة غير صالحة أو غير نشطة.', 422);
} }
// Determine amount based on cycle
$amount = ($cycle === 'monthly') ? ($plan['price_monthly_jod'] ?? $plan['price_jod']) : ($plan['price_annual_jod'] ?? ($plan['price_jod'] * 10));
// 2. Check for existing pending payment for this tenant // 2. Check for existing pending payment for this tenant
$stmt = $db->prepare("SELECT id FROM payment_requests WHERE tenant_id = ? AND status = 'pending' LIMIT 1"); $stmt = $db->prepare("SELECT id FROM payment_requests WHERE tenant_id = ? AND status = 'pending' LIMIT 1");
$stmt->execute([$tenantId]); $stmt->execute([$tenantId]);
@@ -68,15 +76,16 @@ try {
// 6. Create payment request // 6. Create payment request
$paymentId = Database::generateUuid(); $paymentId = Database::generateUuid();
$stmt = $db->prepare(" $stmt = $db->prepare("
INSERT INTO payment_requests (id, tenant_id, user_id, plan_id, amount_jod, internal_reference, cliq_alias, payer_name, status, created_at) INSERT INTO payment_requests (id, tenant_id, user_id, plan_id, billing_cycle, amount_jod, internal_reference, cliq_alias, payer_name, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', NOW()) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NOW())
"); ");
$stmt->execute([ $stmt->execute([
$paymentId, $paymentId,
$tenantId, $tenantId,
$userId, $userId,
$planId, $planId,
$plan['price_jod'], $cycle,
$amount,
$referenceNumber, $referenceNumber,
$cliqAlias, $cliqAlias,
$user['name'] ?? '' $user['name'] ?? ''
@@ -88,17 +97,17 @@ try {
$tenantId, $tenantId,
$userId, $userId,
$paymentId, $paymentId,
json_encode(['plan_id' => $planId, 'amount' => $plan['price_jod'], 'ref' => $referenceNumber]) json_encode(['plan_id' => $planId, 'cycle' => $cycle, 'amount' => $amount, 'ref' => $referenceNumber])
]); ]);
json_success([ json_success([
'payment_id' => $paymentId, 'payment_id' => $paymentId,
'reference_number' => $referenceNumber, 'reference_number' => $referenceNumber,
'cliq_alias' => $cliqAlias, 'cliq_alias' => $cliqAlias,
'amount_jod' => (float)$plan['price_jod'], 'amount_jod' => (float)$amount,
'plan_name' => $plan['name_ar'] ?? $plan['name_en'], 'plan_name' => ($plan['name_ar'] ?? $plan['name_en']) . " (" . ($cycle === 'monthly' ? 'شهري' : 'سنوي') . ")",
'payer_name' => $user['name'] ?? '', 'payer_name' => $user['name'] ?? '',
'instructions' => "قم بالتحويل عبر CliQ إلى الاسم المستعار: {$cliqAlias} بمبلغ {$plan['price_jod']} دينار أردني.", 'instructions' => "قم بالتحويل عبر CliQ إلى الاسم المستعار: {$cliqAlias} بمبلغ {$amount} دينار أردني.",
], 'تم إنشاء طلب الدفع بنجاح'); ], 'تم إنشاء طلب الدفع بنجاح');
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -13,7 +13,7 @@ $db = Database::getInstance();
try { try {
$stmt = $db->query(" $stmt = $db->query("
SELECT id, name_ar, name_en, max_companies, max_invoices_month, max_users, SELECT id, name_ar, name_en, max_companies, max_invoices_month, max_users,
price_jod, ai_features, jofotara_enabled, sort_order price_jod, price_annual_jod, price_monthly_jod, ai_features, jofotara_enabled, sort_order
FROM subscription_plans FROM subscription_plans
WHERE is_active = 1 WHERE is_active = 1
ORDER BY sort_order ASC ORDER BY sort_order ASC
@@ -36,6 +36,8 @@ try {
$plan['max_invoices_month'] = (int)$plan['max_invoices_month']; $plan['max_invoices_month'] = (int)$plan['max_invoices_month'];
$plan['max_users'] = (int)$plan['max_users']; $plan['max_users'] = (int)$plan['max_users'];
$plan['price_jod'] = (float)$plan['price_jod']; $plan['price_jod'] = (float)$plan['price_jod'];
$plan['price_annual_jod'] = (float)$plan['price_annual_jod'];
$plan['price_monthly_jod'] = (float)$plan['price_monthly_jod'];
$plan['ai_features'] = (bool)$plan['ai_features']; $plan['ai_features'] = (bool)$plan['ai_features'];
$plan['jofotara_enabled'] = (bool)$plan['jofotara_enabled']; $plan['jofotara_enabled'] = (bool)$plan['jofotara_enabled'];
} }

View File

@@ -164,11 +164,23 @@
<h2>اختر الباقة المناسبة لحجم أعمالك</h2> <h2>اختر الباقة المناسبة لحجم أعمالك</h2>
<p>لا رسوم خفية. لا عقود طويلة. ابدأ مجاناً وتدرّج حسب احتياجك.</p> <p>لا رسوم خفية. لا عقود طويلة. ابدأ مجاناً وتدرّج حسب احتياجك.</p>
</div> </div>
<div style="display:flex; justify-content:center; margin-bottom:40px;">
<div class="cycle-toggle" style="background:rgba(255,255,255,0.05); padding:6px; border-radius:14px; display:inline-flex; border:1px solid rgba(255,255,255,0.1); cursor:pointer;" onclick="this.classList.toggle('monthly'); document.querySelectorAll('.price-monthly').forEach(el=>el.style.display=this.classList.contains('monthly')?'block':'none'); document.querySelectorAll('.price-annual').forEach(el=>el.style.display=this.classList.contains('monthly')?'none':'block');">
<span class="toggle-btn annual-btn" style="padding:10px 24px; border-radius:10px; font-size:14px; font-weight:700; color:white; background:var(--green-mid); transition:all 0.3s;">دفع سنوي (توفير )</span>
<span class="toggle-btn monthly-btn" style="padding:10px 24px; border-radius:10px; font-size:14px; font-weight:700; color:var(--text-3); transition:all 0.3s;">دفع شهري</span>
</div>
</div>
<style>
.cycle-toggle.monthly .annual-btn { background:transparent; color:var(--text-3); }
.cycle-toggle.monthly .monthly-btn { background:var(--green-mid); color:white; }
</style>
<div class="pricing-grid"> <div class="pricing-grid">
<div class="price-card"> <div class="price-card">
<div class="price-name">التجربة المجانية</div> <div class="price-name">التجربة المجانية</div>
<div class="price-amount">0 <span>دينار/سنة</span></div> <div class="price-amount">0 <span>دينار/شهر</span></div>
<div class="price-desc">للتجربة الأولية</div> <div class="price-desc">للتجربة الأولية</div>
<ul class="price-features"> <ul class="price-features">
<li><span class="feature-check"></span> شركة واحدة</li> <li><span class="feature-check"></span> شركة واحدة</li>
@@ -182,13 +194,14 @@
<div class="price-card popular"> <div class="price-card popular">
<div class="popular-badge"> الأكثر اختياراً</div> <div class="popular-badge"> الأكثر اختياراً</div>
<div class="price-name">الباقة الأساسية (سنوي)</div> <div class="price-name">الباقة الأساسية</div>
<div class="price-amount">120 <span>دينار/سنة</span></div> <div class="price-amount price-annual">120 <span>دينار/سنة</span></div>
<div class="price-amount price-monthly" style="display:none;">15 <span>دينار/شهر</span></div>
<div class="price-desc">للمحاسبين والشركات الصغيرة</div> <div class="price-desc">للمحاسبين والشركات الصغيرة</div>
<ul class="price-features"> <ul class="price-features">
<li><span class="feature-check"></span> شركة واحدة</li> <li><span class="feature-check"></span> حتى 3 شركات</li>
<li><span class="feature-check"></span> 12,000 فاتورة سنوياً</li> <li><span class="feature-check"></span> 500 فاتورة شهرياً</li>
<li><span class="feature-check"></span> مستخدم واحد</li> <li><span class="feature-check"></span> مستخدمين اثنين</li>
<li><span class="feature-check"></span> دعم فني متكامل</li> <li><span class="feature-check"></span> دعم فني متكامل</li>
<li><span class="feature-check"></span> ربط مباشر مع جوفوترة</li> <li><span class="feature-check"></span> ربط مباشر مع جوفوترة</li>
</ul> </ul>
@@ -196,12 +209,13 @@
</div> </div>
<div class="price-card"> <div class="price-card">
<div class="price-name">الباقة الاحترافية (سنوي)</div> <div class="price-name">الباقة الاحترافية</div>
<div class="price-amount">250 <span>دينار/سنة</span></div> <div class="price-amount price-annual">290 <span>دينار/سنة</span></div>
<div class="price-amount price-monthly" style="display:none;">35 <span>دينار/شهر</span></div>
<div class="price-desc">للمكاتب الكبيرة والموزعين</div> <div class="price-desc">للمكاتب الكبيرة والموزعين</div>
<ul class="price-features"> <ul class="price-features">
<li><span class="feature-check"></span> شركات غير محدودة</li> <li><span class="feature-check"></span> شركات غير محدودة</li>
<li><span class="feature-check"></span> 50,000 فاتورة سنوياً</li> <li><span class="feature-check"></span> 3,000 فاتورة شهرياً</li>
<li><span class="feature-check"></span> 5 مستخدمين</li> <li><span class="feature-check"></span> 5 مستخدمين</li>
<li><span class="feature-check"></span> تدقيق ذكي استباقي</li> <li><span class="feature-check"></span> تدقيق ذكي استباقي</li>
<li><span class="feature-check"></span> مدير حساب مخصص</li> <li><span class="feature-check"></span> مدير حساب مخصص</li>

View File

@@ -1951,6 +1951,22 @@
</p> </p>
</div> </div>
<!-- Cycle Toggle -->
<div style="display:flex; justify-content:center; margin-bottom:36px;">
<div style="background:var(--bg-secondary); padding:5px; border-radius:12px; display:flex; gap:5px; border:1px solid var(--border);">
<button @click="billingCycle = 'monthly'"
:class="billingCycle === 'monthly' ? 'btn-navy' : 'btn-ghost'"
style="font-size:13px; padding:8px 20px; border-radius:8px; transition:all 0.2s;">
دفع شهري
</button>
<button @click="billingCycle = 'annual'"
:class="billingCycle === 'annual' ? 'btn-navy' : 'btn-ghost'"
style="font-size:13px; padding:8px 20px; border-radius:8px; transition:all 0.2s;">
دفع سنوي (توفير )
</button>
</div>
</div>
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(290px, 1fr)); gap:24px;"> <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(290px, 1fr)); gap:24px;">
<template x-for="p in plans" :key="p.id"> <template x-for="p in plans" :key="p.id">
<div class="plan-card" :class="subscription?.plan_id === p.id ? 'active-plan' : ''"> <div class="plan-card" :class="subscription?.plan_id === p.id ? 'active-plan' : ''">
@@ -1969,9 +1985,9 @@
style="text-align:center; padding:18px 0; border-top:1px solid var(--border); border-bottom:1px solid var(--border);"> style="text-align:center; padding:18px 0; border-top:1px solid var(--border); border-bottom:1px solid var(--border);">
<span class="num-font" <span class="num-font"
style="font-size:46px; font-weight:800; color:var(--green-mid);" style="font-size:46px; font-weight:800; color:var(--green-mid);"
x-text="p.price_jod"></span> x-text="billingCycle === 'monthly' ? (p.price_monthly_jod || p.price_jod) : (p.price_annual_jod || (p.price_jod * 10))"></span>
<span style="font-size:15px; color:var(--text-3); font-weight:500;"> دينار / <span style="font-size:15px; color:var(--text-3); font-weight:500;"> دينار /
سنة</span> <span x-text="billingCycle === 'monthly' ? 'شهر' : 'سنة'"></span></span>
</div> </div>
<ul <ul
@@ -2927,7 +2943,9 @@
isUploadingBatch: false, batchProgress: { total: 0, current: 0 }, isUploadingBatch: false, batchProgress: { total: 0, current: 0 },
showAddTenantModal: false, showEditTenantModal: false, showTenantStatsModal: false, showAddTenantModal: false, showEditTenantModal: false, showTenantStatsModal: false,
acknowledgedWarnings: false, isEditingInvoice: false, acknowledgedWarnings: false, isEditingInvoice: false,
isBusy: false, globalError: '', isBusy: false,
billingCycle: 'annual', // 'monthly' or 'annual'
globalError: '',
newUser: { name: '', email: '', password: '', role: 'accountant', tenant_id: '' }, newUser: { name: '', email: '', password: '', role: 'accountant', tenant_id: '' },
newCompany: { name: '', tax_identification_number: '', commercial_registration_number: '', address: '', tenant_id: '' }, newCompany: { name: '', tax_identification_number: '', commercial_registration_number: '', address: '', tenant_id: '' },
@@ -3399,7 +3417,10 @@
const res = await fetch('/index.php?route=v1/payments/create', { const res = await fetch('/index.php?route=v1/payments/create', {
method: 'POST', method: 'POST',
headers: { 'Authorization': 'Bearer ' + this.token(), 'Content-Type': 'application/json' }, headers: { 'Authorization': 'Bearer ' + this.token(), 'Content-Type': 'application/json' },
body: JSON.stringify({ plan_id: plan.id }) body: JSON.stringify({
plan_id: plan.id,
billing_cycle: this.billingCycle
})
}); });
const json = await res.json(); const json = await res.json();
this.isBusy = false; this.isBusy = false;

View File

@@ -0,0 +1,46 @@
-- 1. إضافة أعمدة الأسعار (شهري وسنوي) وجدولة الفواتير
ALTER TABLE subscription_plans
ADD COLUMN price_annual_jod DECIMAL(10,2) DEFAULT 0.00 AFTER price_jod,
ADD COLUMN price_monthly_jod DECIMAL(10,2) DEFAULT 0.00 AFTER price_annual_jod;
-- 2. إضافة دورة الفوترة لجدول الاشتراكات وطلبات الدفع
ALTER TABLE subscriptions
ADD COLUMN billing_cycle ENUM('monthly', 'annual') DEFAULT 'annual' AFTER status;
ALTER TABLE payment_requests
ADD COLUMN billing_cycle ENUM('monthly', 'annual') DEFAULT 'annual' AFTER plan_id;
-- 3. تحديث الباقات بالقيم الجديدة المقترحة في التحليل الاستراتيجي
-- الباقة الأساسية
UPDATE subscription_plans
SET
name_ar = 'الباقة الأساسية',
price_annual_jod = 120.00,
price_monthly_jod = 15.00,
max_invoices_month = 500, -- تم تخفيضها من 12000 للتحويل المستقبلي
max_companies = 3, -- تم زيادتها من 1 لجذب المحاسبين المستقلين
max_users = 2,
is_active = 1
WHERE id = 'basic';
-- الباقة الاحترافية
UPDATE subscription_plans
SET
name_ar = 'الباقة الاحترافية',
price_annual_jod = 290.00,
price_monthly_jod = 35.00,
max_invoices_month = 3000,
max_companies = 9999,
max_users = 5,
is_active = 1
WHERE id = 'pro';
-- الباقة المجانية
UPDATE subscription_plans
SET
price_annual_jod = 0.00,
price_monthly_jod = 0.00,
max_invoices_month = 15,
max_companies = 1,
max_users = 1
WHERE id = 'free';