Commit 7537971e authored by Administrator's avatar Administrator

Update 34 files via Son of Anton

parent 1e79566a
<?php
declare(strict_types=1);
namespace App\Modules\Pricing\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Pricing\Models\PricingConfig;
class PricingController extends Controller
{
public function index(Request $request): Response
{
$configs = PricingConfig::allWithDetails();
return $this->view('Pricing.Views.index', ['configs' => $configs]);
}
public function edit(Request $request, string $id): Response
{
$config = PricingConfig::find((int) $id);
if (!$config) {
return $this->redirect('/pricing')->withError('التسعير غير موجود');
}
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
$quals = $db->select("SELECT id, name_ar FROM qualifications WHERE is_active = 1 ORDER BY sort_order");
return $this->view('Pricing.Views.edit', ['config' => $config, 'branches' => $branches, 'quals' => $quals]);
}
public function update(Request $request, string $id): Response
{
$config = PricingConfig::find((int) $id);
if (!$config) {
return $this->redirect('/pricing')->withError('التسعير غير موجود');
}
$data = $this->validate($request->all(), [
'price' => 'required|numeric|min:0',
'effective_from' => 'required|date',
'effective_to' => 'nullable|date',
'notes' => 'nullable|string|max:1000',
]);
$config->update([
'price' => $data['price'],
'effective_from' => $data['effective_from'],
'effective_to' => $data['effective_to'] ?? null,
'notes' => $data['notes'] ?? null,
]);
return $this->redirect('/pricing')->withSuccess('تم تحديث التسعير بنجاح');
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Pricing\Models;
use App\Core\Model;
use App\Core\App;
class DiscountRule extends Model
{
protected static string $table = 'discount_rules';
protected static bool $softDelete = false;
protected static bool $timestamps = true;
protected static array $fillable = [
'discount_code', 'name_ar', 'name_en', 'discount_type', 'value',
'min_quantity', 'max_quantity', 'stackable', 'max_discount_percentage',
'branch_id', 'effective_from', 'effective_to', 'is_active',
];
public static function allActive(): array
{
$db = App::getInstance()->db();
return $db->select("SELECT * FROM discount_rules WHERE is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY discount_code");
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Pricing\Models;
use App\Core\Model;
use App\Core\App;
class PricingConfig extends Model
{
protected static string $table = 'pricing_configs';
protected static bool $softDelete = false;
protected static bool $timestamps = true;
protected static array $fillable = [
'branch_id', 'qualification_id', 'membership_type', 'price', 'currency',
'effective_from', 'effective_to', 'is_active', 'notes',
];
public static function allWithDetails(): array
{
$db = App::getInstance()->db();
return $db->select("
SELECT pc.*, b.name_ar as branch_name, q.name_ar as qualification_name
FROM pricing_configs pc
JOIN branches b ON b.id = pc.branch_id
JOIN qualifications q ON q.id = pc.qualification_id
WHERE pc.is_active = 1
ORDER BY b.name_ar, q.sort_order, pc.effective_from DESC
");
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/pricing', 'Pricing\Controllers\PricingController@index', ['auth'], 'pricing.view'],
['GET', '/pricing/{id:\d+}/edit', 'Pricing\Controllers\PricingController@edit', ['auth'], 'pricing.edit'],
['POST', '/pricing/{id:\d+}', 'Pricing\Controllers\PricingController@update', ['auth', 'csrf'], 'pricing.edit'],
];
\ No newline at end of file
This diff is collapsed.
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل التسعير<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/pricing/<?= (int) $config->id ?>">
<?= csrf_field() ?>
<div class="card" style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">السعر (ج.م) <span style="color:#DC2626;">*</span></label>
<input type="number" name="price" value="<?= e($config->price) ?>" class="form-input" step="0.01" min="0" required>
</div>
<div class="form-group">
<label class="form-label">تاريخ السريان <span style="color:#DC2626;">*</span></label>
<input type="date" name="effective_from" value="<?= e($config->effective_from) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">تاريخ الانتهاء</label>
<input type="date" name="effective_to" value="<?= e($config->effective_to ?? '') ?>" class="form-input">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-textarea" rows="3"><?= e($config->notes ?? '') ?></textarea>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary" style="margin-top:20px;">حفظ</button>
<a href="/pricing" class="btn btn-outline" style="margin-top:20px;">إلغاء</a>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>التسعير<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr><th>الفرع</th><th>المؤهل</th><th>النوع</th><th>السعر</th><th>من</th><th>إلى</th><th>الإجراءات</th></tr>
</thead>
<tbody>
<?php foreach ($configs as $c): ?>
<tr>
<td><?= e($c['branch_name']) ?></td>
<td><?= e($c['qualification_name']) ?></td>
<td><?= e($c['membership_type']) ?></td>
<td style="font-weight:700;direction:ltr;text-align:right;"><?= money($c['price']) ?></td>
<td style="font-size:12px;"><?= e($c['effective_from']) ?></td>
<td style="font-size:12px;"><?= e($c['effective_to'] ?? '—') ?></td>
<td><a href="/pricing/<?= (int) $c['id'] ?>/edit" class="btn btn-sm btn-outline">تعديل</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($configs)): ?>
<tr><td colspan="7" style="text-align:center;padding:40px;color:#6B7280;">لا توجد تسعيرات</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
// Menu and permissions registered in Rules/bootstrap.php (shared group)
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Rules\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Rules\Models\BusinessRule;
use App\Modules\Rules\Models\RuleVersion;
use App\Modules\Rules\Services\RuleEngine;
class RuleController extends Controller
{
public function index(Request $request): Response
{
$category = $request->get('category', '');
$categories = BusinessRule::getCategories();
$db = App::getInstance()->db();
$where = 'is_active = 1';
$params = [];
if ($category !== '') {
$where .= ' AND category = ?';
$params[] = $category;
}
$rules = $db->select("SELECT * FROM business_rules WHERE {$where} ORDER BY category, rule_code", $params);
return $this->view('Rules.Views.index', [
'rules' => $rules,
'categories' => $categories,
'category' => $category,
]);
}
public function edit(Request $request, string $id): Response
{
$rule = BusinessRule::find((int) $id);
if (!$rule) {
return $this->redirect('/rules')->withError('القاعدة غير موجودة');
}
return $this->view('Rules.Views.edit', ['rule' => $rule]);
}
public function update(Request $request, string $id): Response
{
$rule = BusinessRule::find((int) $id);
if (!$rule) {
return $this->redirect('/rules')->withError('القاعدة غير موجودة');
}
$newValueJson = trim((string) $request->post('current_value_json', ''));
$reason = trim((string) $request->post('change_reason', ''));
if ($newValueJson === '' || $reason === '') {
return $this->redirect("/rules/{$id}/edit")->withError('يرجى ملء جميع الحقول المطلوبة');
}
$decoded = json_decode($newValueJson, true);
if ($decoded === null && $newValueJson !== 'null') {
return $this->redirect("/rules/{$id}/edit")->withError('قيمة JSON غير صالحة');
}
try {
RuleEngine::update($rule->rule_code, $newValueJson, $reason, $rule->branch_id ? (int) $rule->branch_id : null);
return $this->redirect('/rules')->withSuccess('تم تحديث القاعدة بنجاح');
} catch (\Throwable $e) {
return $this->redirect("/rules/{$id}/edit")->withError('فشل تحديث القاعدة: ' . $e->getMessage());
}
}
public function history(Request $request, string $id): Response
{
$rule = BusinessRule::find((int) $id);
if (!$rule) {
return $this->redirect('/rules')->withError('القاعدة غير موجودة');
}
$versions = RuleVersion::getForRule((int) $id);
return $this->view('Rules.Views.history', ['rule' => $rule, 'versions' => $versions]);
}
public function simulate(Request $request, string $id): Response
{
$rule = BusinessRule::find((int) $id);
if (!$rule) {
return $this->redirect('/rules')->withError('القاعدة غير موجودة');
}
return $this->view('Rules.Views.simulate', ['rule' => $rule]);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Rules\Models;
use App\Core\Model;
use App\Core\App;
class BusinessRule extends Model
{
protected static string $table = 'business_rules';
protected static bool $softDelete = false;
protected static bool $timestamps = true;
protected static array $fillable = [
'rule_code', 'category', 'name_ar', 'name_en', 'description_ar', 'description_en',
'parameters_json', 'current_value_json', 'data_type', 'branch_id',
'effective_from', 'effective_to', 'is_active', 'version',
];
public function getValue(): array
{
return json_decode($this->current_value_json ?? '{}', true) ?? [];
}
public function getParameters(): array
{
return json_decode($this->parameters_json ?? '{}', true) ?? [];
}
public static function allByCategory(string $category, ?int $branchId = null): array
{
$db = App::getInstance()->db();
if ($branchId) {
return $db->select(
"SELECT * FROM business_rules WHERE category = ? AND (branch_id IS NULL OR branch_id = ?) AND is_active = 1 ORDER BY rule_code",
[$category, $branchId]
);
}
return $db->select(
"SELECT * FROM business_rules WHERE category = ? AND is_active = 1 ORDER BY rule_code",
[$category]
);
}
public static function getCategories(): array
{
$db = App::getInstance()->db();
$rows = $db->select("SELECT DISTINCT category FROM business_rules WHERE is_active = 1 ORDER BY category");
return array_column($rows, 'category');
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Rules\Models;
use App\Core\Model;
use App\Core\App;
class RuleOverride extends Model
{
protected static string $table = 'rule_overrides';
protected static bool $softDelete = false;
protected static bool $timestamps = false;
protected static array $fillable = [
'rule_id', 'entity_type', 'entity_id', 'override_value_json',
'reason', 'approved_by', 'effective_from', 'effective_to', 'is_active',
];
public static function getForEntity(string $entityType, int $entityId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT ro.*, br.rule_code, br.name_ar FROM rule_overrides ro JOIN business_rules br ON br.id = ro.rule_id WHERE ro.entity_type = ? AND ro.entity_id = ? AND ro.is_active = 1 AND (ro.effective_to IS NULL OR ro.effective_to >= CURDATE()) ORDER BY ro.created_at DESC",
[$entityType, $entityId]
);
}
public static function findOverride(int $ruleId, string $entityType, int $entityId): ?array
{
$db = App::getInstance()->db();
return $db->selectOne(
"SELECT * FROM rule_overrides WHERE rule_id = ? AND entity_type = ? AND entity_id = ? AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE())",
[$ruleId, $entityType, $entityId]
);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Rules\Models;
use App\Core\Model;
use App\Core\App;
class RuleVersion extends Model
{
protected static string $table = 'rule_versions';
protected static bool $softDelete = false;
protected static bool $timestamps = false;
protected static array $fillable = [
'rule_id', 'version_number', 'old_value_json', 'new_value_json', 'changed_by', 'change_reason',
];
public static function getForRule(int $ruleId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT rv.*, e.full_name_ar as changed_by_name FROM rule_versions rv LEFT JOIN employees e ON e.id = rv.changed_by WHERE rv.rule_id = ? ORDER BY rv.version_number DESC",
[$ruleId]
);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/rules', 'Rules\Controllers\RuleController@index', ['auth'], 'rules.view'],
['GET', '/rules/{id:\d+}/edit', 'Rules\Controllers\RuleController@edit', ['auth'], 'rules.edit'],
['POST', '/rules/{id:\d+}', 'Rules\Controllers\RuleController@update', ['auth', 'csrf'], 'rules.edit'],
['GET', '/rules/{id:\d+}/history', 'Rules\Controllers\RuleController@history', ['auth'], 'rules.view'],
['GET', '/rules/{id:\d+}/simulate', 'Rules\Controllers\RuleController@simulate', ['auth'], 'rules.view'],
];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Rules\Services;
use App\Core\App;
use App\Core\Logger;
use App\Modules\Rules\Models\BusinessRule;
use App\Modules\Rules\Models\RuleVersion;
use App\Modules\Rules\Models\RuleOverride;
final class RuleEngine
{
private static array $cache = [];
public static function get(string $ruleCode, ?int $branchId = null, ?string $date = null): mixed
{
$date = $date ?? date('Y-m-d');
$cacheKey = "{$ruleCode}_{$branchId}_{$date}";
if (isset(self::$cache[$cacheKey])) {
return self::$cache[$cacheKey];
}
$db = App::getInstance()->db();
// Try branch-specific first
$rule = null;
if ($branchId !== null) {
$rule = $db->selectOne(
"SELECT * FROM business_rules WHERE rule_code = ? AND branch_id = ? AND is_active = 1 AND effective_from <= ? AND (effective_to IS NULL OR effective_to >= ?)",
[$ruleCode, $branchId, $date, $date]
);
}
// Fallback to global
if (!$rule) {
$rule = $db->selectOne(
"SELECT * FROM business_rules WHERE rule_code = ? AND branch_id IS NULL AND is_active = 1 AND effective_from <= ? AND (effective_to IS NULL OR effective_to >= ?)",
[$ruleCode, $date, $date]
);
}
if (!$rule) {
Logger::warning("Rule not found: {$ruleCode}", ['branch_id' => $branchId, 'date' => $date]);
self::$cache[$cacheKey] = null;
return null;
}
$value = json_decode($rule['current_value_json'], true);
self::$cache[$cacheKey] = $value;
return $value;
}
public static function getValue(string $ruleCode, string $key = 'value', ?int $branchId = null): mixed
{
$data = self::get($ruleCode, $branchId);
if ($data === null) {
return null;
}
return $data[$key] ?? ($data['percentage'] ?? ($data['amount'] ?? ($data['value'] ?? null)));
}
public static function getAll(string $category, ?int $branchId = null): array
{
return BusinessRule::allByCategory($category, $branchId);
}
public static function update(string $ruleCode, string $newValueJson, string $reason, ?int $branchId = null): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$where = 'rule_code = ? AND is_active = 1';
$params = [$ruleCode];
if ($branchId !== null) {
$where .= ' AND branch_id = ?';
$params[] = $branchId;
} else {
$where .= ' AND branch_id IS NULL';
}
$rule = $db->selectOne("SELECT * FROM business_rules WHERE {$where}", $params);
if (!$rule) {
throw new \RuntimeException("Rule not found: {$ruleCode}");
}
$newVersion = (int) $rule['version'] + 1;
// Create version history
$db->insert('rule_versions', [
'rule_id' => (int) $rule['id'],
'version_number' => $newVersion,
'old_value_json' => $rule['current_value_json'],
'new_value_json' => $newValueJson,
'changed_by' => $employee ? (int) $employee->id : null,
'changed_at' => date('Y-m-d H:i:s'),
'change_reason' => $reason,
]);
// Update current value
$db->update('business_rules', [
'current_value_json' => $newValueJson,
'version' => $newVersion,
'updated_at' => date('Y-m-d H:i:s'),
'updated_by' => $employee ? (int) $employee->id : null,
], '`id` = ?', [$rule['id']]);
// Clear cache
self::$cache = [];
Logger::info("Rule updated: {$ruleCode}", ['version' => $newVersion, 'reason' => $reason]);
}
public static function override(string $ruleCode, string $entityType, int $entityId, string $overrideValueJson, string $reason): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$rule = $db->selectOne("SELECT id FROM business_rules WHERE rule_code = ? AND branch_id IS NULL AND is_active = 1", [$ruleCode]);
if (!$rule) {
throw new \RuntimeException("Rule not found: {$ruleCode}");
}
$db->insert('rule_overrides', [
'rule_id' => (int) $rule['id'],
'entity_type' => $entityType,
'entity_id' => $entityId,
'override_value_json' => $overrideValueJson,
'reason' => $reason,
'approved_by' => $employee ? (int) $employee->id : null,
'effective_from' => date('Y-m-d'),
'is_active' => 1,
'created_at' => date('Y-m-d H:i:s'),
]);
}
public static function getEffective(string $ruleCode, string $entityType, int $entityId, ?int $branchId = null): mixed
{
$db = App::getInstance()->db();
$rule = $db->selectOne("SELECT id FROM business_rules WHERE rule_code = ? AND branch_id IS NULL AND is_active = 1", [$ruleCode]);
if ($rule) {
$override = RuleOverride::findOverride((int) $rule['id'], $entityType, $entityId);
if ($override) {
return json_decode($override['override_value_json'], true);
}
}
return self::get($ruleCode, $branchId);
}
public static function clearCache(): void
{
self::$cache = [];
}
}
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل القاعدة: <?= e($rule->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/rules/<?= (int) $rule->id ?>">
<?= csrf_field() ?>
<div class="card" style="padding:20px;margin-bottom:20px;">
<table style="width:100%;font-size:14px;margin-bottom:20px;">
<tr><td style="padding:6px 0;color:#6B7280;width:30%;">كود القاعدة</td><td><code><?= e($rule->rule_code) ?></code></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">التصنيف</td><td><?= e($rule->category) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">النوع</td><td><?= e($rule->data_type) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الإصدار الحالي</td><td><?= (int) $rule->version ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ السريان</td><td><?= e($rule->effective_from) ?></td></tr>
</table>
<div class="form-group" style="margin-bottom:20px;">
<label class="form-label">القيمة الحالية (JSON) <span style="color:#DC2626;">*</span></label>
<textarea name="current_value_json" class="form-textarea" rows="6" required style="font-family:monospace;direction:ltr;text-align:left;"><?= e($rule->current_value_json) ?></textarea>
<?php $params = json_decode($rule->parameters_json ?? '{}', true); ?>
<?php if (!empty($params)): ?>
<small style="color:#6B7280;">المعاملات: <?php foreach ($params as $k => $v): ?><code><?= e($k) ?></code> (<?= e($v) ?>) <?php endforeach; ?></small>
<?php endif; ?>
</div>
<div class="form-group">
<label class="form-label">سبب التعديل <span style="color:#DC2626;">*</span></label>
<textarea name="change_reason" class="form-textarea" rows="3" required placeholder="اذكر سبب تعديل هذه القاعدة..."></textarea>
</div>
</div>
<button type="submit" class="btn btn-primary">حفظ التعديل</button>
<a href="/rules" class="btn btn-outline">إلغاء</a>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>سجل تعديلات: <?= e($rule->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/rules" class="btn btn-outline">← العودة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<?php if (empty($versions)): ?>
<div style="padding:40px;text-align:center;color:#6B7280;">لا توجد تعديلات سابقة</div>
<?php else: ?>
<div class="table-responsive">
<table class="data-table">
<thead><tr><th>الإصدار</th><th>التاريخ</th><th>بواسطة</th><th>القيمة القديمة</th><th>القيمة الجديدة</th><th>السبب</th></tr></thead>
<tbody>
<?php foreach ($versions as $v): ?>
<tr>
<td style="text-align:center;font-weight:700;"><?= (int) $v['version_number'] ?></td>
<td style="font-size:12px;"><?= e($v['changed_at']) ?></td>
<td><?= e($v['changed_by_name'] ?? '—') ?></td>
<td style="font-size:11px;direction:ltr;text-align:left;max-width:200px;overflow:hidden;"><code><?= e(mb_substr($v['old_value_json'] ?? '—', 0, 100)) ?></code></td>
<td style="font-size:11px;direction:ltr;text-align:left;max-width:200px;overflow:hidden;"><code><?= e(mb_substr($v['new_value_json'], 0, 100)) ?></code></td>
<td style="font-size:13px;color:#6B7280;"><?= e($v['change_reason'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>محرك القواعد<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/rules" style="display:flex;gap:10px;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">التصنيف</label>
<select name="category" class="form-select" style="min-width:150px;">
<option value="">الكل</option>
<?php foreach ($categories as $c): ?>
<option value="<?= e($c) ?>" <?= $category === $c ? 'selected' : '' ?>><?= e($c) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-outline">تصفية</button>
<a href="/rules" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>كود القاعدة</th>
<th>الاسم</th>
<th>التصنيف</th>
<th>النوع</th>
<th>القيمة الحالية</th>
<th>الإصدار</th>
<th>الفرع</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($rules as $r): ?>
<tr>
<td><code style="font-size:11px;"><?= e($r['rule_code']) ?></code></td>
<td><?= e($r['name_ar']) ?></td>
<td><span style="background:#F3F4F6;padding:2px 8px;border-radius:4px;font-size:12px;"><?= e($r['category']) ?></span></td>
<td style="font-size:12px;"><?= e($r['data_type']) ?></td>
<td style="font-size:12px;max-width:200px;overflow:hidden;text-overflow:ellipsis;">
<?php $val = json_decode($r['current_value_json'], true); ?>
<?php if (is_array($val)): ?>
<?php foreach ($val as $k => $v): ?>
<span style="color:#6B7280;"><?= e($k) ?>:</span> <strong><?= e((string)$v) ?></strong><?= $k !== array_key_last($val) ? ', ' : '' ?>
<?php endforeach; ?>
<?php else: ?>
<?= e($r['current_value_json']) ?>
<?php endif; ?>
</td>
<td style="text-align:center;"><?= (int) $r['version'] ?></td>
<td style="font-size:12px;"><?= $r['branch_id'] ? '#' . (int) $r['branch_id'] : 'عام' ?></td>
<td>
<div style="display:flex;gap:5px;">
<a href="/rules/<?= (int) $r['id'] ?>/edit" class="btn btn-sm btn-outline">تعديل</a>
<a href="/rules/<?= (int) $r['id'] ?>/history" class="btn btn-sm btn-outline">السجل</a>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($rules)): ?>
<tr><td colspan="8" style="text-align:center;padding:40px;color:#6B7280;">لا توجد قواعد</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>محاكاة تعديل: <?= e($rule->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="padding:20px;">
<p style="color:#6B7280;margin-bottom:15px;">محاكاة تأثير تغيير هذه القاعدة على البيانات الحالية. هذه الميزة ستكون متاحة بالكامل بعد إضافة وحدة الأعضاء.</p>
<table style="width:100%;font-size:14px;">
<tr><td style="padding:6px 0;color:#6B7280;width:30%;">كود القاعدة</td><td><code><?= e($rule->rule_code) ?></code></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">القيمة الحالية</td><td style="direction:ltr;text-align:left;"><code><?= e($rule->current_value_json) ?></code></td></tr>
</table>
<div style="margin-top:20px;padding:20px;background:#FFF7ED;border:1px solid #FED7AA;border-radius:8px;">
<strong style="color:#D97706;">⚠ ملاحظة:</strong> المحاكاة الكاملة ستتوفر عند إنشاء بيانات الأعضاء (المرحلة 8). حالياً يمكنك مراجعة القيمة الحالية والمعاملات المتاحة.
</div>
</div>
<a href="/rules" class="btn btn-outline" style="margin-top:20px;">← العودة</a>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
MenuRegistry::register('rules_pricing', [
'label_ar' => 'القواعد والتسعير',
'label_en' => 'Rules & Pricing',
'icon' => '⚙️',
'route' => '/rules',
'permission' => 'rules.view',
'order' => 150,
'children' => [
['label_ar' => 'محرك القواعد', 'label_en' => 'Rules Engine', 'route' => '/rules', 'permission' => 'rules.view', 'order' => 1],
['label_ar' => 'التسعير', 'label_en' => 'Pricing', 'route' => '/pricing', 'permission' => 'pricing.view', 'order' => 2],
['label_ar' => 'كتالوج الخدمات', 'label_en' => 'Service Catalog', 'route' => '/catalog', 'permission' => 'pricing.view', 'order' => 3],
],
]);
PermissionRegistry::register('rules', [
'rules.view' => ['ar' => 'عرض القواعد', 'en' => 'View Rules'],
'rules.edit' => ['ar' => 'تعديل القواعد', 'en' => 'Edit Rules'],
'rules.create' => ['ar' => 'إنشاء قاعدة', 'en' => 'Create Rule'],
'rules.deactivate' => ['ar' => 'تعطيل قاعدة', 'en' => 'Deactivate Rule'],
]);
PermissionRegistry::register('pricing', [
'pricing.view' => ['ar' => 'عرض التسعير', 'en' => 'View Pricing'],
'pricing.edit' => ['ar' => 'تعديل التسعير', 'en' => 'Edit Pricing'],
'pricing.create' => ['ar' => 'إنشاء تسعير', 'en' => 'Create Pricing'],
]);
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\ServiceCatalog\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\ServiceCatalog\Models\ServicePrice;
class CatalogController extends Controller
{
public function index(Request $request): Response
{
$services = ServicePrice::allActive();
return $this->view('ServiceCatalog.Views.index', ['services' => $services]);
}
public function edit(Request $request, string $id): Response
{
$service = ServicePrice::find((int) $id);
if (!$service) {
return $this->redirect('/catalog')->withError('الخدمة غير موجودة');
}
return $this->view('ServiceCatalog.Views.edit', ['service' => $service]);
}
public function update(Request $request, string $id): Response
{
$service = ServicePrice::find((int) $id);
if (!$service) {
return $this->redirect('/catalog')->withError('الخدمة غير موجودة');
}
$data = $this->validate($request->all(), [
'base_amount' => 'nullable|numeric|min:0',
'percentage' => 'nullable|numeric|min:0|max:100',
'annual_amount' => 'nullable|numeric|min:0',
]);
$service->update([
'base_amount' => $data['base_amount'] ?? null,
'percentage' => $data['percentage'] ?? null,
'annual_amount' => $data['annual_amount'] ?? null,
]);
return $this->redirect('/catalog')->withSuccess('تم تحديث الخدمة بنجاح');
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\ServiceCatalog\Models;
use App\Core\Model;
use App\Core\App;
class ServicePrice extends Model
{
protected static string $table = 'service_catalog';
protected static bool $softDelete = false;
protected static bool $timestamps = true;
protected static array $fillable = [
'service_code', 'name_ar', 'name_en', 'price_type', 'base_amount',
'percentage', 'annual_amount', 'currency', 'applies_to', 'branch_id',
'effective_from', 'effective_to', 'is_active',
];
public static function allActive(): array
{
$db = App::getInstance()->db();
return $db->select("
SELECT sc.*, b.name_ar as branch_name
FROM service_catalog sc
LEFT JOIN branches b ON b.id = sc.branch_id
WHERE sc.is_active = 1
ORDER BY sc.service_code
");
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/catalog', 'ServiceCatalog\Controllers\CatalogController@index', ['auth'], 'pricing.view'],
['GET', '/catalog/{id:\d+}/edit', 'ServiceCatalog\Controllers\CatalogController@edit', ['auth'], 'pricing.edit'],
['POST', '/catalog/{id:\d+}', 'ServiceCatalog\Controllers\CatalogController@update', ['auth', 'csrf'], 'pricing.edit'],
];
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل الخدمة: <?= e($service->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/catalog/<?= (int) $service->id ?>">
<?= csrf_field() ?>
<div class="card" style="padding:20px;">
<table style="width:100%;font-size:14px;margin-bottom:20px;">
<tr><td style="padding:6px 0;color:#6B7280;width:30%;">كود الخدمة</td><td><code><?= e($service->service_code) ?></code></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الاسم</td><td><?= e($service->name_ar) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">نوع السعر</td><td><?= e($service->price_type) ?></td></tr>
</table>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">المبلغ الأساسي (ج.م)</label>
<input type="number" name="base_amount" value="<?= e($service->base_amount ?? '') ?>" class="form-input" step="0.01" min="0">
</div>
<div class="form-group">
<label class="form-label">النسبة (%)</label>
<input type="number" name="percentage" value="<?= e($service->percentage ?? '') ?>" class="form-input" step="0.01" min="0" max="100">
</div>
<div class="form-group">
<label class="form-label">المبلغ السنوي (ج.م)</label>
<input type="number" name="annual_amount" value="<?= e($service->annual_amount ?? '') ?>" class="form-input" step="0.01" min="0">
</div>
</div>
</div>
<button type="submit" class="btn btn-primary" style="margin-top:20px;">حفظ</button>
<a href="/catalog" class="btn btn-outline" style="margin-top:20px;">إلغاء</a>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>كتالوج الخدمات<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr><th>الكود</th><th>الخدمة</th><th>النوع</th><th>المبلغ الأساسي</th><th>النسبة</th><th>سنوي</th><th>الفرع</th><th>الإجراءات</th></tr>
</thead>
<tbody>
<?php foreach ($services as $s): ?>
<tr>
<td><code style="font-size:11px;"><?= e($s['service_code']) ?></code></td>
<td><?= e($s['name_ar']) ?></td>
<td><span style="background:#F3F4F6;padding:2px 8px;border-radius:4px;font-size:12px;"><?= e($s['price_type']) ?></span></td>
<td style="direction:ltr;text-align:right;"><?= $s['base_amount'] ? money($s['base_amount']) : '—' ?></td>
<td><?= $s['percentage'] ? percentage($s['percentage']) : '—' ?></td>
<td style="direction:ltr;text-align:right;"><?= $s['annual_amount'] ? money($s['annual_amount']) : '—' ?></td>
<td style="font-size:12px;"><?= e($s['branch_name'] ?? 'عام') ?></td>
<td><a href="/catalog/<?= (int) $s['id'] ?>/edit" class="btn btn-sm btn-outline">تعديل</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($services)): ?>
<tr><td colspan="8" style="text-align:center;padding:40px;color:#6B7280;">لا توجد خدمات</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
// Menu and permissions registered in Rules/bootstrap.php (shared group)
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `business_rules` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`rule_code` VARCHAR(100) NOT NULL,
`category` VARCHAR(50) NOT NULL,
`name_ar` VARCHAR(300) NOT NULL,
`name_en` VARCHAR(300) NULL,
`description_ar` TEXT NULL,
`description_en` TEXT NULL,
`parameters_json` JSON NULL,
`current_value_json` JSON NOT NULL,
`data_type` VARCHAR(50) NOT NULL DEFAULT 'string',
`branch_id` BIGINT UNSIGNED NULL,
`effective_from` DATE NOT NULL,
`effective_to` DATE NULL DEFAULT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`version` INT UNSIGNED NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_business_rules_code_branch` (`rule_code`, `branch_id`),
INDEX `idx_business_rules_category` (`category`),
INDEX `idx_business_rules_branch` (`branch_id`),
INDEX `idx_business_rules_active` (`is_active`),
INDEX `idx_business_rules_effective` (`effective_from`, `effective_to`),
CONSTRAINT `fk_business_rules_branch` FOREIGN KEY (`branch_id`) REFERENCES `branches`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `business_rules`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `rule_versions` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`rule_id` BIGINT UNSIGNED NOT NULL,
`version_number` INT UNSIGNED NOT NULL,
`old_value_json` JSON NULL,
`new_value_json` JSON NOT NULL,
`changed_by` BIGINT UNSIGNED NULL,
`changed_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`change_reason` TEXT NULL,
INDEX `idx_rule_versions_rule` (`rule_id`),
CONSTRAINT `fk_rule_versions_rule` FOREIGN KEY (`rule_id`) REFERENCES `business_rules`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `rule_versions`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `pricing_configs` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`branch_id` BIGINT UNSIGNED NOT NULL,
`qualification_id` BIGINT UNSIGNED NOT NULL,
`membership_type` VARCHAR(50) NOT NULL DEFAULT 'working',
`price` DECIMAL(15,2) NOT NULL,
`currency` VARCHAR(3) NOT NULL DEFAULT 'EGP',
`effective_from` DATE NOT NULL,
`effective_to` DATE NULL DEFAULT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
INDEX `idx_pricing_branch` (`branch_id`),
INDEX `idx_pricing_qual` (`qualification_id`),
INDEX `idx_pricing_type` (`membership_type`),
INDEX `idx_pricing_effective` (`effective_from`, `effective_to`),
CONSTRAINT `fk_pricing_branch` FOREIGN KEY (`branch_id`) REFERENCES `branches`(`id`),
CONSTRAINT `fk_pricing_qual` FOREIGN KEY (`qualification_id`) REFERENCES `qualifications`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `pricing_configs`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `service_catalog` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`service_code` VARCHAR(50) NOT NULL,
`name_ar` VARCHAR(200) NOT NULL,
`name_en` VARCHAR(200) NULL,
`price_type` VARCHAR(50) NOT NULL DEFAULT 'fixed',
`base_amount` DECIMAL(15,2) NULL,
`percentage` DECIMAL(5,2) NULL,
`annual_amount` DECIMAL(15,2) NULL,
`currency` VARCHAR(3) NOT NULL DEFAULT 'EGP',
`applies_to` VARCHAR(100) NULL,
`branch_id` BIGINT UNSIGNED NULL,
`effective_from` DATE NOT NULL,
`effective_to` DATE NULL DEFAULT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_service_catalog_code_branch` (`service_code`, `branch_id`),
INDEX `idx_service_catalog_active` (`is_active`),
CONSTRAINT `fk_service_catalog_branch` FOREIGN KEY (`branch_id`) REFERENCES `branches`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `service_catalog`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `discount_rules` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`discount_code` VARCHAR(50) NOT NULL,
`name_ar` VARCHAR(200) NOT NULL,
`name_en` VARCHAR(200) NULL,
`discount_type` VARCHAR(30) NOT NULL DEFAULT 'percentage',
`value` DECIMAL(15,2) NOT NULL,
`min_quantity` INT UNSIGNED NULL,
`max_quantity` INT UNSIGNED NULL,
`stackable` TINYINT(1) NOT NULL DEFAULT 0,
`max_discount_percentage` DECIMAL(5,2) NULL,
`branch_id` BIGINT UNSIGNED NULL,
`effective_from` DATE NOT NULL,
`effective_to` DATE NULL DEFAULT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
INDEX `idx_discount_rules_code` (`discount_code`),
INDEX `idx_discount_rules_active` (`is_active`),
CONSTRAINT `fk_discount_rules_branch` FOREIGN KEY (`branch_id`) REFERENCES `branches`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `discount_rules`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `rule_overrides` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`rule_id` BIGINT UNSIGNED NOT NULL,
`entity_type` VARCHAR(100) NOT NULL,
`entity_id` BIGINT UNSIGNED NOT NULL,
`override_value_json` JSON NOT NULL,
`reason` TEXT NOT NULL,
`approved_by` BIGINT UNSIGNED NULL,
`effective_from` DATE NOT NULL,
`effective_to` DATE NULL DEFAULT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_rule_overrides_rule` (`rule_id`),
INDEX `idx_rule_overrides_entity` (`entity_type`, `entity_id`),
CONSTRAINT `fk_rule_overrides_rule` FOREIGN KEY (`rule_id`) REFERENCES `business_rules`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `rule_overrides`",
];
\ No newline at end of file
This diff is collapsed.
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
$branches = $db->select("SELECT id, branch_code FROM branches");
$quals = $db->select("SELECT id, code FROM qualifications");
if (empty($branches) || empty($quals)) {
return;
}
$branchMap = [];
foreach ($branches as $b) {
$branchMap[$b['branch_code']] = (int) $b['id'];
}
$qualMap = [];
foreach ($quals as $q) {
$qualMap[$q['code']] = (int) $q['id'];
}
// Current prices from 1/7/2024
$prices = [
// Sheraton
['branch' => 'sheraton', 'qual' => 'high', 'price' => '150000.00', 'from' => '2024-07-01'],
['branch' => 'sheraton', 'qual' => 'medium', 'price' => '225000.00', 'from' => '2024-07-01'],
['branch' => 'sheraton', 'qual' => 'none', 'price' => '300000.00', 'from' => '2024-07-01'],
// 6th October
['branch' => 'sadis', 'qual' => 'high', 'price' => '150000.00', 'from' => '2024-07-01'],
['branch' => 'sadis', 'qual' => 'medium', 'price' => '225000.00', 'from' => '2024-07-01'],
['branch' => 'sadis', 'qual' => 'none', 'price' => '300000.00', 'from' => '2024-07-01'],
// New Capital
['branch' => 'new_capital', 'qual' => 'high', 'price' => '150000.00', 'from' => '2024-07-01'],
['branch' => 'new_capital', 'qual' => 'medium', 'price' => '225000.00', 'from' => '2024-07-01'],
['branch' => 'new_capital', 'qual' => 'none', 'price' => '300000.00', 'from' => '2024-07-01'],
// Previous prices before 1/7/2024
['branch' => 'sheraton', 'qual' => 'high', 'price' => '114000.00', 'from' => '2020-01-01', 'to' => '2024-06-30'],
['branch' => 'sheraton', 'qual' => 'medium', 'price' => '171000.00', 'from' => '2020-01-01', 'to' => '2024-06-30'],
['branch' => 'sheraton', 'qual' => 'none', 'price' => '228000.00', 'from' => '2020-01-01', 'to' => '2024-06-30'],
];
foreach ($prices as $p) {
$branchId = $branchMap[$p['branch']] ?? null;
$qualId = $qualMap[$p['qual']] ?? null;
if (!$branchId || !$qualId) {
continue;
}
$existing = $db->selectOne(
"SELECT id FROM pricing_configs WHERE branch_id = ? AND qualification_id = ? AND effective_from = ? AND membership_type = 'working'",
[$branchId, $qualId, $p['from']]
);
if ($existing) {
continue;
}
$db->insert('pricing_configs', [
'branch_id' => $branchId,
'qualification_id' => $qualId,
'membership_type' => 'working',
'price' => $p['price'],
'currency' => 'EGP',
'effective_from' => $p['from'],
'effective_to' => $p['to'] ?? null,
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]);
}
};
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
$now = date('Y-m-d');
$services = [
['code' => 'SVC_NEW_FORM', 'name_ar' => 'استمارة عضوية جديدة', 'name_en' => 'New Membership Form', 'price_type' => 'fixed', 'base_amount' => '505.00'],
['code' => 'SVC_TRANSFER_FORM', 'name_ar' => 'استمارة تحويل / فصل', 'name_en' => 'Transfer/Separation Form', 'price_type' => 'fixed', 'base_amount' => '570.00'],
['code' => 'SVC_ADDITION_FORM', 'name_ar' => 'استمارة إضافة', 'name_en' => 'Addition Form', 'price_type' => 'fixed', 'base_amount' => '570.00'],
['code' => 'SVC_DEV_FEE', 'name_ar' => 'رسوم تنمية', 'name_en' => 'Development Fee', 'price_type' => 'fixed', 'base_amount' => '35.00'],
['code' => 'SVC_MARTYRS_STAMP', 'name_ar' => 'طابع شهداء', 'name_en' => 'Martyrs Stamp', 'price_type' => 'fixed', 'base_amount' => '5.00'],
['code' => 'SVC_CARNET_REPLACE', 'name_ar' => 'بدل فاقد كارنيه', 'name_en' => 'Carnet Replacement', 'price_type' => 'fixed', 'base_amount' => '200.00'],
['code' => 'SVC_SEASONAL', 'name_ar' => 'عضوية موسمية', 'name_en' => 'Seasonal Membership', 'price_type' => 'percentage', 'percentage' => '5.00'],
['code' => 'SVC_TEMP_MEMBER', 'name_ar' => 'رسوم عضو مؤقت', 'name_en' => 'Temporary Member Fee', 'price_type' => 'percentage', 'percentage' => '10.00'],
['code' => 'SVC_CHILD_4TH', 'name_ar' => 'رسوم ابن رابع', 'name_en' => '4th Child Fee', 'price_type' => 'percentage', 'percentage' => '5.00'],
['code' => 'SVC_CHILD_18', 'name_ar' => 'رسوم ابن 18 سنة', 'name_en' => 'Child 18 Fee', 'price_type' => 'percentage', 'percentage' => '10.00'],
['code' => 'SVC_CHILD_19', 'name_ar' => 'رسوم ابن 19 سنة', 'name_en' => 'Child 19 Fee', 'price_type' => 'percentage', 'percentage' => '15.00'],
['code' => 'SVC_CHILD_20', 'name_ar' => 'رسوم ابن 20 سنة', 'name_en' => 'Child 20 Fee', 'price_type' => 'percentage', 'percentage' => '20.00'],
['code' => 'SVC_CHILD_21', 'name_ar' => 'رسوم ابن 21 سنة', 'name_en' => 'Child 21 Fee', 'price_type' => 'percentage', 'percentage' => '15.00'],
['code' => 'SVC_SPOUSE_2ND', 'name_ar' => 'رسوم زوجة ثانية', 'name_en' => '2nd Spouse Fee', 'price_type' => 'percentage_plus_annual', 'percentage' => '10.00', 'annual_amount' => '150.00'],
['code' => 'SVC_SPOUSE_3RD', 'name_ar' => 'رسوم زوجة ثالثة', 'name_en' => '3rd Spouse Fee', 'price_type' => 'percentage_plus_annual', 'percentage' => '20.00', 'annual_amount' => '200.00'],
['code' => 'SVC_SPOUSE_4TH', 'name_ar' => 'رسوم زوجة رابعة', 'name_en' => '4th Spouse Fee', 'price_type' => 'percentage_plus_annual', 'percentage' => '30.00', 'annual_amount' => '300.00'],
['code' => 'SVC_SPOUSE_FOREIGN', 'name_ar' => 'رسوم زوج أجنبي', 'name_en' => 'Foreign Spouse Fee', 'price_type' => 'percentage', 'percentage' => '15.00'],
['code' => 'SVC_SPOUSE_ACQUIRED', 'name_ar' => 'رسوم زوج مكتسب', 'name_en' => 'Acquired Spouse Fee', 'price_type' => 'percentage', 'percentage' => '50.00'],
['code' => 'SVC_SPOUSE_BASE', 'name_ar' => 'رسوم زوج أساسي', 'name_en' => 'Base Spouse Fee', 'price_type' => 'percentage', 'percentage' => '15.00'],
['code' => 'SVC_WAIVER', 'name_ar' => 'رسوم تنازل', 'name_en' => 'Waiver Fee', 'price_type' => 'percentage', 'percentage' => '30.00'],
['code' => 'SVC_SPORTS_CONV', 'name_ar' => 'رسوم تحويل رياضي', 'name_en' => 'Sports Conversion Fee', 'price_type' => 'percentage', 'percentage' => '50.00'],
['code' => 'SVC_ANNUAL_MEMBER', 'name_ar' => 'اشتراك سنوي - عضو', 'name_en' => 'Annual Sub - Member', 'price_type' => 'fixed', 'base_amount' => '492.00'],
['code' => 'SVC_ANNUAL_SPOUSE', 'name_ar' => 'اشتراك سنوي - زوجة', 'name_en' => 'Annual Sub - Spouse', 'price_type' => 'fixed', 'base_amount' => '492.00'],
['code' => 'SVC_ANNUAL_CHILD', 'name_ar' => 'اشتراك سنوي - ابن/ابنة', 'name_en' => 'Annual Sub - Child', 'price_type' => 'fixed', 'base_amount' => '222.00'],
['code' => 'SVC_ANNUAL_TEMP', 'name_ar' => 'اشتراك سنوي - مؤقت', 'name_en' => 'Annual Sub - Temp', 'price_type' => 'fixed', 'base_amount' => '222.00'],
];
foreach ($services as $s) {
$existing = $db->selectOne("SELECT id FROM service_catalog WHERE service_code = ? AND branch_id IS NULL", [$s['code']]);
if ($existing) {
continue;
}
$db->insert('service_catalog', [
'service_code' => $s['code'],
'name_ar' => $s['name_ar'],
'name_en' => $s['name_en'] ?? null,
'price_type' => $s['price_type'],
'base_amount' => $s['base_amount'] ?? null,
'percentage' => $s['percentage'] ?? null,
'annual_amount' => $s['annual_amount'] ?? null,
'currency' => 'EGP',
'applies_to' => $s['applies_to'] ?? null,
'branch_id' => null,
'effective_from' => $now,
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]);
}
};
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment