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
$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
LEFT JOIN subscription_plans sp ON s.plan_id = sp.id
WHERE s.tenant_id = ?
@@ -60,7 +60,9 @@ final class QuotaMiddleware
// Auto-reset period counter if billing period has ended
if (!empty($sub['current_period_end']) && strtotime($sub['current_period_end']) < time()) {
$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("
UPDATE subscriptions

View File

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

View File

@@ -31,7 +31,7 @@ final class QuotaMiddleware
// Fetch subscription with plan info
$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
LEFT JOIN subscription_plans sp ON s.plan_id = sp.id
WHERE s.tenant_id = ?
@@ -60,7 +60,9 @@ final class QuotaMiddleware
// Auto-reset period counter if billing period has ended
if (!empty($sub['current_period_end']) && strtotime($sub['current_period_end']) < time()) {
$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("
UPDATE subscriptions

View File

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

View File

@@ -13,7 +13,7 @@ $db = Database::getInstance();
try {
$stmt = $db->query("
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
WHERE is_active = 1
ORDER BY sort_order ASC
@@ -36,6 +36,8 @@ try {
$plan['max_invoices_month'] = (int)$plan['max_invoices_month'];
$plan['max_users'] = (int)$plan['max_users'];
$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['jofotara_enabled'] = (bool)$plan['jofotara_enabled'];
}

View File

@@ -164,11 +164,23 @@
<h2>اختر الباقة المناسبة لحجم أعمالك</h2>
<p>لا رسوم خفية. لا عقود طويلة. ابدأ مجاناً وتدرّج حسب احتياجك.</p>
</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="price-card">
<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>
<ul class="price-features">
<li><span class="feature-check"></span> شركة واحدة</li>
@@ -182,13 +194,14 @@
<div class="price-card popular">
<div class="popular-badge"> الأكثر اختياراً</div>
<div class="price-name">الباقة الأساسية (سنوي)</div>
<div class="price-amount">120 <span>دينار/سنة</span></div>
<div class="price-name">الباقة الأساسية</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>
<ul class="price-features">
<li><span class="feature-check"></span> شركة واحدة</li>
<li><span class="feature-check"></span> 12,000 فاتورة سنوياً</li>
<li><span class="feature-check"></span> مستخدم واحد</li>
<li><span class="feature-check"></span> حتى 3 شركات</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>
</ul>
@@ -196,12 +209,13 @@
</div>
<div class="price-card">
<div class="price-name">الباقة الاحترافية (سنوي)</div>
<div class="price-amount">250 <span>دينار/سنة</span></div>
<div class="price-name">الباقة الاحترافية</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>
<ul class="price-features">
<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> تدقيق ذكي استباقي</li>
<li><span class="feature-check"></span> مدير حساب مخصص</li>

View File

@@ -1951,6 +1951,22 @@
</p>
</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;">
<template x-for="p in plans" :key="p.id">
<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);">
<span class="num-font"
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>
<span x-text="billingCycle === 'monthly' ? 'شهر' : 'سنة'"></span></span>
</div>
<ul
@@ -2927,7 +2943,9 @@
isUploadingBatch: false, batchProgress: { total: 0, current: 0 },
showAddTenantModal: false, showEditTenantModal: false, showTenantStatsModal: false,
acknowledgedWarnings: false, isEditingInvoice: false,
isBusy: false, globalError: '',
isBusy: false,
billingCycle: 'annual', // 'monthly' or 'annual'
globalError: '',
newUser: { name: '', email: '', password: '', role: 'accountant', 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', {
method: 'POST',
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();
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';