Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
Clubphp
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Clubphp
Commits
6291dcf2
Commit
6291dcf2
authored
Apr 18, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
test
parent
0b418d73
Changes
12
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
1022 additions
and
127 deletions
+1022
-127
ReportController.php
app/Modules/Accounting/Controllers/ReportController.php
+349
-5
Routes.php
app/Modules/Accounting/Routes.php
+2
-0
index.php
app/Modules/Accounting/Views/dashboard/index.php
+141
-13
revenue_analysis.php
app/Modules/Accounting/Views/reports/revenue_analysis.php
+240
-0
treasury.php
app/Modules/Accounting/Views/reports/treasury.php
+198
-0
bootstrap.php
app/Modules/Accounting/bootstrap.php
+4
-0
ActivitySubscriptionController.php
...scriptions/Controllers/ActivitySubscriptionController.php
+1
-0
ForeignController.php
app/Modules/Foreign/Controllers/ForeignController.php
+22
-0
PayrollController.php
app/Modules/HR/Controllers/PayrollController.php
+14
-0
PaymentService.php
app/Modules/Payments/Services/PaymentService.php
+18
-16
InventoryPaymentService.php
app/Modules/Sales/Services/InventoryPaymentService.php
+14
-93
SeasonalController.php
app/Modules/Seasonal/Controllers/SeasonalController.php
+19
-0
No files found.
app/Modules/Accounting/Controllers/ReportController.php
View file @
6291dcf2
This diff is collapsed.
Click to expand it.
app/Modules/Accounting/Routes.php
View file @
6291dcf2
...
...
@@ -65,4 +65,6 @@ return [
[
'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/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'
],
];
app/Modules/Accounting/Views/dashboard/index.php
View file @
6291dcf2
This diff is collapsed.
Click to expand it.
app/Modules/Accounting/Views/reports/revenue_analysis.php
0 → 100644
View file @
6291dcf2
This diff is collapsed.
Click to expand it.
app/Modules/Accounting/Views/reports/treasury.php
0 → 100644
View file @
6291dcf2
This diff is collapsed.
Click to expand it.
app/Modules/Accounting/bootstrap.php
View file @
6291dcf2
...
...
@@ -21,6 +21,8 @@ PermissionRegistry::register('accounting', [
'accounting.reports.ar'
=>
[
'ar'
=>
'تقرير المدينين'
,
'en'
=>
'Accounts Receivable Report'
],
'accounting.reports.ap'
=>
[
'ar'
=>
'تقرير الدائنين'
,
'en'
=>
'Accounts Payable Report'
],
'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
'accounting.fiscal_year.view'
=>
[
'ar'
=>
'عرض السنوات المالية'
,
'en'
=>
'View Fiscal Years'
],
...
...
@@ -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'
=>
'الدائنون (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'
=>
'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
],
],
]);
...
...
app/Modules/ActivitySubscriptions/Controllers/ActivitySubscriptionController.php
View file @
6291dcf2
...
...
@@ -129,6 +129,7 @@ class ActivitySubscriptionController extends Controller
}
else
{
$result
=
InventoryPaymentService
::
processGuestPayment
([
'amount'
=>
$amount
,
'payment_type'
=>
'activity_subscription'
,
'payment_method'
=>
$paymentMethod
,
'guest_name'
=>
$sub
[
'player_name'
]
??
'لاعب'
,
'related_entity_type'
=>
'activity_subscriptions'
,
...
...
app/Modules/Foreign/Controllers/ForeignController.php
View file @
6291dcf2
...
...
@@ -10,6 +10,7 @@ use App\Core\App;
use
App\Core\EventBus
;
use
App\Modules\Foreign\Models\ForeignMemberDetail
;
use
App\Modules\Rules\Services\RuleEngine
;
use
App\Modules\Payments\Services\PaymentService
;
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
]);
// 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
]);
return
$this
->
redirect
(
"/members/
{
$memberId
}
"
)
->
withSuccess
(
'تم تسجيل العضوية الأجنبية — الرسوم: '
.
number_format
((
float
)
$feeUsd
,
2
)
.
' USD'
);
...
...
app/Modules/HR/Controllers/PayrollController.php
View file @
6291dcf2
...
...
@@ -13,6 +13,7 @@ use App\Modules\HR\Models\HrPayrollRun;
use
App\Modules\HR\Models\HrEmployeeProfile
;
use
App\Modules\HR\Services\PayrollCalculationService
;
use
App\Modules\HR\Services\SalarySlipService
;
use
App\Core\EventBus
;
class
PayrollController
extends
Controller
{
...
...
@@ -197,6 +198,19 @@ class PayrollController extends Controller
$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
]);
return
$this
->
redirect
(
'/hr/payroll/periods/'
.
$id
)
->
withSuccess
(
'تم صرف الرواتب بنجاح'
);
...
...
app/Modules/Payments/Services/PaymentService.php
View file @
6291dcf2
...
...
@@ -41,15 +41,13 @@ final class PaymentService
$employee
=
App
::
getInstance
()
->
currentEmployee
();
// 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'
;
$paymentType
=
$data
[
'payment_type'
]
??
''
;
$paymentMethod
=
$data
[
'payment_method'
]
??
'cash'
;
$description
=
$data
[
'description'
]
??
''
;
$guestName
=
$data
[
'guest_name'
]
??
null
;
if
(
$memberId
<=
0
)
{
return
[
'success'
=>
false
,
'error'
=>
'العضو غير محدد'
];
}
if
(
bccomp
((
string
)
$amount
,
'0.01'
,
2
)
<
0
)
{
return
[
'success'
=>
false
,
'error'
=>
'المبلغ يجب أن يكون أكبر من صفر'
];
}
...
...
@@ -57,10 +55,13 @@ final class PaymentService
return
[
'success'
=>
false
,
'error'
=>
'نوع الدفعة مطلوب'
];
}
// Verify member exists
$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'
=>
'العضو غير موجود'
];
// Verify member exists (skip for guest payments)
$member
=
null
;
if
(
$memberId
!==
null
&&
$memberId
>
0
)
{
$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
();
...
...
@@ -70,7 +71,7 @@ final class PaymentService
// Create payment record
$paymentId
=
$db
->
insert
(
'payments'
,
[
'member_id'
=>
$memberId
,
'member_id'
=>
(
$memberId
!==
null
&&
$memberId
>
0
)
?
$memberId
:
null
,
'payment_type'
=>
$paymentType
,
'amount'
=>
$amount
,
'currency'
=>
$data
[
'currency'
]
??
'EGP'
,
...
...
@@ -84,7 +85,7 @@ final class PaymentService
'transfer_bank'
=>
$data
[
'transfer_bank'
]
??
null
,
'related_entity_type'
=>
$data
[
'related_entity_type'
]
??
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'
),
'received_by_employee_id'
=>
$employee
?
(
int
)
$employee
->
id
:
null
,
'is_voided'
=>
0
,
...
...
@@ -96,7 +97,7 @@ final class PaymentService
// Auto-generate description if empty
if
(
$description
===
''
)
{
$description
=
self
::
getPaymentTypeLabel
(
$paymentType
);
if
(
$member
[
'form_number'
])
{
if
(
$member
&&
$member
[
'form_number'
])
{
$description
.=
' — استمارة '
.
$member
[
'form_number'
];
}
}
...
...
@@ -104,7 +105,7 @@ final class PaymentService
// Create receipt
$receiptId
=
$db
->
insert
(
'receipts'
,
[
'receipt_number'
=>
$receiptNumber
,
'member_id'
=>
$memberId
,
'member_id'
=>
(
$memberId
!==
null
&&
$memberId
>
0
)
?
$memberId
:
null
,
'payment_id'
=>
$paymentId
,
'receipt_type'
=>
'payment'
,
'amount'
=>
$amount
,
...
...
@@ -127,7 +128,7 @@ final class PaymentService
'payment_id'
=>
$paymentId
,
'receipt_id'
=>
$receiptId
,
'receipt_number'
=>
$receiptNumber
,
'member_id'
=>
$memberId
,
'member_id'
=>
(
$memberId
!==
null
&&
$memberId
>
0
)
?
$memberId
:
0
,
'type'
=>
$paymentType
,
'amount'
=>
$amount
,
'method'
=>
$paymentMethod
,
...
...
@@ -135,7 +136,7 @@ final class PaymentService
Logger
::
info
(
"Payment processed"
,
[
'payment_id'
=>
$paymentId
,
'member_id'
=>
$memberId
,
'member_id'
=>
$memberId
??
0
,
'type'
=>
$paymentType
,
'amount'
=>
$amount
,
]);
...
...
@@ -151,7 +152,7 @@ final class PaymentService
}
catch
(
\Throwable
$e
)
{
$db
->
rollBack
();
Logger
::
error
(
"Payment failed: "
.
$e
->
getMessage
(),
[
'member_id'
=>
$memberId
,
'member_id'
=>
$memberId
??
0
,
'type'
=>
$paymentType
,
'amount'
=>
$amount
,
]);
...
...
@@ -311,7 +312,8 @@ final class PaymentService
'carnet_replacement'
=>
'بدل فاقد كارنيه'
,
'seasonal_fee'
=>
'رسوم عضوية موسمية'
,
'sports_conversion'
=>
'رسوم تحويل رياضي'
,
'inventory_sale'
=>
'مبيعات مخزون'
,
'inventory_sale'
=>
'مبيعات مخزون'
,
'activity_subscription'
=>
'اشتراك نشاط'
,
'other'
=>
'أخرى'
,
default
=>
$type
,
};
...
...
app/Modules/Sales/Services/InventoryPaymentService.php
View file @
6291dcf2
...
...
@@ -3,108 +3,29 @@ declare(strict_types=1);
namespace
App\Modules\Sales\Services
;
use
App\Core\App
;
use
App\Core\EventBus
;
use
App\Core\Logger
;
use
App\Modules\Payments\Services\PaymentService
;
/**
* Guest payment wrapper —
bypasses the member_id validation in PaymentService
.
*
Creates payment + receipt directly for guest sales
.
* Guest payment wrapper —
delegates to PaymentService with member_id = null
.
*
Kept as a thin adapter so callers don't need to change
.
*/
final
class
InventoryPaymentService
{
/**
* Process a guest payment (no member_id required).
* Delegates entirely to the central PaymentService.
*/
public
static
function
processGuestPayment
(
array
$data
)
:
array
{
$db
=
App
::
getInstance
()
->
db
();
$employee
=
App
::
getInstance
()
->
currentEmployee
();
$amount
=
$data
[
'amount'
]
??
'0.00'
;
$paymentMethod
=
$data
[
'payment_method'
]
??
'cash'
;
$guestName
=
$data
[
'guest_name'
]
??
'زائر'
;
$description
=
$data
[
'description'
]
??
'مبيعات مخزون (زائر)'
;
if
(
bccomp
((
string
)
$amount
,
'0.01'
,
2
)
<
0
)
{
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
()];
}
return
PaymentService
::
processPayment
([
'member_id'
=>
null
,
'amount'
=>
$data
[
'amount'
]
??
'0.00'
,
'payment_type'
=>
$data
[
'payment_type'
]
??
'inventory_sale'
,
'payment_method'
=>
$data
[
'payment_method'
]
??
'cash'
,
'guest_name'
=>
$data
[
'guest_name'
]
??
'زائر'
,
'related_entity_type'
=>
$data
[
'related_entity_type'
]
??
null
,
'related_entity_id'
=>
$data
[
'related_entity_id'
]
??
null
,
'description'
=>
$data
[
'description'
]
??
'مبيعات مخزون (زائر)'
,
]);
}
}
app/Modules/Seasonal/Controllers/SeasonalController.php
View file @
6291dcf2
...
...
@@ -10,6 +10,7 @@ use App\Core\App;
use
App\Core\EventBus
;
use
App\Modules\Seasonal\Models\SeasonalMembership
;
use
App\Modules\Rules\Services\RuleEngine
;
use
App\Modules\Payments\Services\PaymentService
;
class
SeasonalController
extends
Controller
{
...
...
@@ -109,6 +110,24 @@ class SeasonalController extends Controller
'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'
,
[
'member_id'
=>
(
int
)
$memberId
,
'seasonal_id'
=>
(
int
)
$seasonal
->
id
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment