Commit 6291dcf2 authored by Mahmoud Aglan's avatar Mahmoud Aglan

test

parent 0b418d73
...@@ -65,4 +65,6 @@ return [ ...@@ -65,4 +65,6 @@ return [
['GET', '/accounting/reports/accounts-receivable', 'Accounting\Controllers\ReportController@accountsReceivable', ['auth'], 'accounting.reports.ar'], ['GET', '/accounting/reports/accounts-receivable', 'Accounting\Controllers\ReportController@accountsReceivable', ['auth'], 'accounting.reports.ar'],
['GET', '/accounting/reports/accounts-payable', 'Accounting\Controllers\ReportController@accountsPayable', ['auth'], 'accounting.reports.ap'], ['GET', '/accounting/reports/accounts-payable', 'Accounting\Controllers\ReportController@accountsPayable', ['auth'], 'accounting.reports.ap'],
['GET', '/accounting/reports/member-statement', 'Accounting\Controllers\ReportController@memberStatement', ['auth'], 'accounting.reports.member_statement'], ['GET', '/accounting/reports/member-statement', 'Accounting\Controllers\ReportController@memberStatement', ['auth'], 'accounting.reports.member_statement'],
['GET', '/accounting/reports/treasury', 'Accounting\Controllers\ReportController@treasury', ['auth'], 'accounting.reports.treasury'],
['GET', '/accounting/reports/revenue-analysis', 'Accounting\Controllers\ReportController@revenueAnalysis', ['auth'], 'accounting.reports.revenue_analysis'],
]; ];
This diff is collapsed.
This diff is collapsed.
...@@ -21,6 +21,8 @@ PermissionRegistry::register('accounting', [ ...@@ -21,6 +21,8 @@ PermissionRegistry::register('accounting', [
'accounting.reports.ar' => ['ar' => 'تقرير المدينين', 'en' => 'Accounts Receivable Report'], 'accounting.reports.ar' => ['ar' => 'تقرير المدينين', 'en' => 'Accounts Receivable Report'],
'accounting.reports.ap' => ['ar' => 'تقرير الدائنين', 'en' => 'Accounts Payable Report'], 'accounting.reports.ap' => ['ar' => 'تقرير الدائنين', 'en' => 'Accounts Payable Report'],
'accounting.reports.member_statement' => ['ar' => 'كشف حساب عضو', 'en' => 'Member Statement'], 'accounting.reports.member_statement' => ['ar' => 'كشف حساب عضو', 'en' => 'Member Statement'],
'accounting.reports.treasury' => ['ar' => 'تقرير الخزينة والمدفوعات', 'en' => 'Treasury & Payments Report'],
'accounting.reports.revenue_analysis' => ['ar' => 'تحليل الإيرادات', 'en' => 'Revenue Analysis'],
// Fiscal Year // Fiscal Year
'accounting.fiscal_year.view' => ['ar' => 'عرض السنوات المالية', 'en' => 'View Fiscal Years'], 'accounting.fiscal_year.view' => ['ar' => 'عرض السنوات المالية', 'en' => 'View Fiscal Years'],
...@@ -84,6 +86,8 @@ MenuRegistry::register('accounting', [ ...@@ -84,6 +86,8 @@ MenuRegistry::register('accounting', [
['label_ar' => 'المدينون (AR)', 'label_en' => 'Accounts Receivable', 'route' => '/accounting/reports/accounts-receivable', 'permission' => 'accounting.reports.ar', 'order' => 14], ['label_ar' => 'المدينون (AR)', 'label_en' => 'Accounts Receivable', 'route' => '/accounting/reports/accounts-receivable', 'permission' => 'accounting.reports.ar', 'order' => 14],
['label_ar' => 'الدائنون (AP)', 'label_en' => 'Accounts Payable', 'route' => '/accounting/reports/accounts-payable', 'permission' => 'accounting.reports.ap', 'order' => 15], ['label_ar' => 'الدائنون (AP)', 'label_en' => 'Accounts Payable', 'route' => '/accounting/reports/accounts-payable', 'permission' => 'accounting.reports.ap', 'order' => 15],
['label_ar' => 'كشف حساب عضو', 'label_en' => 'Member Statement', 'route' => '/accounting/reports/member-statement', 'permission' => 'accounting.reports.member_statement','order' => 16], ['label_ar' => 'كشف حساب عضو', 'label_en' => 'Member Statement', 'route' => '/accounting/reports/member-statement', 'permission' => 'accounting.reports.member_statement','order' => 16],
['label_ar' => 'الخزينة والمدفوعات', 'label_en' => 'Treasury & Payments', 'route' => '/accounting/reports/treasury', 'permission' => 'accounting.reports.treasury', 'order' => 17],
['label_ar' => 'تحليل الإيرادات', 'label_en' => 'Revenue Analysis', 'route' => '/accounting/reports/revenue-analysis', 'permission' => 'accounting.reports.revenue_analysis','order' => 18],
], ],
]); ]);
......
...@@ -129,6 +129,7 @@ class ActivitySubscriptionController extends Controller ...@@ -129,6 +129,7 @@ class ActivitySubscriptionController extends Controller
} else { } else {
$result = InventoryPaymentService::processGuestPayment([ $result = InventoryPaymentService::processGuestPayment([
'amount' => $amount, 'amount' => $amount,
'payment_type' => 'activity_subscription',
'payment_method' => $paymentMethod, 'payment_method' => $paymentMethod,
'guest_name' => $sub['player_name'] ?? 'لاعب', 'guest_name' => $sub['player_name'] ?? 'لاعب',
'related_entity_type' => 'activity_subscriptions', 'related_entity_type' => 'activity_subscriptions',
......
...@@ -10,6 +10,7 @@ use App\Core\App; ...@@ -10,6 +10,7 @@ use App\Core\App;
use App\Core\EventBus; use App\Core\EventBus;
use App\Modules\Foreign\Models\ForeignMemberDetail; use App\Modules\Foreign\Models\ForeignMemberDetail;
use App\Modules\Rules\Services\RuleEngine; use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Payments\Services\PaymentService;
class ForeignController extends Controller class ForeignController extends Controller
{ {
...@@ -108,6 +109,27 @@ class ForeignController extends Controller ...@@ -108,6 +109,27 @@ class ForeignController extends Controller
$db->update('members', ['membership_type' => 'foreign', 'updated_at' => date('Y-m-d H:i:s')], '`id` = ?', [(int) $memberId]); $db->update('members', ['membership_type' => 'foreign', 'updated_at' => date('Y-m-d H:i:s')], '`id` = ?', [(int) $memberId]);
// Process payment through central PaymentService (use EGP amount if available, otherwise USD)
$chargeAmount = $feeEgp ?? $feeUsd;
$chargeCurrency = $feeEgp ? 'EGP' : 'USD';
if (bccomp((string) $chargeAmount, '0.00', 2) > 0) {
$payResult = PaymentService::processPayment([
'member_id' => (int) $memberId,
'amount' => (string) $chargeAmount,
'payment_type' => 'membership_fee',
'payment_method' => $data['payment_method'] ?? 'cash',
'currency' => $chargeCurrency,
'related_entity_type' => 'foreign_member_details',
'related_entity_id' => (int) $foreign->id,
'description' => 'رسوم عضوية أجنبية — ' . number_format((float) $feeUsd, 2) . ' USD',
]);
if (!$payResult['success']) {
return $this->redirect("/members/{$memberId}")
->withError('تم تسجيل العضوية الأجنبية لكن فشل تسجيل الدفع: ' . ($payResult['error'] ?? ''));
}
}
EventBus::dispatch('foreign.registered', ['member_id' => (int) $memberId, 'foreign_id' => (int) $foreign->id]); EventBus::dispatch('foreign.registered', ['member_id' => (int) $memberId, 'foreign_id' => (int) $foreign->id]);
return $this->redirect("/members/{$memberId}")->withSuccess('تم تسجيل العضوية الأجنبية — الرسوم: ' . number_format((float) $feeUsd, 2) . ' USD'); return $this->redirect("/members/{$memberId}")->withSuccess('تم تسجيل العضوية الأجنبية — الرسوم: ' . number_format((float) $feeUsd, 2) . ' USD');
......
...@@ -13,6 +13,7 @@ use App\Modules\HR\Models\HrPayrollRun; ...@@ -13,6 +13,7 @@ use App\Modules\HR\Models\HrPayrollRun;
use App\Modules\HR\Models\HrEmployeeProfile; use App\Modules\HR\Models\HrEmployeeProfile;
use App\Modules\HR\Services\PayrollCalculationService; use App\Modules\HR\Services\PayrollCalculationService;
use App\Modules\HR\Services\SalarySlipService; use App\Modules\HR\Services\SalarySlipService;
use App\Core\EventBus;
class PayrollController extends Controller class PayrollController extends Controller
{ {
...@@ -197,6 +198,19 @@ class PayrollController extends Controller ...@@ -197,6 +198,19 @@ class PayrollController extends Controller
$db->commit(); $db->commit();
// Dispatch payroll paid event for each run — triggers Accounting auto-post
$runs = $db->select(
"SELECT id FROM hr_payroll_runs WHERE period_id = ? AND is_archived = 0",
[(int) $id]
);
foreach ($runs as $run) {
EventBus::dispatch('hr.payroll.paid', [
'payroll_run_id' => (int) $run['id'],
'period_id' => (int) $id,
'payment_date' => $paymentDate,
]);
}
Logger::info("Payroll period marked paid", ['period_id' => (int) $id, 'payment_date' => $paymentDate]); Logger::info("Payroll period marked paid", ['period_id' => (int) $id, 'payment_date' => $paymentDate]);
return $this->redirect('/hr/payroll/periods/' . $id)->withSuccess('تم صرف الرواتب بنجاح'); return $this->redirect('/hr/payroll/periods/' . $id)->withSuccess('تم صرف الرواتب بنجاح');
......
...@@ -41,15 +41,13 @@ final class PaymentService ...@@ -41,15 +41,13 @@ final class PaymentService
$employee = App::getInstance()->currentEmployee(); $employee = App::getInstance()->currentEmployee();
// Validate required fields // Validate required fields
$memberId = (int) ($data['member_id'] ?? 0); $memberId = isset($data['member_id']) && $data['member_id'] !== null ? (int) $data['member_id'] : null;
$amount = $data['amount'] ?? '0.00'; $amount = $data['amount'] ?? '0.00';
$paymentType = $data['payment_type'] ?? ''; $paymentType = $data['payment_type'] ?? '';
$paymentMethod = $data['payment_method'] ?? 'cash'; $paymentMethod = $data['payment_method'] ?? 'cash';
$description = $data['description'] ?? ''; $description = $data['description'] ?? '';
$guestName = $data['guest_name'] ?? null;
if ($memberId <= 0) {
return ['success' => false, 'error' => 'العضو غير محدد'];
}
if (bccomp((string) $amount, '0.01', 2) < 0) { if (bccomp((string) $amount, '0.01', 2) < 0) {
return ['success' => false, 'error' => 'المبلغ يجب أن يكون أكبر من صفر']; return ['success' => false, 'error' => 'المبلغ يجب أن يكون أكبر من صفر'];
} }
...@@ -57,10 +55,13 @@ final class PaymentService ...@@ -57,10 +55,13 @@ final class PaymentService
return ['success' => false, 'error' => 'نوع الدفعة مطلوب']; return ['success' => false, 'error' => 'نوع الدفعة مطلوب'];
} }
// Verify member exists // Verify member exists (skip for guest payments)
$member = $db->selectOne("SELECT id, full_name_ar, form_number FROM members WHERE id = ? AND is_archived = 0", [$memberId]); $member = null;
if (!$member) { if ($memberId !== null && $memberId > 0) {
return ['success' => false, 'error' => 'العضو غير موجود']; $member = $db->selectOne("SELECT id, full_name_ar, form_number FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$member) {
return ['success' => false, 'error' => 'العضو غير موجود'];
}
} }
$db->beginTransaction(); $db->beginTransaction();
...@@ -70,7 +71,7 @@ final class PaymentService ...@@ -70,7 +71,7 @@ final class PaymentService
// Create payment record // Create payment record
$paymentId = $db->insert('payments', [ $paymentId = $db->insert('payments', [
'member_id' => $memberId, 'member_id' => ($memberId !== null && $memberId > 0) ? $memberId : null,
'payment_type' => $paymentType, 'payment_type' => $paymentType,
'amount' => $amount, 'amount' => $amount,
'currency' => $data['currency'] ?? 'EGP', 'currency' => $data['currency'] ?? 'EGP',
...@@ -84,7 +85,7 @@ final class PaymentService ...@@ -84,7 +85,7 @@ final class PaymentService
'transfer_bank' => $data['transfer_bank'] ?? null, 'transfer_bank' => $data['transfer_bank'] ?? null,
'related_entity_type' => $data['related_entity_type'] ?? null, 'related_entity_type' => $data['related_entity_type'] ?? null,
'related_entity_id' => $data['related_entity_id'] ?? null, 'related_entity_id' => $data['related_entity_id'] ?? null,
'notes' => $data['notes'] ?? $description, 'notes' => $data['notes'] ?? ($guestName ? $description . ' — ' . $guestName : $description),
'payment_date' => $data['payment_date'] ?? date('Y-m-d'), 'payment_date' => $data['payment_date'] ?? date('Y-m-d'),
'received_by_employee_id' => $employee ? (int) $employee->id : null, 'received_by_employee_id' => $employee ? (int) $employee->id : null,
'is_voided' => 0, 'is_voided' => 0,
...@@ -96,7 +97,7 @@ final class PaymentService ...@@ -96,7 +97,7 @@ final class PaymentService
// Auto-generate description if empty // Auto-generate description if empty
if ($description === '') { if ($description === '') {
$description = self::getPaymentTypeLabel($paymentType); $description = self::getPaymentTypeLabel($paymentType);
if ($member['form_number']) { if ($member && $member['form_number']) {
$description .= ' — استمارة ' . $member['form_number']; $description .= ' — استمارة ' . $member['form_number'];
} }
} }
...@@ -104,7 +105,7 @@ final class PaymentService ...@@ -104,7 +105,7 @@ final class PaymentService
// Create receipt // Create receipt
$receiptId = $db->insert('receipts', [ $receiptId = $db->insert('receipts', [
'receipt_number' => $receiptNumber, 'receipt_number' => $receiptNumber,
'member_id' => $memberId, 'member_id' => ($memberId !== null && $memberId > 0) ? $memberId : null,
'payment_id' => $paymentId, 'payment_id' => $paymentId,
'receipt_type' => 'payment', 'receipt_type' => 'payment',
'amount' => $amount, 'amount' => $amount,
...@@ -127,7 +128,7 @@ final class PaymentService ...@@ -127,7 +128,7 @@ final class PaymentService
'payment_id' => $paymentId, 'payment_id' => $paymentId,
'receipt_id' => $receiptId, 'receipt_id' => $receiptId,
'receipt_number' => $receiptNumber, 'receipt_number' => $receiptNumber,
'member_id' => $memberId, 'member_id' => ($memberId !== null && $memberId > 0) ? $memberId : 0,
'type' => $paymentType, 'type' => $paymentType,
'amount' => $amount, 'amount' => $amount,
'method' => $paymentMethod, 'method' => $paymentMethod,
...@@ -135,7 +136,7 @@ final class PaymentService ...@@ -135,7 +136,7 @@ final class PaymentService
Logger::info("Payment processed", [ Logger::info("Payment processed", [
'payment_id' => $paymentId, 'payment_id' => $paymentId,
'member_id' => $memberId, 'member_id' => $memberId ?? 0,
'type' => $paymentType, 'type' => $paymentType,
'amount' => $amount, 'amount' => $amount,
]); ]);
...@@ -151,7 +152,7 @@ final class PaymentService ...@@ -151,7 +152,7 @@ final class PaymentService
} catch (\Throwable $e) { } catch (\Throwable $e) {
$db->rollBack(); $db->rollBack();
Logger::error("Payment failed: " . $e->getMessage(), [ Logger::error("Payment failed: " . $e->getMessage(), [
'member_id' => $memberId, 'member_id' => $memberId ?? 0,
'type' => $paymentType, 'type' => $paymentType,
'amount' => $amount, 'amount' => $amount,
]); ]);
...@@ -311,7 +312,8 @@ final class PaymentService ...@@ -311,7 +312,8 @@ final class PaymentService
'carnet_replacement' => 'بدل فاقد كارنيه', 'carnet_replacement' => 'بدل فاقد كارنيه',
'seasonal_fee' => 'رسوم عضوية موسمية', 'seasonal_fee' => 'رسوم عضوية موسمية',
'sports_conversion' => 'رسوم تحويل رياضي', 'sports_conversion' => 'رسوم تحويل رياضي',
'inventory_sale' => 'مبيعات مخزون', 'inventory_sale' => 'مبيعات مخزون',
'activity_subscription' => 'اشتراك نشاط',
'other' => 'أخرى', 'other' => 'أخرى',
default => $type, default => $type,
}; };
......
...@@ -3,108 +3,29 @@ declare(strict_types=1); ...@@ -3,108 +3,29 @@ declare(strict_types=1);
namespace App\Modules\Sales\Services; namespace App\Modules\Sales\Services;
use App\Core\App; use App\Modules\Payments\Services\PaymentService;
use App\Core\EventBus;
use App\Core\Logger;
/** /**
* Guest payment wrapper — bypasses the member_id validation in PaymentService. * Guest payment wrapper — delegates to PaymentService with member_id = null.
* Creates payment + receipt directly for guest sales. * Kept as a thin adapter so callers don't need to change.
*/ */
final class InventoryPaymentService final class InventoryPaymentService
{ {
/** /**
* Process a guest payment (no member_id required). * Process a guest payment (no member_id required).
* Delegates entirely to the central PaymentService.
*/ */
public static function processGuestPayment(array $data): array public static function processGuestPayment(array $data): array
{ {
$db = App::getInstance()->db(); return PaymentService::processPayment([
$employee = App::getInstance()->currentEmployee(); 'member_id' => null,
'amount' => $data['amount'] ?? '0.00',
$amount = $data['amount'] ?? '0.00'; 'payment_type' => $data['payment_type'] ?? 'inventory_sale',
$paymentMethod = $data['payment_method'] ?? 'cash'; 'payment_method' => $data['payment_method'] ?? 'cash',
$guestName = $data['guest_name'] ?? 'زائر'; 'guest_name' => $data['guest_name'] ?? 'زائر',
$description = $data['description'] ?? 'مبيعات مخزون (زائر)'; 'related_entity_type' => $data['related_entity_type'] ?? null,
'related_entity_id' => $data['related_entity_id'] ?? null,
if (bccomp((string) $amount, '0.01', 2) < 0) { 'description' => $data['description'] ?? 'مبيعات مخزون (زائر)',
return ['success' => false, 'error' => 'المبلغ يجب أن يكون أكبر من صفر']; ]);
}
$db->beginTransaction();
try {
// Generate receipt number (same pattern as PaymentService)
$year = date('Y');
$prefix = 'REC-' . $year . '-';
$last = $db->selectOne(
"SELECT receipt_number FROM receipts WHERE receipt_number LIKE ? ORDER BY id DESC LIMIT 1",
[$prefix . '%']
);
$seq = $last ? ((int) substr($last['receipt_number'], -6)) + 1 : 1;
$receiptNumber = $prefix . str_pad((string) $seq, 6, '0', STR_PAD_LEFT);
// Create payment record (member_id = NULL for guest)
$paymentId = $db->insert('payments', [
'member_id' => null,
'payment_type' => 'inventory_sale',
'amount' => $amount,
'currency' => 'EGP',
'payment_method' => $paymentMethod,
'related_entity_type' => $data['related_entity_type'] ?? null,
'related_entity_id' => $data['related_entity_id'] ?? null,
'notes' => $description . ' — ' . $guestName,
'payment_date' => date('Y-m-d'),
'received_by_employee_id' => $employee ? (int) $employee->id : null,
'is_voided' => 0,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
]);
// Create receipt (member_id = NULL — requires Phase_25_021 migration)
$receiptId = $db->insert('receipts', [
'receipt_number' => $receiptNumber,
'member_id' => null,
'payment_id' => $paymentId,
'receipt_type' => 'payment',
'amount' => $amount,
'amount_in_words_ar' => function_exists('number_to_arabic_words') ? number_to_arabic_words((float) $amount) : '',
'description_ar' => $description,
'issued_by_employee_id' => $employee ? (int) $employee->id : null,
'issued_at' => date('Y-m-d H:i:s'),
'is_voided' => 0,
'print_count' => 0,
'created_at' => date('Y-m-d H:i:s'),
]);
$db->update('payments', ['receipt_id' => $receiptId], '`id` = ?', [$paymentId]);
$db->commit();
// Fire payment.completed so Accounting module auto-posts GL entries
EventBus::dispatch('payment.completed', [
'payment_id' => $paymentId,
'receipt_id' => $receiptId,
'receipt_number' => $receiptNumber,
'member_id' => 0,
'type' => 'inventory_sale',
'amount' => $amount,
'method' => $paymentMethod,
]);
Logger::info("Guest payment #{$paymentId} processed — receipt: {$receiptNumber}");
return [
'success' => true,
'payment_id' => $paymentId,
'receipt_id' => $receiptId,
'receipt_number' => $receiptNumber,
'amount' => $amount,
];
} catch (\Throwable $e) {
$db->rollBack();
Logger::error("Guest payment failed: " . $e->getMessage());
return ['success' => false, 'error' => 'فشل تسجيل الدفع: ' . $e->getMessage()];
}
} }
} }
...@@ -10,6 +10,7 @@ use App\Core\App; ...@@ -10,6 +10,7 @@ use App\Core\App;
use App\Core\EventBus; use App\Core\EventBus;
use App\Modules\Seasonal\Models\SeasonalMembership; use App\Modules\Seasonal\Models\SeasonalMembership;
use App\Modules\Rules\Services\RuleEngine; use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Payments\Services\PaymentService;
class SeasonalController extends Controller class SeasonalController extends Controller
{ {
...@@ -109,6 +110,24 @@ class SeasonalController extends Controller ...@@ -109,6 +110,24 @@ class SeasonalController extends Controller
'notes' => $data['notes'] ?? null, 'notes' => $data['notes'] ?? null,
]); ]);
// Process payment through central PaymentService
if (bccomp($fee, '0.00', 2) > 0) {
$payResult = PaymentService::processPayment([
'member_id' => (int) $memberId,
'amount' => $fee,
'payment_type' => 'seasonal_fee',
'payment_method' => $data['payment_method'] ?? 'cash',
'related_entity_type' => 'seasonal_memberships',
'related_entity_id' => (int) $seasonal->id,
'description' => 'رسوم عضوية موسمية — من ' . $startDate . ' إلى ' . $endDate,
]);
if (!$payResult['success']) {
return $this->redirect("/members/{$memberId}/seasonal/create")
->withError('تم إنشاء العضوية الموسمية لكن فشل تسجيل الدفع: ' . ($payResult['error'] ?? ''));
}
}
EventBus::dispatch('seasonal.created', [ EventBus::dispatch('seasonal.created', [
'member_id' => (int) $memberId, 'member_id' => (int) $memberId,
'seasonal_id' => (int) $seasonal->id, 'seasonal_id' => (int) $seasonal->id,
......
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