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
c674d6f6
Commit
c674d6f6
authored
May 22, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
dsfghgjdfgtkjdgfkj
parent
7c9f8efb
Changes
24
Show whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
1566 additions
and
551 deletions
+1566
-551
BookingWizardController.php
...es/SportsActivity/Controllers/BookingWizardController.php
+12
-7
DashboardController.php
...odules/SportsActivity/Controllers/DashboardController.php
+30
-70
GroupController.php
app/Modules/SportsActivity/Controllers/GroupController.php
+20
-0
SaCardController.php
app/Modules/SportsActivity/Controllers/SaCardController.php
+21
-0
Routes.php
app/Modules/SportsActivity/Routes.php
+2
-0
SaConstants.php
app/Modules/SportsActivity/SaConstants.php
+88
-0
AcademyPricingService.php
...Modules/SportsActivity/Services/AcademyPricingService.php
+9
-39
AttendanceRuleService.php
...Modules/SportsActivity/Services/AttendanceRuleService.php
+115
-0
BookingService.php
app/Modules/SportsActivity/Services/BookingService.php
+90
-76
CardRenewalService.php
app/Modules/SportsActivity/Services/CardRenewalService.php
+116
-0
DiscountCalculatorService.php
...les/SportsActivity/Services/DiscountCalculatorService.php
+96
-0
EnrollmentService.php
app/Modules/SportsActivity/Services/EnrollmentService.php
+155
-98
GateAccessService.php
app/Modules/SportsActivity/Services/GateAccessService.php
+18
-1
GroupTransferService.php
app/Modules/SportsActivity/Services/GroupTransferService.php
+104
-0
NumberGeneratorService.php
...odules/SportsActivity/Services/NumberGeneratorService.php
+38
-0
PoolGridService.php
app/Modules/SportsActivity/Services/PoolGridService.php
+28
-17
RegistrationWizardService.php
...les/SportsActivity/Services/RegistrationWizardService.php
+63
-79
SaEventListenerService.php
...odules/SportsActivity/Services/SaEventListenerService.php
+195
-0
SubscriptionGeneratorService.php
.../SportsActivity/Services/SubscriptionGeneratorService.php
+57
-45
bootstrap.php
app/Modules/SportsActivity/bootstrap.php
+51
-119
SaAttendanceReportJob.php
cron/jobs/SaAttendanceReportJob.php
+69
-0
SaCardExpiryJob.php
cron/jobs/SaCardExpiryJob.php
+83
-0
SaOverdueSubscriptionJob.php
cron/jobs/SaOverdueSubscriptionJob.php
+85
-0
Phase_76_001_sa_module_enhancements.php
database/migrations/Phase_76_001_sa_module_enhancements.php
+21
-0
No files found.
app/Modules/SportsActivity/Controllers/BookingWizardController.php
View file @
c674d6f6
...
...
@@ -241,6 +241,18 @@ class BookingWizardController extends Controller
{
$this
->
authorize
(
'sa.booking_wizard.use'
);
$errors
=
$this
->
validate
(
$request
->
allPost
(),
[
'unit_id'
=>
'required|integer'
,
'date'
=>
'required|date'
,
'start_time'
=>
'required|string'
,
'end_time'
=>
'required|string'
,
'participants'
=>
'required|integer|min:1'
,
'booker_name'
=>
'required|string|min:2|max:200'
,
]);
if
(
$errors
)
{
return
$this
->
json
([
'success'
=>
false
,
'error'
=>
'بيانات الحجز غير مكتملة'
]);
}
$unitId
=
(
int
)
$request
->
post
(
'unit_id'
,
0
);
$date
=
trim
((
string
)
$request
->
post
(
'date'
,
''
));
$startTime
=
trim
((
string
)
$request
->
post
(
'start_time'
,
''
));
...
...
@@ -251,13 +263,6 @@ class BookingWizardController extends Controller
$isMember
=
(
bool
)
$request
->
post
(
'is_member'
,
false
);
$memberId
=
(
int
)
$request
->
post
(
'member_id'
,
0
);
if
(
$unitId
<=
0
||
$date
===
''
||
$startTime
===
''
||
$endTime
===
''
)
{
return
$this
->
json
([
'success'
=>
false
,
'error'
=>
'بيانات الحجز غير مكتملة'
]);
}
if
(
$bookerName
===
''
)
{
return
$this
->
json
([
'success'
=>
false
,
'error'
=>
'اسم الحاجز مطلوب'
]);
}
$branch
=
App
::
getInstance
()
->
currentBranch
();
$bookerType
=
$isMember
?
'member'
:
'guest'
;
...
...
app/Modules/SportsActivity/Controllers/DashboardController.php
View file @
c674d6f6
...
...
@@ -10,84 +10,44 @@ use App\Core\App;
class
DashboardController
extends
Controller
{
/**
* Main SportsActivity dashboard with summary cards.
*/
public
function
index
(
Request
$request
)
:
Response
{
$db
=
App
::
getInstance
()
->
db
();
$today
=
date
(
'Y-m-d'
);
// Total active disciplines
$activeDisciplines
=
(
int
)
(
$db
->
selectOne
(
"SELECT COUNT(*) as cnt FROM sa_disciplines WHERE is_active = 1"
,
[]
)[
'cnt'
]
??
0
);
// Total active facilities + units
$activeFacilities
=
(
int
)
(
$db
->
selectOne
(
"SELECT COUNT(*) as cnt FROM sa_facilities WHERE is_active = 1 AND is_archived = 0"
,
[]
)[
'cnt'
]
??
0
);
$activeUnits
=
(
int
)
(
$db
->
selectOne
(
"SELECT COUNT(*) as cnt FROM sa_facility_units WHERE is_active = 1"
,
[]
)[
'cnt'
]
??
0
);
// Total active coaches
$activeCoaches
=
(
int
)
(
$db
->
selectOne
(
"SELECT COUNT(*) as cnt FROM sa_coaches WHERE is_active = 1 AND is_archived = 0"
,
[]
)[
'cnt'
]
??
0
);
// Total registered players (not archived)
$totalPlayers
=
(
int
)
(
$db
->
selectOne
(
"SELECT COUNT(*) as cnt FROM sa_players WHERE is_archived = 0"
,
[]
)[
'cnt'
]
??
0
);
// Total active groups + enrolled players
$activeGroups
=
(
int
)
(
$db
->
selectOne
(
"SELECT COUNT(*) as cnt FROM sa_groups WHERE status = 'active' AND is_archived = 0"
,
[]
)[
'cnt'
]
??
0
);
$enrolledPlayers
=
(
int
)
(
$db
->
selectOne
(
"SELECT COUNT(*) as cnt FROM sa_group_players WHERE status = 'active'"
,
[]
)[
'cnt'
]
??
0
);
// Today's bookings count
$todayBookings
=
(
int
)
(
$db
->
selectOne
(
"SELECT COUNT(*) as cnt FROM sa_bookings WHERE booking_date = ? AND status NOT IN ('cancelled', 'no_show')"
,
[
$today
]
)[
'cnt'
]
??
0
);
// Pending medical approvals
$pendingMedical
=
(
int
)
(
$db
->
selectOne
(
"SELECT COUNT(*) as cnt FROM sa_player_documents WHERE document_type = 'medical_cert' AND approval_status = 'pending'"
,
[]
)[
'cnt'
]
??
0
);
$branchScope
=
''
;
$branchParams
=
[];
if
(
function_exists
(
'branch_scope'
))
{
$branchScope
=
branch_scope
(
''
,
'branch_id'
);
$branchParams
=
branch_params
();
}
// Overdue subscriptions
$overdueSubscriptions
=
(
int
)
(
$db
->
selectOne
(
"SELECT COUNT(*) as cnt FROM sa_subscriptions WHERE payment_status IN ('unpaid', 'overdue') AND period_end < ?"
,
[
$today
]
)[
'cnt'
]
??
0
);
$stats
=
$db
->
selectOne
(
"
SELECT
(SELECT COUNT(*) FROM sa_disciplines WHERE is_active = 1) as active_disciplines,
(SELECT COUNT(*) FROM sa_facilities WHERE is_active = 1 AND is_archived = 0) as active_facilities,
(SELECT COUNT(*) FROM sa_facility_units WHERE is_active = 1) as active_units,
(SELECT COUNT(*) FROM sa_coaches WHERE is_active = 1 AND is_archived = 0) as active_coaches,
(SELECT COUNT(*) FROM sa_players WHERE is_archived = 0
{
$branchScope
}
) as total_players,
(SELECT COUNT(*) FROM sa_groups WHERE status = 'active' AND is_archived = 0) as active_groups,
(SELECT COUNT(*) FROM sa_group_players WHERE status = 'active') as enrolled_players,
(SELECT COUNT(*) FROM sa_bookings WHERE booking_date = ? AND status NOT IN ('cancelled','no_show')
{
$branchScope
}
) as today_bookings,
(SELECT COUNT(*) FROM sa_player_documents WHERE document_type = 'medical_cert' AND approval_status = 'pending') as pending_medical,
(SELECT COUNT(*) FROM sa_subscriptions WHERE payment_status IN ('unpaid','overdue') AND period_end < ?) as overdue_subscriptions
"
,
array_merge
([
$today
],
$branchParams
,
$branchParams
,
[
$today
]));
return
$this
->
view
(
'SportsActivity.Views.dashboard'
,
[
'stats'
=>
[
'active_disciplines'
=>
$activeDisciplines
,
'active_facilities'
=>
$activeFacilities
,
'active_units'
=>
$activeUnits
,
'active_coaches'
=>
$activeCoaches
,
'total_players'
=>
$totalPlayers
,
'active_groups'
=>
$activeGroups
,
'enrolled_players'
=>
$enrolledPlayers
,
'today_bookings'
=>
$todayBookings
,
'pending_medical'
=>
$pendingMedical
,
'overdue_subscriptions'
=>
$overdueSubscriptions
,
'active_disciplines'
=>
(
int
)
(
$stats
[
'active_disciplines'
]
??
0
)
,
'active_facilities'
=>
(
int
)
(
$stats
[
'active_facilities'
]
??
0
)
,
'active_units'
=>
(
int
)
(
$stats
[
'active_units'
]
??
0
)
,
'active_coaches'
=>
(
int
)
(
$stats
[
'active_coaches'
]
??
0
)
,
'total_players'
=>
(
int
)
(
$stats
[
'total_players'
]
??
0
)
,
'active_groups'
=>
(
int
)
(
$stats
[
'active_groups'
]
??
0
)
,
'enrolled_players'
=>
(
int
)
(
$stats
[
'enrolled_players'
]
??
0
)
,
'today_bookings'
=>
(
int
)
(
$stats
[
'today_bookings'
]
??
0
)
,
'pending_medical'
=>
(
int
)
(
$stats
[
'pending_medical'
]
??
0
)
,
'overdue_subscriptions'
=>
(
int
)
(
$stats
[
'overdue_subscriptions'
]
??
0
)
,
],
'today'
=>
$today
,
]);
...
...
app/Modules/SportsActivity/Controllers/GroupController.php
View file @
c674d6f6
...
...
@@ -11,6 +11,7 @@ use App\Core\Pagination;
use
App\Modules\SportsActivity\Models\Group
;
use
App\Modules\SportsActivity\Models\Program
;
use
App\Modules\SportsActivity\Services\EnrollmentService
;
use
App\Modules\SportsActivity\Services\GroupTransferService
;
use
App\Modules\SportsActivity\Services\ScheduleGeneratorService
;
class
GroupController
extends
Controller
...
...
@@ -436,6 +437,25 @@ class GroupController extends Controller
return
$this
->
redirect
(
'/sa/groups/'
.
$id
)
->
withSuccess
(
"تم حفظ الجدول (
{
$inserted
}
حصة)"
);
}
public
function
transferPlayer
(
Request
$request
,
string
$id
)
:
Response
{
$playerId
=
(
int
)
$request
->
post
(
'player_id'
,
0
);
$toGroupId
=
(
int
)
$request
->
post
(
'to_group_id'
,
0
);
$reason
=
trim
((
string
)
$request
->
post
(
'reason'
,
''
));
if
(
$playerId
===
0
||
$toGroupId
===
0
)
{
return
$this
->
redirect
(
'/sa/groups/'
.
$id
)
->
withError
(
'يرجى تحديد اللاعب والمجموعة المنقول إليها'
);
}
$result
=
GroupTransferService
::
transfer
(
$playerId
,
(
int
)
$id
,
$toGroupId
,
$reason
);
if
(
$result
[
'success'
])
{
return
$this
->
redirect
(
'/sa/groups/'
.
$id
)
->
withSuccess
(
'تم نقل اللاعب بنجاح'
);
}
return
$this
->
redirect
(
'/sa/groups/'
.
$id
)
->
withError
(
$result
[
'error'
]);
}
public
function
generateSessions
(
Request
$request
,
string
$id
)
:
Response
{
$group
=
Group
::
find
((
int
)
$id
);
...
...
app/Modules/SportsActivity/Controllers/SaCardController.php
View file @
c674d6f6
...
...
@@ -9,6 +9,7 @@ use App\Core\Response;
use
App\Core\App
;
use
App\Core\Pagination
;
use
App\Modules\Carnets\Services\QRCodeGenerator
;
use
App\Modules\SportsActivity\Services\CardRenewalService
;
class
SaCardController
extends
Controller
{
...
...
@@ -239,6 +240,26 @@ class SaCardController extends Controller
]);
}
public
function
renew
(
Request
$request
,
string
$id
)
:
Response
{
$months
=
(
int
)
$request
->
post
(
'months'
,
0
);
if
(
$months
<
1
||
$months
>
24
)
{
return
$this
->
redirect
(
'/sa/cards/'
.
$id
)
->
withError
(
'يرجى تحديد مدة تجديد صالحة (1-24 شهر)'
);
}
$result
=
CardRenewalService
::
renewCard
((
int
)
$id
,
$months
);
if
(
$result
[
'success'
])
{
$msg
=
'تم إنشاء طلب تجديد الكارت'
;
if
(
!
empty
(
$result
[
'request_number'
]))
{
$msg
.=
' (طلب دفع: '
.
$result
[
'request_number'
]
.
')'
;
}
return
$this
->
redirect
(
'/sa/cards/'
.
$id
)
->
withSuccess
(
$msg
);
}
return
$this
->
redirect
(
'/sa/cards/'
.
$id
)
->
withError
(
$result
[
'error'
]);
}
public
function
report
(
Request
$request
)
:
Response
{
$db
=
App
::
getInstance
()
->
db
();
...
...
app/Modules/SportsActivity/Routes.php
View file @
c674d6f6
...
...
@@ -94,6 +94,7 @@ return [
[
'POST'
,
'/sa/groups/{id:\d+}'
,
'SportsActivity\Controllers\GroupController@update'
,
[
'auth'
,
'csrf'
],
'sa.group.manage'
],
[
'POST'
,
'/sa/groups/{id:\d+}/enroll'
,
'SportsActivity\Controllers\GroupController@enroll'
,
[
'auth'
,
'csrf'
],
'sa.group.enroll'
],
[
'POST'
,
'/sa/groups/{id:\d+}/remove-player'
,
'SportsActivity\Controllers\GroupController@removePlayer'
,
[
'auth'
,
'csrf'
],
'sa.group.manage'
],
[
'POST'
,
'/sa/groups/{id:\d+}/transfer-player'
,
'SportsActivity\Controllers\GroupController@transferPlayer'
,
[
'auth'
,
'csrf'
],
'sa.group.manage'
],
[
'POST'
,
'/sa/groups/{id:\d+}/schedule'
,
'SportsActivity\Controllers\GroupController@saveSchedule'
,
[
'auth'
,
'csrf'
],
'sa.group.manage'
],
[
'POST'
,
'/sa/groups/{id:\d+}/generate-sessions'
,
'SportsActivity\Controllers\GroupController@generateSessions'
,
[
'auth'
,
'csrf'
],
'sa.schedule.manage'
],
...
...
@@ -216,6 +217,7 @@ return [
[
'POST'
,
'/sa/cards/{id:\d+}/suspend'
,
'SportsActivity\Controllers\SaCardController@suspend'
,
[
'auth'
,
'csrf'
],
'sa.card.manage'
],
[
'POST'
,
'/sa/cards/{id:\d+}/revoke'
,
'SportsActivity\Controllers\SaCardController@revoke'
,
[
'auth'
,
'csrf'
],
'sa.card.manage'
],
[
'POST'
,
'/sa/cards/{id:\d+}/reactivate'
,
'SportsActivity\Controllers\SaCardController@reactivate'
,
[
'auth'
,
'csrf'
],
'sa.card.manage'
],
[
'POST'
,
'/sa/cards/{id:\d+}/renew'
,
'SportsActivity\Controllers\SaCardController@renew'
,
[
'auth'
,
'csrf'
],
'sa.card.manage'
],
[
'GET'
,
'/sa/cards/{id:\d+}/print'
,
'SportsActivity\Controllers\SaCardController@print'
,
[
'auth'
],
'sa.card.print'
],
// ─── Gate Access ────────────────────────────────────────────────────────────
...
...
app/Modules/SportsActivity/SaConstants.php
0 → 100644
View file @
c674d6f6
<?php
declare
(
strict_types
=
1
);
namespace
App\Modules\SportsActivity
;
final
class
SaConstants
{
// Enrollment statuses (sa_group_players.status)
const
STATUS_ACTIVE
=
'active'
;
const
STATUS_PENDING_PAYMENT
=
'pending_payment'
;
const
STATUS_WITHDRAWN
=
'withdrawn'
;
const
STATUS_TRANSFERRED
=
'transferred'
;
// Booking statuses (sa_bookings.status)
const
BOOKING_CONFIRMED
=
'confirmed'
;
const
BOOKING_CANCELLED
=
'cancelled'
;
const
BOOKING_NO_SHOW
=
'no_show'
;
const
BOOKING_COMPLETED
=
'completed'
;
// Booking types
const
BOOKING_TYPE_HOURLY
=
'hourly'
;
const
BOOKING_TYPE_TRAINING
=
'training'
;
// Payment statuses
const
PAYMENT_UNPAID
=
'unpaid'
;
const
PAYMENT_PENDING
=
'pending'
;
const
PAYMENT_PAID
=
'paid'
;
const
PAYMENT_OVERDUE
=
'overdue'
;
const
PAYMENT_REFUNDED
=
'refunded'
;
// Player types
const
PLAYER_MEMBER
=
'member'
;
const
PLAYER_NON_MEMBER
=
'non_member'
;
// Booker types
const
BOOKER_MEMBER
=
'member'
;
const
BOOKER_GUEST
=
'guest'
;
// Medical statuses
const
MEDICAL_FIT
=
'fit'
;
const
MEDICAL_CONDITIONAL
=
'conditional'
;
const
MEDICAL_PENDING
=
'pending'
;
const
MEDICAL_EXPIRED
=
'expired'
;
const
MEDICAL_UNFIT
=
'unfit'
;
// Card statuses
const
CARD_ACTIVE
=
'active'
;
const
CARD_TEMPORARY
=
'temporary'
;
const
CARD_SUSPENDED
=
'suspended'
;
const
CARD_REVOKED
=
'revoked'
;
const
CARD_EXPIRED
=
'expired'
;
// Pool grid actions
const
GRID_TRAINING
=
'training'
;
const
GRID_BLOCKED
=
'blocked'
;
const
GRID_MAINTENANCE
=
'maintenance'
;
const
GRID_HOURLY
=
'hourly'
;
// Registration statuses
const
REG_IN_PROGRESS
=
'in_progress'
;
const
REG_PENDING_PAYMENT
=
'pending_payment'
;
const
REG_COMPLETED
=
'completed'
;
const
REG_CANCELLED
=
'cancelled'
;
// Payment types (used with PaymentRequestService)
const
PAY_TYPE_SPORTS_REG
=
'sports_registration'
;
const
PAY_TYPE_HOURLY_BOOKING
=
'hourly_booking'
;
const
PAY_TYPE_FORM_FEE
=
'sa_form_fee'
;
const
PAY_TYPE_SPORTS_SUB
=
'sports_subscription'
;
const
PAY_TYPE_CARD_RENEWAL
=
'sa_card_renewal'
;
// Entity types (related_entity_type)
const
ENTITY_GROUP_PLAYERS
=
'sa_group_players'
;
const
ENTITY_SUBSCRIPTIONS
=
'sa_subscriptions'
;
const
ENTITY_BOOKINGS
=
'sa_bookings'
;
const
ENTITY_REGISTRATIONS
=
'sa_registrations'
;
const
ENTITY_REG_FORM
=
'sa_registration_form'
;
const
ENTITY_PLAYER_CARDS
=
'sa_player_cards'
;
// Group statuses
const
GROUP_ACTIVE
=
'active'
;
const
GROUP_PAUSED
=
'paused'
;
const
GROUP_COMPLETED
=
'completed'
;
const
GROUP_CANCELLED
=
'cancelled'
;
// Cancelled booking statuses (for exclusion)
const
EXCLUDED_STATUSES
=
[
'cancelled'
,
'no_show'
];
}
app/Modules/SportsActivity/Services/AcademyPricingService.php
View file @
c674d6f6
...
...
@@ -35,43 +35,13 @@ final class AcademyPricingService
$basePrice
=
$isMember
?
(
float
)
$rule
[
'price_member'
]
:
(
float
)
$rule
[
'price_nonmember'
];
$totalBeforeDiscount
=
$basePrice
*
$months
;
$discounts
=
[];
$totalDiscount
=
0.0
;
// 15% discount for 3-month advance payment
if
(
$months
>=
3
)
{
$advanceDiscount
=
self
::
getDiscountRule
(
'ADVANCE_3MONTHS_15PCT'
);
if
(
$advanceDiscount
&&
(
int
)
$advanceDiscount
[
'is_active'
])
{
$discountAmount
=
$totalBeforeDiscount
*
((
float
)
$advanceDiscount
[
'discount_value'
]
/
100
);
$totalDiscount
+=
$discountAmount
;
$discounts
[]
=
[
'code'
=>
'ADVANCE_3MONTHS_15PCT'
,
'name_ar'
=>
$advanceDiscount
[
'name_ar'
],
'amount'
=>
round
(
$discountAmount
,
2
),
'percentage'
=>
(
float
)
$advanceDiscount
[
'discount_value'
],
];
}
}
// Sibling discount (chess academy)
if
(
$hasSibling
)
{
$siblingDiscount
=
self
::
getDiscountRule
(
'CHESS_SIBLING_10PCT'
);
if
(
$siblingDiscount
&&
(
int
)
$siblingDiscount
[
'is_active'
])
{
$siblingAcademy
=
$siblingDiscount
[
'academy_code'
];
if
(
!
$siblingAcademy
||
$siblingAcademy
===
$academyCode
)
{
$discountAmount
=
(
$totalBeforeDiscount
-
$totalDiscount
)
*
((
float
)
$siblingDiscount
[
'discount_value'
]
/
100
);
$totalDiscount
+=
$discountAmount
;
$discounts
[]
=
[
'code'
=>
'CHESS_SIBLING_10PCT'
,
'name_ar'
=>
$siblingDiscount
[
'name_ar'
],
'amount'
=>
round
(
$discountAmount
,
2
),
'percentage'
=>
(
float
)
$siblingDiscount
[
'discount_value'
],
];
}
}
}
$finalTotal
=
round
(
$totalBeforeDiscount
-
$totalDiscount
,
2
);
$discountResult
=
DiscountCalculatorService
::
calculateDiscounts
(
$totalBeforeDiscount
,
$months
,
$hasSibling
,
$academyCode
);
return
[
'found'
=>
true
,
...
...
@@ -82,9 +52,9 @@ final class AcademyPricingService
'base_price_monthly'
=>
$basePrice
,
'months'
=>
$months
,
'subtotal'
=>
$totalBeforeDiscount
,
'discounts'
=>
$discount
s
,
'total_discount'
=>
round
(
$totalDiscount
,
2
)
,
'final_total'
=>
$
finalTotal
,
'discounts'
=>
$discount
Result
[
'discounts'
]
,
'total_discount'
=>
$discountResult
[
'total_discount'
]
,
'final_total'
=>
$
discountResult
[
'final_amount'
]
,
'sessions_per_week'
=>
$rule
[
'sessions_per_week'
]
?
(
int
)
$rule
[
'sessions_per_week'
]
:
null
,
'session_duration_minutes'
=>
$rule
[
'session_duration_minutes'
]
?
(
int
)
$rule
[
'session_duration_minutes'
]
:
null
,
'includes_fitness'
=>
(
bool
)
(
int
)
(
$rule
[
'includes_fitness'
]
??
0
),
...
...
app/Modules/SportsActivity/Services/AttendanceRuleService.php
0 → 100644
View file @
c674d6f6
<?php
declare
(
strict_types
=
1
);
namespace
App\Modules\SportsActivity\Services
;
use
App\Core\App
;
use
App\Core\EventBus
;
use
App\Core\Logger
;
final
class
AttendanceRuleService
{
private
const
DEFAULT_ABSENCE_THRESHOLD
=
3
;
public
static
function
checkAbsenceThreshold
(
int
$playerId
,
int
$groupId
)
:
?
array
{
$db
=
App
::
getInstance
()
->
db
();
$monthStart
=
date
(
'Y-m-01'
);
$today
=
date
(
'Y-m-d'
);
$absences
=
(
int
)
(
$db
->
selectOne
(
"SELECT COUNT(*) as cnt FROM sa_attendance
WHERE player_id = ? AND group_id = ? AND status = 'absent'
AND attendance_date BETWEEN ? AND ?"
,
[
$playerId
,
$groupId
,
$monthStart
,
$today
]
)[
'cnt'
]
??
0
);
$thresholdRow
=
$db
->
selectOne
(
"SELECT config_value FROM system_config WHERE config_key = 'sa.absence_threshold'"
,
[]
);
$threshold
=
(
int
)
(
$thresholdRow
[
'config_value'
]
??
self
::
DEFAULT_ABSENCE_THRESHOLD
);
if
(
$absences
>=
$threshold
)
{
return
[
'player_id'
=>
$playerId
,
'group_id'
=>
$groupId
,
'absences'
=>
$absences
,
'threshold'
=>
$threshold
,
'month'
=>
date
(
'Y-m'
),
];
}
return
null
;
}
public
static
function
getAttendanceRate
(
int
$playerId
,
int
$groupId
,
string
$from
,
string
$to
)
:
float
{
$db
=
App
::
getInstance
()
->
db
();
$total
=
(
int
)
(
$db
->
selectOne
(
"SELECT COUNT(*) as cnt FROM sa_attendance
WHERE player_id = ? AND group_id = ? AND attendance_date BETWEEN ? AND ?"
,
[
$playerId
,
$groupId
,
$from
,
$to
]
)[
'cnt'
]
??
0
);
if
(
$total
===
0
)
{
return
0.0
;
}
$present
=
(
int
)
(
$db
->
selectOne
(
"SELECT COUNT(*) as cnt FROM sa_attendance
WHERE player_id = ? AND group_id = ? AND status IN ('present', 'late')
AND attendance_date BETWEEN ? AND ?"
,
[
$playerId
,
$groupId
,
$from
,
$to
]
)[
'cnt'
]
??
0
);
return
round
((
$present
/
$total
)
*
100
,
1
);
}
public
static
function
processMonthlyAbsenceReport
()
:
array
{
$db
=
App
::
getInstance
()
->
db
();
$lastMonth
=
date
(
'Y-m-01'
,
strtotime
(
'-1 month'
));
$lastMonthEnd
=
date
(
'Y-m-t'
,
strtotime
(
'-1 month'
));
$processed
=
0
;
$thresholdRow
=
$db
->
selectOne
(
"SELECT config_value FROM system_config WHERE config_key = 'sa.absence_threshold'"
,
[]
);
$threshold
=
(
int
)
(
$thresholdRow
[
'config_value'
]
??
self
::
DEFAULT_ABSENCE_THRESHOLD
);
$absentees
=
$db
->
select
(
"SELECT a.player_id, a.group_id, COUNT(*) as absence_count,
p.full_name_ar, p.phone, p.guardian_phone, g.name_ar as group_name
FROM sa_attendance a
JOIN sa_players p ON p.id = a.player_id
JOIN sa_groups g ON g.id = a.group_id
WHERE a.status = 'absent'
AND a.attendance_date BETWEEN ? AND ?
GROUP BY a.player_id, a.group_id, p.full_name_ar, p.phone, p.guardian_phone, g.name_ar
HAVING COUNT(*) >= ?"
,
[
$lastMonth
,
$lastMonthEnd
,
$threshold
]
);
foreach
(
$absentees
as
$row
)
{
EventBus
::
dispatch
(
'sa.player.absence_threshold'
,
[
'player_id'
=>
(
int
)
$row
[
'player_id'
],
'group_id'
=>
(
int
)
$row
[
'group_id'
],
'player_name'
=>
$row
[
'full_name_ar'
],
'group_name'
=>
$row
[
'group_name'
],
'phone'
=>
$row
[
'guardian_phone'
]
?:
$row
[
'phone'
],
'absence_count'
=>
(
int
)
$row
[
'absence_count'
],
'threshold'
=>
$threshold
,
'month'
=>
date
(
'Y-m'
,
strtotime
(
'-1 month'
)),
]);
$processed
++
;
}
Logger
::
info
(
"SA Attendance report:
{
$processed
}
players exceeded absence threshold"
);
return
[
'processed'
=>
$processed
,
'threshold'
=>
$threshold
];
}
}
app/Modules/SportsActivity/Services/BookingService.php
View file @
c674d6f6
...
...
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace
App\Modules\SportsActivity\Services
;
use
App\Core\App
;
use
App\Modules\Payments\Services\PaymentService
;
use
App\Modules\SportsActivity\SaConstants
;
final
class
BookingService
{
...
...
@@ -17,10 +19,10 @@ final class BookingService
$endTime
=
$data
[
'end_time'
]
??
''
;
$participantCount
=
(
int
)
(
$data
[
'participant_count'
]
??
1
);
$spotsReserved
=
(
int
)
(
$data
[
'spots_reserved'
]
??
$participantCount
);
$bookerType
=
$data
[
'booker_type'
]
??
'guest'
;
$bookerType
=
$data
[
'booker_type'
]
??
SaConstants
::
BOOKER_GUEST
;
$bookerId
=
isset
(
$data
[
'booker_id'
])
?
(
int
)
$data
[
'booker_id'
]
:
null
;
$bookerName
=
$data
[
'booker_name'
]
??
''
;
$isMember
=
(
$bookerType
===
'member'
);
$isMember
=
(
$bookerType
===
SaConstants
::
BOOKER_MEMBER
);
$availability
=
SlotAvailabilityService
::
check
(
$unitId
,
$date
,
$startTime
,
$endTime
,
$spotsReserved
);
if
(
!
$availability
[
'available'
])
{
...
...
@@ -30,13 +32,15 @@ final class BookingService
$pricing
=
PricingCalculatorService
::
calculate
(
$unitId
,
$date
,
$startTime
,
$endTime
,
$participantCount
,
$isMember
);
$totalAmount
=
$pricing
?
$pricing
[
'total_amount'
]
:
0.00
;
$bookingNumber
=
self
::
generate
Number
();
$bookingNumber
=
NumberGeneratorService
::
booking
Number
();
$employeeId
=
(
int
)
(
App
::
getInstance
()
->
session
()
->
get
(
'employee_id'
)
??
0
);
$db
->
beginTransaction
();
try
{
$bookingData
=
[
'booking_number'
=>
$bookingNumber
,
'facility_unit_id'
=>
$unitId
,
'booking_type'
=>
'hourly'
,
'booking_type'
=>
SaConstants
::
BOOKING_TYPE_HOURLY
,
'booking_date'
=>
$date
,
'start_time'
=>
$startTime
,
'end_time'
=>
$endTime
,
...
...
@@ -46,8 +50,8 @@ final class BookingService
'participant_count'
=>
$participantCount
,
'spots_reserved'
=>
$spotsReserved
,
'total_amount'
=>
$totalAmount
,
'payment_status'
=>
'unpaid'
,
'status'
=>
'confirmed'
,
'payment_status'
=>
SaConstants
::
PAYMENT_UNPAID
,
'status'
=>
SaConstants
::
BOOKING_CONFIRMED
,
'notes'
=>
$data
[
'notes'
]
??
null
,
'branch_id'
=>
$data
[
'branch_id'
]
??
null
,
'created_by'
=>
$employeeId
,
...
...
@@ -71,6 +75,12 @@ final class BookingService
}
}
$db
->
commit
();
}
catch
(
\Throwable
$e
)
{
$db
->
rollBack
();
return
[
'success'
=>
false
,
'error'
=>
'فشل إنشاء الحجز: '
.
$e
->
getMessage
()];
}
return
[
'success'
=>
true
,
'booking_id'
=>
$bookingId
,
...
...
@@ -115,13 +125,15 @@ final class BookingService
}
}
$bookingNumber
=
self
::
generate
Number
();
$bookingNumber
=
NumberGeneratorService
::
booking
Number
();
$employeeId
=
(
int
)
(
App
::
getInstance
()
->
session
()
->
get
(
'employee_id'
)
??
0
);
$db
->
beginTransaction
();
try
{
$db
->
insert
(
'sa_bookings'
,
[
'booking_number'
=>
$bookingNumber
,
'facility_unit_id'
=>
$facilityUnitId
,
'booking_type'
=>
'training'
,
'booking_type'
=>
SaConstants
::
BOOKING_TYPE_TRAINING
,
'booking_date'
=>
$date
,
'start_time'
=>
$startTime
,
'end_time'
=>
$endTime
,
...
...
@@ -130,8 +142,8 @@ final class BookingService
'participant_count'
=>
(
int
)
$group
[
'current_count'
],
'spots_reserved'
=>
$spotsNeeded
,
'total_amount'
=>
0.00
,
'payment_status'
=>
'unpaid'
,
'status'
=>
'confirmed'
,
'payment_status'
=>
SaConstants
::
PAYMENT_UNPAID
,
'status'
=>
SaConstants
::
BOOKING_CONFIRMED
,
'is_recurring'
=>
1
,
'branch_id'
=>
null
,
'created_by'
=>
$employeeId
,
...
...
@@ -139,9 +151,16 @@ final class BookingService
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
]);
$bookingId
=
(
int
)
$db
->
lastInsertId
();
$db
->
commit
();
}
catch
(
\Throwable
$e
)
{
$db
->
rollBack
();
return
[
'success'
=>
false
,
'error'
=>
'فشل إنشاء حجز التدريب: '
.
$e
->
getMessage
()];
}
return
[
'success'
=>
true
,
'booking_id'
=>
(
int
)
$db
->
lastInsertId
()
,
'booking_id'
=>
$bookingId
,
'booking_number'
=>
$bookingNumber
,
];
}
...
...
@@ -155,36 +174,31 @@ final class BookingService
return
[
'success'
=>
false
,
'error'
=>
'الحجز غير موجود'
];
}
if
(
in_array
(
$booking
[
'status'
],
[
'cancelled'
,
'no_show'
]))
{
if
(
in_array
(
$booking
[
'status'
],
[
SaConstants
::
BOOKING_CANCELLED
,
SaConstants
::
BOOKING_NO_SHOW
]))
{
return
[
'success'
=>
false
,
'error'
=>
'الحجز ملغي بالفعل'
];
}
$employeeId
=
(
int
)
(
App
::
getInstance
()
->
session
()
->
get
(
'employee_id'
)
??
0
);
$db
->
update
(
'sa_bookings'
,
[
'status'
=>
'cancelled'
,
'status'
=>
SaConstants
::
BOOKING_CANCELLED
,
'cancellation_reason'
=>
$reason
,
'cancelled_by'
=>
$employeeId
,
'cancelled_at'
=>
date
(
'Y-m-d H:i:s'
),
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[
$bookingId
]);
return
[
'success'
=>
true
];
}
private
static
function
generateNumber
()
:
string
{
$db
=
App
::
getInstance
()
->
db
();
$datePrefix
=
'BK-'
.
date
(
'Ymd'
)
.
'-'
;
for
(
$attempt
=
0
;
$attempt
<
10
;
$attempt
++
)
{
$number
=
$datePrefix
.
str_pad
((
string
)
random_int
(
1
,
9999
),
4
,
'0'
,
STR_PAD_LEFT
);
$exists
=
$db
->
selectOne
(
"SELECT id FROM sa_bookings WHERE booking_number = ?"
,
[
$number
]);
if
(
!
$exists
)
{
return
$number
;
// Auto-void payment if paid and within 24-hour void window
if
(
$booking
[
'payment_status'
]
===
SaConstants
::
PAYMENT_PAID
&&
!
empty
(
$booking
[
'payment_id'
]))
{
$voidResult
=
PaymentService
::
voidPayment
((
int
)
$booking
[
'payment_id'
],
'إلغاء حجز: '
.
(
$reason
?:
'بدون سبب'
));
if
(
$voidResult
[
'success'
]
??
false
)
{
$db
->
update
(
'sa_bookings'
,
[
'payment_status'
=>
SaConstants
::
PAYMENT_REFUNDED
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[
$bookingId
]);
}
}
return
$datePrefix
.
str_pad
((
string
)
random_int
(
10000
,
99999
),
5
,
'0'
,
STR_PAD_LEFT
)
;
return
[
'success'
=>
true
]
;
}
}
app/Modules/SportsActivity/Services/CardRenewalService.php
0 → 100644
View file @
c674d6f6
<?php
declare
(
strict_types
=
1
);
namespace
App\Modules\SportsActivity\Services
;
use
App\Core\App
;
use
App\Core\EventBus
;
use
App\Modules\Cashier\Services\PaymentRequestService
;
use
App\Modules\SportsActivity\SaConstants
;
final
class
CardRenewalService
{
public
static
function
renewCard
(
int
$cardId
,
int
$months
=
1
)
:
array
{
$db
=
App
::
getInstance
()
->
db
();
$card
=
$db
->
selectOne
(
"SELECT c.*, p.full_name_ar, p.member_id, p.player_type
FROM sa_player_cards c
JOIN sa_players p ON p.id = c.player_id
WHERE c.id = ? AND c.is_archived = 0"
,
[
$cardId
]
);
if
(
!
$card
)
{
return
[
'success'
=>
false
,
'error'
=>
'الكارت غير موجود'
];
}
if
(
!
in_array
(
$card
[
'status'
],
[
SaConstants
::
CARD_EXPIRED
,
SaConstants
::
CARD_ACTIVE
],
true
))
{
return
[
'success'
=>
false
,
'error'
=>
'حالة الكارت لا تسمح بالتجديد ('
.
$card
[
'status'
]
.
')'
];
}
$feeRow
=
$db
->
selectOne
(
"SELECT config_value FROM system_config WHERE config_key = ?"
,
[
'sa.card_renewal_fee'
]
);
$fee
=
(
float
)
(
$feeRow
[
'config_value'
]
??
'25.00'
);
if
(
$fee
<=
0
)
{
return
self
::
applyRenewal
(
$cardId
,
$months
,
null
);
}
$memberId
=
(
int
)
(
$card
[
'member_id'
]
??
0
);
$description
=
'تجديد كارت نشاط رياضي — '
.
(
$card
[
'full_name_ar'
]
??
''
);
$result
=
PaymentRequestService
::
createRequest
([
'member_id'
=>
$memberId
,
'payment_type'
=>
SaConstants
::
PAY_TYPE_CARD_RENEWAL
,
'amount'
=>
(
string
)
$fee
,
'description_ar'
=>
$description
,
'related_entity_type'
=>
SaConstants
::
ENTITY_PLAYER_CARDS
,
'related_entity_id'
=>
$cardId
,
]);
if
(
!
$result
[
'success'
])
{
return
$result
;
}
return
[
'success'
=>
true
,
'request_id'
=>
$result
[
'request_id'
],
'request_number'
=>
$result
[
'request_number'
],
'fee'
=>
$fee
,
'months'
=>
$months
,
];
}
public
static
function
completeRenewal
(
int
$cardId
,
int
$paymentId
)
:
array
{
return
self
::
applyRenewal
(
$cardId
,
1
,
$paymentId
);
}
private
static
function
applyRenewal
(
int
$cardId
,
int
$months
,
?
int
$paymentId
)
:
array
{
$db
=
App
::
getInstance
()
->
db
();
$card
=
$db
->
selectOne
(
"SELECT * FROM sa_player_cards WHERE id = ? AND is_archived = 0"
,
[
$cardId
]
);
if
(
!
$card
)
{
return
[
'success'
=>
false
,
'error'
=>
'الكارت غير موجود'
];
}
$baseDate
=
(
$card
[
'valid_until'
]
&&
$card
[
'valid_until'
]
>=
date
(
'Y-m-d'
))
?
$card
[
'valid_until'
]
:
date
(
'Y-m-d'
);
$newValidUntil
=
date
(
'Y-m-d'
,
strtotime
(
"+
{
$months
}
months"
,
strtotime
(
$baseDate
)));
$db
->
update
(
'sa_player_cards'
,
[
'status'
=>
SaConstants
::
CARD_ACTIVE
,
'valid_until'
=>
$newValidUntil
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[
$cardId
]);
$db
->
update
(
'sa_players'
,
[
'card_status'
=>
SaConstants
::
CARD_ACTIVE
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[(
int
)
$card
[
'player_id'
]]);
EventBus
::
dispatch
(
'sa.card.renewed'
,
[
'card_id'
=>
$cardId
,
'player_id'
=>
(
int
)
$card
[
'player_id'
],
'valid_until'
=>
$newValidUntil
,
'payment_id'
=>
$paymentId
,
]);
return
[
'success'
=>
true
,
'card_id'
=>
$cardId
,
'valid_until'
=>
$newValidUntil
,
];
}
}
app/Modules/SportsActivity/Services/DiscountCalculatorService.php
0 → 100644
View file @
c674d6f6
<?php
declare
(
strict_types
=
1
);
namespace
App\Modules\SportsActivity\Services
;
use
App\Core\App
;
final
class
DiscountCalculatorService
{
/**
* Calculate all applicable discounts for a subscription amount.
*/
public
static
function
calculateDiscounts
(
float
$subscriptionAmount
,
int
$months
,
bool
$hasSibling
,
?
string
$academyCode
=
null
)
:
array
{
$discounts
=
[];
$totalDiscount
=
0.0
;
if
(
$months
>=
3
)
{
$advance
=
self
::
getAdvanceDiscount
(
$subscriptionAmount
);
if
(
$advance
)
{
$totalDiscount
+=
$advance
[
'amount'
];
$discounts
[]
=
$advance
;
}
}
if
(
$hasSibling
)
{
$remaining
=
$subscriptionAmount
-
$totalDiscount
;
$sibling
=
self
::
getSiblingDiscount
(
$remaining
,
$academyCode
);
if
(
$sibling
)
{
$totalDiscount
+=
$sibling
[
'amount'
];
$discounts
[]
=
$sibling
;
}
}
return
[
'discounts'
=>
$discounts
,
'total_discount'
=>
round
(
$totalDiscount
,
2
),
'final_amount'
=>
round
(
$subscriptionAmount
-
$totalDiscount
,
2
),
];
}
private
static
function
getAdvanceDiscount
(
float
$amount
)
:
?
array
{
$db
=
App
::
getInstance
()
->
db
();
$rule
=
$db
->
selectOne
(
"SELECT * FROM sa_discount_rules WHERE rule_code = 'ADVANCE_3MONTHS_15PCT' AND is_active = 1"
,
[]
);
if
(
!
$rule
)
{
return
null
;
}
$discountAmount
=
$amount
*
((
float
)
$rule
[
'discount_value'
]
/
100
);
return
[
'code'
=>
'ADVANCE_3MONTHS_15PCT'
,
'name_ar'
=>
$rule
[
'name_ar'
],
'amount'
=>
round
(
$discountAmount
,
2
),
'percentage'
=>
(
float
)
$rule
[
'discount_value'
],
];
}
private
static
function
getSiblingDiscount
(
float
$remainingAmount
,
?
string
$academyCode
)
:
?
array
{
$db
=
App
::
getInstance
()
->
db
();
$rule
=
$db
->
selectOne
(
"SELECT * FROM sa_discount_rules WHERE condition_type = 'sibling' AND is_active = 1"
,
[]
);
if
(
!
$rule
)
{
return
null
;
}
$ruleAcademy
=
$rule
[
'academy_code'
]
??
null
;
if
(
$ruleAcademy
&&
$academyCode
&&
$ruleAcademy
!==
$academyCode
)
{
return
null
;
}
$discountAmount
=
$remainingAmount
*
((
float
)
$rule
[
'discount_value'
]
/
100
);
return
[
'code'
=>
$rule
[
'rule_code'
],
'name_ar'
=>
$rule
[
'name_ar'
],
'amount'
=>
round
(
$discountAmount
,
2
),
'percentage'
=>
(
float
)
$rule
[
'discount_value'
],
];
}
}
app/Modules/SportsActivity/Services/EnrollmentService.php
View file @
c674d6f6
...
...
@@ -5,6 +5,7 @@ namespace App\Modules\SportsActivity\Services;
use
App\Core\App
;
use
App\Modules\Cashier\Services\PaymentRequestService
;
use
App\Modules\SportsActivity\SaConstants
;
final
class
EnrollmentService
{
...
...
@@ -20,11 +21,11 @@ final class EnrollmentService
return
[
'success'
=>
false
,
'error'
=>
'اللاعب غير مسجل في النظام'
];
}
if
(
!
in_array
(
$player
[
'medical_status'
],
[
'fit'
,
'conditional'
]))
{
if
(
!
in_array
(
$player
[
'medical_status'
],
[
SaConstants
::
MEDICAL_FIT
,
SaConstants
::
MEDICAL_CONDITIONAL
]))
{
return
[
'success'
=>
false
,
'error'
=>
'الحالة الطبية للاعب لا تسمح بالتسجيل ('
.
$player
[
'medical_status'
]
.
')'
];
}
if
(
$player
[
'medical_status'
]
===
'fit'
&&
$player
[
'medical_expiry_date'
])
{
if
(
$player
[
'medical_status'
]
===
SaConstants
::
MEDICAL_FIT
&&
$player
[
'medical_expiry_date'
])
{
if
(
$player
[
'medical_expiry_date'
]
<
date
(
'Y-m-d'
))
{
return
[
'success'
=>
false
,
'error'
=>
'الشهادة الطبية منتهية الصلاحية'
];
}
...
...
@@ -35,10 +36,29 @@ final class EnrollmentService
return
[
'success'
=>
false
,
'error'
=>
'المجموعة غير موجودة'
];
}
if
(
$group
[
'status'
]
!==
'active'
)
{
if
(
$group
[
'status'
]
!==
SaConstants
::
GROUP_ACTIVE
)
{
return
[
'success'
=>
false
,
'error'
=>
'المجموعة غير نشطة'
];
}
// Age validation (backwards compatible — skips if columns are NULL)
if
(
!
empty
(
$group
[
'min_age'
])
||
!
empty
(
$group
[
'max_age'
]))
{
$playerAge
=
null
;
if
(
!
empty
(
$player
[
'date_of_birth'
]))
{
$playerAge
=
(
int
)
date_diff
(
date_create
(
$player
[
'date_of_birth'
]),
date_create
()
)
->
y
;
}
if
(
$playerAge
!==
null
)
{
if
(
!
empty
(
$group
[
'min_age'
])
&&
$playerAge
<
(
int
)
$group
[
'min_age'
])
{
return
[
'success'
=>
false
,
'error'
=>
'عمر اللاعب أقل من الحد الأدنى للمجموعة ('
.
$group
[
'min_age'
]
.
' سنة)'
];
}
if
(
!
empty
(
$group
[
'max_age'
])
&&
$playerAge
>
(
int
)
$group
[
'max_age'
])
{
return
[
'success'
=>
false
,
'error'
=>
'عمر اللاعب أكبر من الحد الأقصى للمجموعة ('
.
$group
[
'max_age'
]
.
' سنة)'
];
}
}
}
$existing
=
$db
->
selectOne
(
"SELECT id FROM sa_group_players WHERE group_id = ? AND player_id = ? AND status IN ('active','pending_payment')"
,
[
$groupId
,
$playerId
]
...
...
@@ -51,17 +71,19 @@ final class EnrollmentService
return
[
'success'
=>
false
,
'error'
=>
'المجموعة ممتلئة'
,
'suggest_waitlist'
=>
true
];
}
$fee
=
$player
[
'player_type'
]
===
'member'
$fee
=
$player
[
'player_type'
]
===
SaConstants
::
PLAYER_MEMBER
?
(
string
)
$group
[
'monthly_fee_member'
]
:
(
string
)
$group
[
'monthly_fee_nonmember'
];
$employeeId
=
(
int
)
(
App
::
getInstance
()
->
session
()
->
get
(
'employee_id'
)
??
0
);
$db
->
beginTransaction
();
try
{
$enrollmentId
=
$db
->
insert
(
'sa_group_players'
,
[
'group_id'
=>
$groupId
,
'player_id'
=>
$playerId
,
'enrolled_at'
=>
date
(
'Y-m-d'
),
'status'
=>
'pending_payment'
,
'status'
=>
SaConstants
::
STATUS_PENDING_PAYMENT
,
'created_at'
=>
date
(
'Y-m-d H:i:s'
),
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
'created_by'
=>
$employeeId
,
...
...
@@ -72,10 +94,10 @@ final class EnrollmentService
$requestResult
=
PaymentRequestService
::
createRequest
([
'member_id'
=>
$memberId
,
'payment_type'
=>
'sports_registration'
,
'payment_type'
=>
SaConstants
::
PAY_TYPE_SPORTS_REG
,
'amount'
=>
$fee
,
'description_ar'
=>
$description
,
'related_entity_type'
=>
'sa_group_players'
,
'related_entity_type'
=>
SaConstants
::
ENTITY_GROUP_PLAYERS
,
'related_entity_id'
=>
$enrollmentId
,
]);
...
...
@@ -86,6 +108,12 @@ final class EnrollmentService
],
'id = ?'
,
[
$enrollmentId
]);
}
$db
->
commit
();
}
catch
(
\Throwable
$e
)
{
$db
->
rollBack
();
return
[
'success'
=>
false
,
'error'
=>
'فشل عملية التسجيل: '
.
$e
->
getMessage
()];
}
return
[
'success'
=>
true
,
'enrollment_id'
=>
$enrollmentId
,
...
...
@@ -99,26 +127,30 @@ final class EnrollmentService
{
$db
=
App
::
getInstance
()
->
db
();
$db
->
beginTransaction
();
try
{
$enrollment
=
$db
->
selectOne
(
"SELECT id, status FROM sa_group_players WHERE group_id = ? AND player_id = ? AND status IN ('active','pending_payment')"
,
[
$groupId
,
$playerId
]
);
if
(
!
$enrollment
)
{
$db
->
rollBack
();
return
[
'success'
=>
false
,
'error'
=>
'اللاعب غير مسجل في هذه المجموعة'
];
}
$wasActive
=
$enrollment
[
'status'
]
===
'active'
;
$wasActive
=
$enrollment
[
'status'
]
===
SaConstants
::
STATUS_ACTIVE
;
$db
->
update
(
'sa_group_players'
,
[
'status'
=>
'withdrawn'
,
'status'
=>
SaConstants
::
STATUS_WITHDRAWN
,
'left_at'
=>
date
(
'Y-m-d'
),
'notes'
=>
$reason
?:
null
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[(
int
)
$enrollment
[
'id'
]]);
$newCount
=
null
;
if
(
$wasActive
)
{
$group
=
$db
->
selectOne
(
"SELECT current_count FROM sa_groups WHERE id = ?
"
,
[
$groupId
]);
$group
=
$db
->
selectOne
(
"SELECT current_count FROM sa_groups WHERE id = ? FOR UPDATE
"
,
[
$groupId
]);
$newCount
=
max
(
0
,
(
int
)
$group
[
'current_count'
]
-
1
);
$db
->
update
(
'sa_groups'
,
[
...
...
@@ -126,32 +158,41 @@ final class EnrollmentService
'is_full'
=>
0
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[
$groupId
]);
}
$db
->
commit
();
return
[
'success'
=>
true
,
'new_count'
=>
$newCount
];
}
catch
(
\Throwable
$e
)
{
$db
->
rollBack
();
return
[
'success'
=>
false
,
'error'
=>
'فشل عملية الانسحاب: '
.
$e
->
getMessage
()];
}
return
[
'success'
=>
true
,
'new_count'
=>
null
];
}
public
static
function
activateEnrollment
(
int
$enrollmentId
,
int
$paymentId
)
:
bool
{
$db
=
App
::
getInstance
()
->
db
();
$db
->
beginTransaction
();
try
{
$enrollment
=
$db
->
selectOne
(
"SELECT * FROM sa_group_players WHERE id = ? AND status = 'pending_payment'"
,
[
$enrollmentId
]
);
if
(
!
$enrollment
)
{
$db
->
rollBack
();
return
false
;
}
$db
->
update
(
'sa_group_players'
,
[
'status'
=>
'active'
,
'status'
=>
SaConstants
::
STATUS_ACTIVE
,
'activated_by_payment_id'
=>
$paymentId
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[
$enrollmentId
]);
$group
=
$db
->
selectOne
(
"SELECT current_count, max_capacity FROM sa_groups WHERE id = ?"
,
[(
int
)
$enrollment
[
'group_id'
]]);
$group
=
$db
->
selectOne
(
"SELECT current_count, max_capacity FROM sa_groups WHERE id = ? FOR UPDATE"
,
[(
int
)
$enrollment
[
'group_id'
]]
);
$newCount
=
(
int
)
$group
[
'current_count'
]
+
1
;
$isFull
=
$newCount
>=
(
int
)
$group
[
'max_capacity'
]
?
1
:
0
;
...
...
@@ -161,28 +202,39 @@ final class EnrollmentService
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[(
int
)
$enrollment
[
'group_id'
]]);
$db
->
commit
();
return
true
;
}
catch
(
\Throwable
$e
)
{
$db
->
rollBack
();
return
false
;
}
}
public
static
function
deactivateEnrollment
(
int
$enrollmentId
)
:
bool
{
$db
=
App
::
getInstance
()
->
db
();
$db
->
beginTransaction
();
try
{
$enrollment
=
$db
->
selectOne
(
"SELECT * FROM sa_group_players WHERE id = ? AND status = 'active'"
,
[
$enrollmentId
]
);
if
(
!
$enrollment
)
{
$db
->
rollBack
();
return
false
;
}
$db
->
update
(
'sa_group_players'
,
[
'status'
=>
'pending_payment'
,
'status'
=>
SaConstants
::
STATUS_PENDING_PAYMENT
,
'activated_by_payment_id'
=>
null
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[
$enrollmentId
]);
$group
=
$db
->
selectOne
(
"SELECT current_count FROM sa_groups WHERE id = ?"
,
[(
int
)
$enrollment
[
'group_id'
]]);
$group
=
$db
->
selectOne
(
"SELECT current_count FROM sa_groups WHERE id = ? FOR UPDATE"
,
[(
int
)
$enrollment
[
'group_id'
]]
);
$newCount
=
max
(
0
,
(
int
)
$group
[
'current_count'
]
-
1
);
$db
->
update
(
'sa_groups'
,
[
...
...
@@ -191,6 +243,11 @@ final class EnrollmentService
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[(
int
)
$enrollment
[
'group_id'
]]);
$db
->
commit
();
return
true
;
}
catch
(
\Throwable
$e
)
{
$db
->
rollBack
();
return
false
;
}
}
}
app/Modules/SportsActivity/Services/GateAccessService.php
View file @
c674d6f6
...
...
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace
App\Modules\SportsActivity\Services
;
use
App\Core\App
;
use
App\Core\EventBus
;
final
class
GateAccessService
{
...
...
@@ -29,6 +30,7 @@ final class GateAccessService
}
if
(
$card
[
'status'
]
===
'suspended'
)
{
self
::
dispatchDenialEvent
(
$cardNumber
,
$card
,
'الكارت موقوف'
);
return
[
'granted'
=>
false
,
'reason'
=>
'الكارت موقوف'
,
...
...
@@ -37,6 +39,7 @@ final class GateAccessService
}
if
(
$card
[
'status'
]
===
'revoked'
)
{
self
::
dispatchDenialEvent
(
$cardNumber
,
$card
,
'الكارت ملغى'
);
return
[
'granted'
=>
false
,
'reason'
=>
'الكارت ملغى'
,
...
...
@@ -45,6 +48,7 @@ final class GateAccessService
}
if
(
$card
[
'status'
]
===
'expired'
)
{
self
::
dispatchDenialEvent
(
$cardNumber
,
$card
,
'الكارت منتهي الصلاحية'
);
return
[
'granted'
=>
false
,
'reason'
=>
'الكارت منتهي الصلاحية'
,
...
...
@@ -58,6 +62,7 @@ final class GateAccessService
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[(
int
)
$card
[
'id'
]]);
self
::
dispatchDenialEvent
(
$cardNumber
,
$card
,
'الكارت منتهي الصلاحية'
);
return
[
'granted'
=>
false
,
'reason'
=>
'الكارت منتهي الصلاحية'
,
...
...
@@ -66,11 +71,13 @@ final class GateAccessService
}
if
(
!
in_array
(
$card
[
'status'
],
[
'active'
,
'temporary'
],
true
))
{
return
[
$result
=
[
'granted'
=>
false
,
'reason'
=>
'حالة الكارت غير صالحة للدخول ('
.
$card
[
'status'
]
.
')'
,
'card'
=>
$card
,
];
self
::
dispatchDenialEvent
(
$cardNumber
,
$card
,
$result
[
'reason'
]);
return
$result
;
}
return
[
...
...
@@ -83,6 +90,16 @@ final class GateAccessService
];
}
private
static
function
dispatchDenialEvent
(
?
string
$cardNumber
,
?
array
$card
,
string
$reason
)
:
void
{
EventBus
::
dispatch
(
'sa.gate.access_denied'
,
[
'player_id'
=>
(
int
)
(
$card
[
'player_id'
]
??
0
),
'player_name'
=>
$card
[
'full_name_ar'
]
??
''
,
'card_number'
=>
$cardNumber
??
''
,
'reason'
=>
$reason
,
]);
}
public
static
function
recordEntry
(
int
$playerId
,
?
int
$cardId
,
string
$accessPoint
=
''
)
:
int
{
$db
=
App
::
getInstance
()
->
db
();
...
...
app/Modules/SportsActivity/Services/GroupTransferService.php
0 → 100644
View file @
c674d6f6
<?php
declare
(
strict_types
=
1
);
namespace
App\Modules\SportsActivity\Services
;
use
App\Core\App
;
use
App\Core\EventBus
;
use
App\Modules\SportsActivity\SaConstants
;
final
class
GroupTransferService
{
public
static
function
transfer
(
int
$playerId
,
int
$fromGroupId
,
int
$toGroupId
,
string
$reason
=
''
)
:
array
{
$db
=
App
::
getInstance
()
->
db
();
if
(
$fromGroupId
===
$toGroupId
)
{
return
[
'success'
=>
false
,
'error'
=>
'لا يمكن النقل لنفس المجموعة'
];
}
$db
->
beginTransaction
();
try
{
$enrollment
=
$db
->
selectOne
(
"SELECT id FROM sa_group_players WHERE group_id = ? AND player_id = ? AND status = ?"
,
[
$fromGroupId
,
$playerId
,
SaConstants
::
STATUS_ACTIVE
]
);
if
(
!
$enrollment
)
{
$db
->
rollBack
();
return
[
'success'
=>
false
,
'error'
=>
'اللاعب غير مسجل في المجموعة المصدر'
];
}
$toGroup
=
$db
->
selectOne
(
"SELECT current_count, max_capacity FROM sa_groups WHERE id = ? AND status = ? AND is_archived = 0 FOR UPDATE"
,
[
$toGroupId
,
SaConstants
::
GROUP_ACTIVE
]
);
if
(
!
$toGroup
)
{
$db
->
rollBack
();
return
[
'success'
=>
false
,
'error'
=>
'المجموعة المستهدفة غير موجودة أو غير نشطة'
];
}
if
((
int
)
$toGroup
[
'current_count'
]
>=
(
int
)
$toGroup
[
'max_capacity'
])
{
$db
->
rollBack
();
return
[
'success'
=>
false
,
'error'
=>
'المجموعة المستهدفة ممتلئة'
];
}
$fromGroup
=
$db
->
selectOne
(
"SELECT current_count FROM sa_groups WHERE id = ? FOR UPDATE"
,
[
$fromGroupId
]
);
$employeeId
=
(
int
)
(
App
::
getInstance
()
->
session
()
->
get
(
'employee_id'
)
??
0
);
$now
=
date
(
'Y-m-d H:i:s'
);
$db
->
update
(
'sa_group_players'
,
[
'status'
=>
SaConstants
::
STATUS_TRANSFERRED
,
'left_at'
=>
date
(
'Y-m-d'
),
'transferred_to_group_id'
=>
$toGroupId
,
'transfer_reason'
=>
$reason
?:
null
,
'updated_at'
=>
$now
,
],
'id = ?'
,
[(
int
)
$enrollment
[
'id'
]]);
$newEnrollmentId
=
$db
->
insert
(
'sa_group_players'
,
[
'group_id'
=>
$toGroupId
,
'player_id'
=>
$playerId
,
'enrolled_at'
=>
date
(
'Y-m-d'
),
'status'
=>
SaConstants
::
STATUS_ACTIVE
,
'created_at'
=>
$now
,
'updated_at'
=>
$now
,
'created_by'
=>
$employeeId
,
]);
$newFromCount
=
max
(
0
,
(
int
)
$fromGroup
[
'current_count'
]
-
1
);
$db
->
update
(
'sa_groups'
,
[
'current_count'
=>
$newFromCount
,
'is_full'
=>
0
,
'updated_at'
=>
$now
,
],
'id = ?'
,
[
$fromGroupId
]);
$newToCount
=
(
int
)
$toGroup
[
'current_count'
]
+
1
;
$db
->
update
(
'sa_groups'
,
[
'current_count'
=>
$newToCount
,
'is_full'
=>
$newToCount
>=
(
int
)
$toGroup
[
'max_capacity'
]
?
1
:
0
,
'updated_at'
=>
$now
,
],
'id = ?'
,
[
$toGroupId
]);
$db
->
commit
();
EventBus
::
dispatch
(
'sa.player.transferred'
,
[
'player_id'
=>
$playerId
,
'from_group_id'
=>
$fromGroupId
,
'to_group_id'
=>
$toGroupId
,
'enrollment_id'
=>
$newEnrollmentId
,
'reason'
=>
$reason
,
]);
return
[
'success'
=>
true
,
'enrollment_id'
=>
$newEnrollmentId
,
];
}
catch
(
\Throwable
$e
)
{
$db
->
rollBack
();
return
[
'success'
=>
false
,
'error'
=>
'فشل عملية النقل: '
.
$e
->
getMessage
()];
}
}
}
app/Modules/SportsActivity/Services/NumberGeneratorService.php
0 → 100644
View file @
c674d6f6
<?php
declare
(
strict_types
=
1
);
namespace
App\Modules\SportsActivity\Services
;
use
App\Core\App
;
final
class
NumberGeneratorService
{
public
static
function
subscriptionNumber
()
:
string
{
return
self
::
generateWithRetry
(
'sa_subscriptions'
,
'subscription_number'
,
'SUB'
);
}
public
static
function
bookingNumber
()
:
string
{
return
self
::
generateWithRetry
(
'sa_bookings'
,
'booking_number'
,
'BK'
);
}
private
static
function
generateWithRetry
(
string
$table
,
string
$column
,
string
$prefix
,
int
$maxAttempts
=
10
)
:
string
{
$db
=
App
::
getInstance
()
->
db
();
$datePrefix
=
$prefix
.
'-'
.
date
(
'Ymd'
)
.
'-'
;
for
(
$attempt
=
0
;
$attempt
<
$maxAttempts
;
$attempt
++
)
{
$number
=
$datePrefix
.
str_pad
((
string
)
random_int
(
1
,
9999
),
4
,
'0'
,
STR_PAD_LEFT
);
$exists
=
$db
->
selectOne
(
"SELECT id FROM `
{
$table
}
` WHERE `
{
$column
}
` = ?"
,
[
$number
]
);
if
(
!
$exists
)
{
return
$number
;
}
}
return
$datePrefix
.
str_pad
((
string
)
random_int
(
10000
,
99999
),
5
,
'0'
,
STR_PAD_LEFT
);
}
}
app/Modules/SportsActivity/Services/PoolGridService.php
View file @
c674d6f6
...
...
@@ -148,6 +148,17 @@ final class PoolGridService
$label
=
$notes
?:
'حجز ساعة'
;
}
// Pre-fetch all active bookings for this facility+date to avoid per-cell SELECT
$allConflicts
=
$db
->
select
(
"SELECT zone_row, zone_col, start_time FROM sa_pool_zone_bookings
WHERE facility_id = ? AND booking_date = ? AND status = 'active'"
,
[
$facilityId
,
$date
]
);
$conflictSet
=
[];
foreach
(
$allConflicts
as
$c
)
{
$conflictSet
[
$c
[
'start_time'
]
.
':'
.
$c
[
'zone_row'
]
.
':'
.
$c
[
'zone_col'
]]
=
true
;
}
$assigned
=
0
;
$skipped
=
0
;
...
...
@@ -168,14 +179,8 @@ final class PoolGridService
continue
;
}
$conflict
=
$db
->
selectOne
(
"SELECT id FROM sa_pool_zone_bookings
WHERE facility_id = ? AND booking_date = ? AND start_time = ?
AND zone_row = ? AND zone_col = ? AND status = 'active'"
,
[
$facilityId
,
$date
,
$startTimeFull
,
$row
,
$col
]
);
if
(
$conflict
)
{
$key
=
$startTimeFull
.
':'
.
$row
.
':'
.
$col
;
if
(
isset
(
$conflictSet
[
$key
]))
{
$skipped
++
;
continue
;
}
...
...
@@ -203,6 +208,7 @@ final class PoolGridService
}
$db
->
insert
(
'sa_pool_zone_bookings'
,
$insertData
);
$conflictSet
[
$key
]
=
true
;
$assigned
++
;
}
}
...
...
@@ -220,6 +226,17 @@ final class PoolGridService
$employeeId
=
(
int
)
(
App
::
getInstance
()
->
session
()
->
get
(
'employee_id'
)
??
0
);
$cleared
=
0
;
// Pre-fetch all active bookings for this facility+date
$allActive
=
$db
->
select
(
"SELECT id, zone_row, zone_col, start_time FROM sa_pool_zone_bookings
WHERE facility_id = ? AND booking_date = ? AND status = 'active'"
,
[
$facilityId
,
$date
]
);
$activeMap
=
[];
foreach
(
$allActive
as
$a
)
{
$activeMap
[
$a
[
'start_time'
]
.
':'
.
$a
[
'zone_row'
]
.
':'
.
$a
[
'zone_col'
]]
=
(
int
)
$a
[
'id'
];
}
foreach
(
$slots
as
$slot
)
{
$startTime
=
$slot
[
'start_time'
]
??
''
;
if
(
!
$startTime
)
continue
;
...
...
@@ -231,20 +248,14 @@ final class PoolGridService
if
(
$row
<
0
||
$col
<
0
)
continue
;
$existing
=
$db
->
selectOne
(
"SELECT id FROM sa_pool_zone_bookings
WHERE facility_id = ? AND booking_date = ? AND start_time = ?
AND zone_row = ? AND zone_col = ? AND status = 'active'"
,
[
$facilityId
,
$date
,
$startTimeFull
,
$row
,
$col
]
);
if
(
$existing
)
{
$key
=
$startTimeFull
.
':'
.
$row
.
':'
.
$col
;
if
(
isset
(
$activeMap
[
$key
]))
{
$db
->
update
(
'sa_pool_zone_bookings'
,
[
'status'
=>
'cancelled'
,
'cancelled_by'
=>
$employeeId
,
'cancelled_at'
=>
$ts
,
'updated_at'
=>
$ts
,
],
'id = ?'
,
[
(
int
)
$existing
[
'id'
]]);
],
'id = ?'
,
[
$activeMap
[
$key
]]);
$cleared
++
;
}
}
...
...
app/Modules/SportsActivity/Services/RegistrationWizardService.php
View file @
c674d6f6
...
...
@@ -197,50 +197,25 @@ final class RegistrationWizardService
:
(
float
)
$group
[
'monthly_fee_nonmember'
];
$subscriptionAmount
=
$monthlyFee
*
max
(
1
,
$months
);
$discounts
=
[];
$totalDiscount
=
0.0
;
// 15% discount for 3-month advance payment
if
(
$months
>=
3
)
{
$advanceRule
=
$db
->
selectOne
(
"SELECT * FROM sa_discount_rules WHERE rule_code = 'ADVANCE_3MONTHS_15PCT' AND is_active = 1"
,
[]
);
if
(
$advanceRule
)
{
$discountAmt
=
$subscriptionAmount
*
((
float
)
$advanceRule
[
'discount_value'
]
/
100
);
$totalDiscount
+=
$discountAmt
;
$discounts
[]
=
[
'code'
=>
'ADVANCE_3MONTHS_15PCT'
,
'name_ar'
=>
$advanceRule
[
'name_ar'
],
'amount'
=>
round
(
$discountAmt
,
2
)];
}
}
// Sibling discount (if applicable to this academy)
if
(
$hasSibling
)
{
$siblingRule
=
$db
->
selectOne
(
"SELECT * FROM sa_discount_rules WHERE condition_type = 'sibling' AND is_active = 1"
,
[]
);
if
(
$siblingRule
)
{
$acadCode
=
$siblingRule
[
'academy_code'
];
$applyDiscount
=
true
;
if
(
$acadCode
)
{
$program
=
$db
->
selectOne
(
"SELECT p.academy_id FROM sa_programs p WHERE p.id = ?"
,
[(
int
)
$group
[
'program_id'
]]);
// Resolve academy code for discount scoping
$academyCode
=
null
;
if
(
!
empty
(
$group
[
'program_id'
]))
{
$program
=
$db
->
selectOne
(
"SELECT academy_id FROM sa_programs WHERE id = ?"
,
[(
int
)
$group
[
'program_id'
]]);
if
(
$program
)
{
$academy
=
$db
->
selectOne
(
"SELECT code FROM sa_academies WHERE id = ?"
,
[(
int
)
$program
[
'academy_id'
]]);
if
(
!
$academy
||
$academy
[
'code'
]
!==
$acadCode
)
{
$applyDiscount
=
false
;
}
}
}
if
(
$applyDiscount
)
{
$remaining
=
$subscriptionAmount
-
$totalDiscount
;
$discountAmt
=
$remaining
*
((
float
)
$siblingRule
[
'discount_value'
]
/
100
);
$totalDiscount
+=
$discountAmt
;
$discounts
[]
=
[
'code'
=>
$siblingRule
[
'rule_code'
],
'name_ar'
=>
$siblingRule
[
'name_ar'
],
'amount'
=>
round
(
$discountAmt
,
2
)];
}
$academyCode
=
$academy
[
'code'
]
??
null
;
}
}
$subscriptionAfterDiscount
=
round
(
$subscriptionAmount
-
$totalDiscount
,
2
);
$discountResult
=
DiscountCalculatorService
::
calculateDiscounts
(
$subscriptionAmount
,
$months
,
$hasSibling
,
$academyCode
);
$subscriptionAfterDiscount
=
$discountResult
[
'final_amount'
];
$totalFees
=
(
float
)
$registration
[
'registration_fee'
]
+
(
float
)
$registration
[
'card_fee'
]
...
...
@@ -261,8 +236,8 @@ final class RegistrationWizardService
'monthly_fee'
=>
$monthlyFee
,
'months'
=>
$months
,
'subscription_before_discount'
=>
$subscriptionAmount
,
'discounts'
=>
$discount
s
,
'total_discount'
=>
round
(
$totalDiscount
,
2
)
,
'discounts'
=>
$discount
Result
[
'discounts'
]
,
'total_discount'
=>
$discountResult
[
'total_discount'
]
,
'subscription_amount'
=>
$subscriptionAfterDiscount
,
'total_fees'
=>
$totalFees
,
];
...
...
@@ -490,6 +465,10 @@ final class RegistrationWizardService
return
[
'success'
=>
false
,
'error'
=>
'التسجيل غير موجود'
];
}
$groupId
=
(
int
)
(
$registration
[
'group_id'
]
??
0
);
$db
->
beginTransaction
();
try
{
$db
->
update
(
'sa_registrations'
,
[
'status'
=>
'completed'
,
'payment_status'
=>
'paid'
,
...
...
@@ -502,7 +481,6 @@ final class RegistrationWizardService
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[(
int
)
$registration
[
'player_id'
]]);
$groupId
=
(
int
)
(
$registration
[
'group_id'
]
??
0
);
if
(
$groupId
>
0
)
{
$existingEnrollment
=
$db
->
selectOne
(
"SELECT id FROM sa_group_players WHERE group_id = ? AND player_id = ? AND status IN ('active','pending_payment')"
,
...
...
@@ -511,7 +489,7 @@ final class RegistrationWizardService
if
(
!
$existingEnrollment
)
{
$employeeId
=
(
int
)
(
$registration
[
'created_by'
]
??
0
);
$enrollmentId
=
$db
->
insert
(
'sa_group_players'
,
[
$db
->
insert
(
'sa_group_players'
,
[
'group_id'
=>
$groupId
,
'player_id'
=>
(
int
)
$registration
[
'player_id'
],
'enrolled_at'
=>
date
(
'Y-m-d'
),
...
...
@@ -522,7 +500,7 @@ final class RegistrationWizardService
'created_by'
=>
$employeeId
,
]);
$group
=
$db
->
selectOne
(
"SELECT current_count, max_capacity FROM sa_groups WHERE id = ?
"
,
[
$groupId
]);
$group
=
$db
->
selectOne
(
"SELECT current_count, max_capacity FROM sa_groups WHERE id = ? FOR UPDATE
"
,
[
$groupId
]);
$newCount
=
(
int
)
$group
[
'current_count'
]
+
1
;
$db
->
update
(
'sa_groups'
,
[
'current_count'
=>
$newCount
,
...
...
@@ -532,6 +510,12 @@ final class RegistrationWizardService
}
}
$db
->
commit
();
}
catch
(
\Throwable
$e
)
{
$db
->
rollBack
();
return
[
'success'
=>
false
,
'error'
=>
'فشل إتمام التسجيل: '
.
$e
->
getMessage
()];
}
EventBus
::
dispatch
(
'sa.registration.completed'
,
[
'registration_id'
=>
$registrationId
,
'player_id'
=>
(
int
)
$registration
[
'player_id'
],
...
...
app/Modules/SportsActivity/Services/SaEventListenerService.php
0 → 100644
View file @
c674d6f6
<?php
declare
(
strict_types
=
1
);
namespace
App\Modules\SportsActivity\Services
;
use
App\Core\App
;
use
App\Core\Logger
;
use
App\Modules\SportsActivity\SaConstants
;
final
class
SaEventListenerService
{
public
static
function
handlePaymentCompleted
(
array
$data
)
:
void
{
$entityType
=
$data
[
'related_entity_type'
]
??
''
;
try
{
if
(
$entityType
===
SaConstants
::
ENTITY_REG_FORM
)
{
self
::
handleRegistrationFormPaid
((
int
)
(
$data
[
'related_entity_id'
]
??
0
));
return
;
}
if
(
$entityType
===
SaConstants
::
ENTITY_REGISTRATIONS
)
{
self
::
handleRegistrationPaid
(
(
int
)
(
$data
[
'related_entity_id'
]
??
0
),
(
int
)
(
$data
[
'payment_id'
]
??
0
)
);
return
;
}
if
(
$entityType
===
SaConstants
::
ENTITY_SUBSCRIPTIONS
)
{
self
::
handleSubscriptionPaid
(
$data
);
return
;
}
if
(
$entityType
===
SaConstants
::
ENTITY_BOOKINGS
)
{
self
::
handleBookingPaid
(
$data
);
return
;
}
if
(
$entityType
===
SaConstants
::
ENTITY_GROUP_PLAYERS
)
{
self
::
handleEnrollmentPaid
(
(
int
)
(
$data
[
'related_entity_id'
]
??
0
),
(
int
)
(
$data
[
'payment_id'
]
??
0
),
$data
);
return
;
}
if
(
$entityType
===
SaConstants
::
ENTITY_PLAYER_CARDS
)
{
CardRenewalService
::
completeRenewal
(
(
int
)
(
$data
[
'related_entity_id'
]
??
0
),
(
int
)
(
$data
[
'payment_id'
]
??
0
)
);
return
;
}
}
catch
(
\Throwable
$e
)
{
Logger
::
error
(
'SA payment_request.completed listener failed: '
.
$e
->
getMessage
());
}
}
public
static
function
handlePaymentVoided
(
array
$data
)
:
void
{
try
{
$db
=
App
::
getInstance
()
->
db
();
$paymentId
=
(
int
)
(
$data
[
'payment_id'
]
??
0
);
if
(
$paymentId
<
1
)
{
return
;
}
$sub
=
$db
->
selectOne
(
"SELECT id FROM sa_subscriptions WHERE payment_id = ?"
,
[
$paymentId
]);
if
(
$sub
)
{
$db
->
update
(
'sa_subscriptions'
,
[
'payment_status'
=>
SaConstants
::
PAYMENT_UNPAID
,
'paid_at'
=>
null
,
'paid_amount'
=>
null
,
'payment_id'
=>
null
,
'receipt_id'
=>
null
,
'receipt_number'
=>
null
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[(
int
)
$sub
[
'id'
]]);
}
$bk
=
$db
->
selectOne
(
"SELECT id FROM sa_bookings WHERE payment_id = ?"
,
[
$paymentId
]);
if
(
$bk
)
{
$db
->
update
(
'sa_bookings'
,
[
'payment_status'
=>
SaConstants
::
PAYMENT_UNPAID
,
'payment_id'
=>
null
,
'receipt_id'
=>
null
,
'receipt_number'
=>
null
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[(
int
)
$bk
[
'id'
]]);
}
$enrollment
=
$db
->
selectOne
(
"SELECT id FROM sa_group_players WHERE activated_by_payment_id = ? AND status = 'active'"
,
[
$paymentId
]
);
if
(
$enrollment
)
{
EnrollmentService
::
deactivateEnrollment
((
int
)
$enrollment
[
'id'
]);
}
}
catch
(
\Throwable
$e
)
{
Logger
::
error
(
'SA payment.voided listener failed: '
.
$e
->
getMessage
());
}
}
private
static
function
handleRegistrationFormPaid
(
int
$registrationId
)
:
void
{
if
(
$registrationId
<
1
)
{
return
;
}
$db
=
App
::
getInstance
()
->
db
();
$db
->
update
(
'sa_registrations'
,
[
'form_payment_status'
=>
SaConstants
::
PAYMENT_PAID
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[
$registrationId
]);
}
private
static
function
handleRegistrationPaid
(
int
$registrationId
,
int
$paymentId
)
:
void
{
if
(
$registrationId
<
1
)
{
return
;
}
RegistrationWizardService
::
completeRegistration
(
$registrationId
,
$paymentId
);
}
private
static
function
handleSubscriptionPaid
(
array
$data
)
:
void
{
$db
=
App
::
getInstance
()
->
db
();
$db
->
update
(
'sa_subscriptions'
,
[
'payment_status'
=>
SaConstants
::
PAYMENT_PAID
,
'paid_at'
=>
date
(
'Y-m-d H:i:s'
),
'paid_amount'
=>
$data
[
'amount'
]
??
0
,
'payment_id'
=>
$data
[
'payment_id'
]
??
null
,
'receipt_id'
=>
$data
[
'receipt_id'
]
??
null
,
'receipt_number'
=>
$data
[
'receipt_number'
]
??
null
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[(
int
)
(
$data
[
'related_entity_id'
]
??
0
)]);
}
private
static
function
handleBookingPaid
(
array
$data
)
:
void
{
$db
=
App
::
getInstance
()
->
db
();
$db
->
update
(
'sa_bookings'
,
[
'payment_status'
=>
SaConstants
::
PAYMENT_PAID
,
'payment_id'
=>
$data
[
'payment_id'
]
??
null
,
'receipt_id'
=>
$data
[
'receipt_id'
]
??
null
,
'receipt_number'
=>
$data
[
'receipt_number'
]
??
null
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[(
int
)
(
$data
[
'related_entity_id'
]
??
0
)]);
}
private
static
function
handleEnrollmentPaid
(
int
$enrollmentId
,
int
$paymentId
,
array
$data
)
:
void
{
if
(
$enrollmentId
<
1
||
$paymentId
<
1
)
{
return
;
}
$db
=
App
::
getInstance
()
->
db
();
EnrollmentService
::
activateEnrollment
(
$enrollmentId
,
$paymentId
);
$enrollment
=
$db
->
selectOne
(
"SELECT gp.player_id, gp.group_id, g.name_ar as group_name
FROM sa_group_players gp
LEFT JOIN sa_groups g ON g.id = gp.group_id
WHERE gp.id = ?"
,
[
$enrollmentId
]
);
if
(
$enrollment
)
{
$periodStart
=
date
(
'Y-m-d'
);
$periodEnd
=
date
(
'Y-m-t'
);
$subNumber
=
NumberGeneratorService
::
subscriptionNumber
();
$db
->
insert
(
'sa_subscriptions'
,
[
'subscription_number'
=>
$subNumber
,
'player_id'
=>
(
int
)
$enrollment
[
'player_id'
],
'group_id'
=>
(
int
)
$enrollment
[
'group_id'
],
'period_start'
=>
$periodStart
,
'period_end'
=>
$periodEnd
,
'amount'
=>
$data
[
'amount'
]
??
'0.00'
,
'discount_amount'
=>
'0.00'
,
'final_amount'
=>
$data
[
'amount'
]
??
'0.00'
,
'payment_status'
=>
SaConstants
::
PAYMENT_PAID
,
'paid_at'
=>
date
(
'Y-m-d H:i:s'
),
'paid_amount'
=>
$data
[
'amount'
]
??
'0.00'
,
'payment_id'
=>
$paymentId
,
'receipt_id'
=>
$data
[
'receipt_id'
]
??
null
,
'receipt_number'
=>
$data
[
'receipt_number'
]
??
null
,
'created_at'
=>
date
(
'Y-m-d H:i:s'
),
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
]);
}
}
}
app/Modules/SportsActivity/Services/SubscriptionGeneratorService.php
View file @
c674d6f6
...
...
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace
App\Modules\SportsActivity\Services
;
use
App\Core\App
;
use
App\Modules\SportsActivity\SaConstants
;
final
class
SubscriptionGeneratorService
{
...
...
@@ -14,55 +15,67 @@ final class SubscriptionGeneratorService
$periodStart
=
$yearMonth
.
'-01'
;
$periodEnd
=
date
(
'Y-m-t'
,
strtotime
(
$periodStart
));
$groups
=
$db
->
select
(
"SELECT g.*, p.name_ar as program_name
FROM sa_groups g
JOIN sa_programs p ON p.id = g.program_id
WHERE g.status = 'active' AND g.is_archived = 0"
,
[]
);
$generated
=
0
;
$skipped
=
0
;
$employeeId
=
(
int
)
(
App
::
getInstance
()
->
session
()
->
get
(
'employee_id'
)
??
0
);
foreach
(
$groups
as
$group
)
{
$players
=
$db
->
select
(
"SELECT gp.*, sp.player_type, sp.full_name_ar
$groupPlayers
=
$db
->
select
(
"SELECT gp.player_id, gp.group_id, gp.enrolled_at,
sp.player_type,
g.monthly_fee_member, g.monthly_fee_nonmember
FROM sa_group_players gp
JOIN sa_players sp ON sp.id = gp.player_id
WHERE gp.group_id = ? AND gp.status = 'active'"
,
[(
int
)
$group
[
'id'
]]
JOIN sa_groups g ON g.id = gp.group_id
WHERE gp.status = ?
AND g.status = ? AND g.is_archived = 0"
,
[
SaConstants
::
STATUS_ACTIVE
,
SaConstants
::
GROUP_ACTIVE
]
);
foreach
(
$players
as
$player
)
{
$existing
=
$db
->
selectOne
(
"SELECT id FROM sa_subscriptions
WHERE player_id = ? AND group_id = ? AND period_start = ?"
,
[(
int
)
$player
[
'player_id'
],
(
int
)
$group
[
'id'
],
$periodStart
]
$existingSubs
=
$db
->
select
(
"SELECT player_id, group_id FROM sa_subscriptions WHERE period_start = ?"
,
[
$periodStart
]
);
$existingSet
=
[];
foreach
(
$existingSubs
as
$es
)
{
$existingSet
[
$es
[
'player_id'
]
.
':'
.
$es
[
'group_id'
]]
=
true
;
}
if
(
$existing
)
{
$generated
=
0
;
$skipped
=
0
;
$employeeId
=
(
int
)
(
App
::
getInstance
()
->
session
()
->
get
(
'employee_id'
)
??
0
);
foreach
(
$groupPlayers
as
$gp
)
{
$key
=
$gp
[
'player_id'
]
.
':'
.
$gp
[
'group_id'
];
if
(
isset
(
$existingSet
[
$key
]))
{
$skipped
++
;
continue
;
}
$amount
=
$player
[
'player_type'
]
===
'member'
?
(
float
)
$group
[
'monthly_fee_member'
]
:
(
float
)
$group
[
'monthly_fee_nonmember'
];
$amount
=
$gp
[
'player_type'
]
===
SaConstants
::
PLAYER_MEMBER
?
(
float
)
$gp
[
'monthly_fee_member'
]
:
(
float
)
$gp
[
'monthly_fee_nonmember'
];
// Proration: first month + enrolled after 15th = half fee
$isFirstMonth
=
!
$db
->
selectOne
(
"SELECT id FROM sa_subscriptions WHERE player_id = ? AND group_id = ? AND period_start < ? LIMIT 1"
,
[(
int
)
$gp
[
'player_id'
],
(
int
)
$gp
[
'group_id'
],
$periodStart
]
);
if
(
$isFirstMonth
&&
!
empty
(
$gp
[
'enrolled_at'
]))
{
$enrollmentDay
=
(
int
)
date
(
'j'
,
strtotime
(
$gp
[
'enrolled_at'
]));
if
(
$enrollmentDay
>
15
)
{
$amount
=
round
(
$amount
/
2
,
2
);
}
}
$subNumber
=
'SUB-'
.
date
(
'Ymd'
)
.
'-'
.
str_pad
((
string
)
random_int
(
1
,
9999
),
4
,
'0'
,
STR_PAD_LEFT
);
$subNumber
=
NumberGeneratorService
::
subscriptionNumber
(
);
$db
->
insert
(
'sa_subscriptions'
,
[
'subscription_number'
=>
$subNumber
,
'player_id'
=>
(
int
)
$player
[
'player_id'
],
'group_id'
=>
(
int
)
$group
[
'
id'
],
'player_id'
=>
(
int
)
$gp
[
'player_id'
],
'group_id'
=>
(
int
)
$gp
[
'group_
id'
],
'period_start'
=>
$periodStart
,
'period_end'
=>
$periodEnd
,
'amount'
=>
$amount
,
'discount_amount'
=>
0.00
,
'final_amount'
=>
$amount
,
'payment_status'
=>
'unpaid'
,
'payment_status'
=>
SaConstants
::
PAYMENT_UNPAID
,
'paid_amount'
=>
0.00
,
'created_at'
=>
date
(
'Y-m-d H:i:s'
),
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
...
...
@@ -71,7 +84,6 @@ final class SubscriptionGeneratorService
$generated
++
;
}
}
return
[
'success'
=>
true
,
...
...
app/Modules/SportsActivity/bootstrap.php
View file @
c674d6f6
...
...
@@ -98,131 +98,63 @@ PermissionRegistry::register('sports_activity', [
// ─── Event Listeners ────────────────────────────────────────────────────────
EventBus
::
listen
(
'payment_request.completed'
,
function
(
array
$data
)
:
void
{
$entityType
=
$data
[
'related_entity_type'
]
??
''
;
try
{
if
(
$entityType
===
'sa_registration_form'
)
{
$registrationId
=
(
int
)
(
$data
[
'related_entity_id'
]
??
0
);
if
(
$registrationId
>
0
)
{
$db
=
App
::
getInstance
()
->
db
();
$db
->
update
(
'sa_registrations'
,
[
'form_payment_status'
=>
'paid'
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[
$registrationId
]);
}
return
;
}
EventBus
::
listen
(
'payment_request.completed'
,
[
\App\Modules\SportsActivity\Services\SaEventListenerService
::
class
,
'handlePaymentCompleted'
],
60
);
if
(
$entityType
===
'sa_registrations'
)
{
$registrationId
=
(
int
)
(
$data
[
'related_entity_id'
]
??
0
);
$paymentId
=
(
int
)
(
$data
[
'payment_id'
]
??
0
);
if
(
$registrationId
>
0
)
{
\App\Modules\SportsActivity\Services\RegistrationWizardService
::
completeRegistration
(
$registrationId
,
$paymentId
);
}
return
;
}
EventBus
::
listen
(
'payment.voided'
,
[
\App\Modules\SportsActivity\Services\SaEventListenerService
::
class
,
'handlePaymentVoided'
],
60
);
$db
=
App
::
getInstance
()
->
db
();
if
(
$entityType
===
'sa_subscriptions'
)
{
$db
->
update
(
'sa_subscriptions'
,
[
'payment_status'
=>
'paid'
,
'paid_at'
=>
date
(
'Y-m-d H:i:s'
),
'paid_amount'
=>
$data
[
'amount'
]
??
0
,
'payment_id'
=>
$data
[
'payment_id'
]
??
null
,
'receipt_id'
=>
$data
[
'receipt_id'
]
??
null
,
'receipt_number'
=>
$data
[
'receipt_number'
]
??
null
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[(
int
)
(
$data
[
'related_entity_id'
]
??
0
)]);
}
elseif
(
$entityType
===
'sa_bookings'
)
{
$db
->
update
(
'sa_bookings'
,
[
'payment_status'
=>
'paid'
,
'payment_id'
=>
$data
[
'payment_id'
]
??
null
,
'receipt_id'
=>
$data
[
'receipt_id'
]
??
null
,
'receipt_number'
=>
$data
[
'receipt_number'
]
??
null
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[(
int
)
(
$data
[
'related_entity_id'
]
??
0
)]);
}
elseif
(
$entityType
===
'sa_group_players'
)
{
$enrollmentId
=
(
int
)
(
$data
[
'related_entity_id'
]
??
0
);
$paymentId
=
(
int
)
(
$data
[
'payment_id'
]
??
0
);
if
(
$enrollmentId
<
1
||
$paymentId
<
1
)
return
;
\App\Modules\SportsActivity\Services\EnrollmentService
::
activateEnrollment
(
$enrollmentId
,
$paymentId
);
// ─── Notification Listeners ─────────────────────────────────────────────────
$enrollment
=
$db
->
selectOne
(
"SELECT gp.player_id, gp.group_id, g.name_ar as group_name
FROM sa_group_players gp
LEFT JOIN sa_groups g ON g.id = gp.group_id
WHERE gp.id = ?"
,
[
$enrollmentId
]
EventBus
::
listen
(
'sa.card.expiry_reminder'
,
function
(
array
$data
)
:
void
{
$phone
=
$data
[
'phone'
]
??
''
;
if
(
$phone
!==
''
)
{
\App\Modules\Notifications\Services\SmsNotificationService
::
send
(
$phone
,
'تنبيه: كارت النشاط الرياضي للاعب '
.
(
$data
[
'player_name'
]
??
''
)
.
' سينتهي بتاريخ '
.
(
$data
[
'expiry_date'
]
??
''
)
.
'. يرجى التجديد.'
);
if
(
$enrollment
)
{
$periodStart
=
date
(
'Y-m-d'
);
$periodEnd
=
date
(
'Y-m-t'
);
$subNumber
=
'SUB-'
.
date
(
'Ymd'
)
.
'-'
.
str_pad
((
string
)
random_int
(
1
,
9999
),
4
,
'0'
,
STR_PAD_LEFT
);
$db
->
insert
(
'sa_subscriptions'
,
[
'subscription_number'
=>
$subNumber
,
'player_id'
=>
(
int
)
$enrollment
[
'player_id'
],
'group_id'
=>
(
int
)
$enrollment
[
'group_id'
],
'period_start'
=>
$periodStart
,
'period_end'
=>
$periodEnd
,
'amount'
=>
$data
[
'amount'
]
??
'0.00'
,
'discount_amount'
=>
'0.00'
,
'final_amount'
=>
$data
[
'amount'
]
??
'0.00'
,
'payment_status'
=>
'paid'
,
'paid_at'
=>
date
(
'Y-m-d H:i:s'
),
'paid_amount'
=>
$data
[
'amount'
]
??
'0.00'
,
'payment_id'
=>
$paymentId
,
'receipt_id'
=>
$data
[
'receipt_id'
]
??
null
,
'receipt_number'
=>
$data
[
'receipt_number'
]
??
null
,
'created_at'
=>
date
(
'Y-m-d H:i:s'
),
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
]);
}
}
}
catch
(
\Throwable
$e
)
{
Logger
::
error
(
'SA payment_request.completed listener failed: '
.
$e
->
getMessage
());
}
},
60
);
EventBus
::
listen
(
'payment.voided'
,
function
(
array
$data
)
:
void
{
try
{
$db
=
App
::
getInstance
()
->
db
();
$paymentId
=
(
int
)
(
$data
[
'payment_id'
]
??
0
);
if
(
$paymentId
<
1
)
return
;
},
80
);
$sub
=
$db
->
selectOne
(
"SELECT id FROM sa_subscriptions WHERE payment_id = ?"
,
[
$paymentId
]);
if
(
$sub
)
{
$db
->
update
(
'sa_subscriptions'
,
[
'payment_status'
=>
'unpaid'
,
'paid_at'
=>
null
,
'paid_amount'
=>
null
,
'payment_id'
=>
null
,
'receipt_id'
=>
null
,
'receipt_number'
=>
null
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[(
int
)
$sub
[
'id'
]]);
EventBus
::
listen
(
'sa.subscription.overdue'
,
function
(
array
$data
)
:
void
{
$phone
=
$data
[
'phone'
]
??
''
;
if
(
$phone
!==
''
)
{
\App\Modules\Notifications\Services\SmsNotificationService
::
send
(
$phone
,
'تنبيه: اشتراك النشاط الرياضي للاعب '
.
(
$data
[
'player_name'
]
??
''
)
.
' متأخر السداد. المبلغ المطلوب: '
.
(
$data
[
'amount'
]
??
'0'
)
.
' جنيه.'
);
}
},
80
);
$bk
=
$db
->
selectOne
(
"SELECT id FROM sa_bookings WHERE payment_id = ?"
,
[
$paymentId
]);
if
(
$bk
)
{
$db
->
update
(
'sa_bookings'
,
[
'payment_status'
=>
'unpaid'
,
'payment_id'
=>
null
,
'receipt_id'
=>
null
,
'receipt_number'
=>
null
,
'updated_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[(
int
)
$bk
[
'id'
]]);
EventBus
::
listen
(
'sa.player.transferred'
,
function
(
array
$data
)
:
void
{
$db
=
App
::
getInstance
()
->
db
();
$player
=
$db
->
selectOne
(
"SELECT full_name_ar, phone, guardian_phone FROM sa_players WHERE id = ?"
,
[(
int
)
(
$data
[
'player_id'
]
??
0
)]
);
if
(
!
$player
)
return
;
$phone
=
$player
[
'guardian_phone'
]
?:
$player
[
'phone'
]
??
''
;
if
(
$phone
!==
''
)
{
$toGroup
=
$db
->
selectOne
(
"SELECT name_ar FROM sa_groups WHERE id = ?"
,
[(
int
)
(
$data
[
'to_group_id'
]
??
0
)]);
\App\Modules\Notifications\Services\SmsNotificationService
::
send
(
$phone
,
'تم نقل اللاعب '
.
$player
[
'full_name_ar'
]
.
' إلى مجموعة '
.
(
$toGroup
[
'name_ar'
]
??
''
)
.
' بنجاح.'
);
}
},
80
);
$enrollment
=
$db
->
selectOne
(
"SELECT id FROM sa_group_players WHERE activated_by_payment_id = ? AND status = 'active'"
,
[
$paymentId
]
EventBus
::
listen
(
'sa.player.absence_threshold'
,
function
(
array
$data
)
:
void
{
$phone
=
$data
[
'phone'
]
??
''
;
if
(
$phone
!==
''
)
{
\App\Modules\Notifications\Services\SmsNotificationService
::
send
(
$phone
,
'تنبيه: اللاعب '
.
(
$data
[
'player_name'
]
??
''
)
.
' تجاوز حد الغياب ('
.
(
$data
[
'absence_count'
]
??
0
)
.
' مرات) في مجموعة '
.
(
$data
[
'group_name'
]
??
''
)
.
'. يرجى المتابعة.'
);
if
(
$enrollment
)
{
\App\Modules\SportsActivity\Services\EnrollmentService
::
deactivateEnrollment
((
int
)
$enrollment
[
'id'
]);
}
}
catch
(
\Throwable
$e
)
{
Logger
::
error
(
'SA payment.voided listener failed: '
.
$e
->
getMessage
());
}
},
60
);
},
80
);
EventBus
::
listen
(
'sa.gate.access_denied'
,
function
(
array
$data
)
:
void
{
Logger
::
warning
(
'SA Gate denial: '
.
(
$data
[
'player_name'
]
??
'unknown'
)
.
' - '
.
(
$data
[
'reason'
]
??
''
),
$data
);
},
50
);
cron/jobs/SaAttendanceReportJob.php
0 → 100644
View file @
c674d6f6
<?php
declare
(
strict_types
=
1
);
namespace
CronJobs
;
use
App\Core\Database
;
use
App\Core\EventBus
;
use
App\Core\Logger
;
use
App\Core\App
;
class
SaAttendanceReportJob
{
private
Database
$db
;
public
function
__construct
(
Database
$db
)
{
$this
->
db
=
$db
;
}
public
function
shouldRun
()
:
bool
{
return
(
int
)
date
(
'j'
)
===
1
;
}
public
function
run
()
:
array
{
App
::
getInstance
()
->
setDb
(
$this
->
db
);
$lastMonth
=
date
(
'Y-m'
,
strtotime
(
'-1 month'
));
$from
=
$lastMonth
.
'-01'
;
$to
=
date
(
'Y-m-t'
,
strtotime
(
$from
));
$threshold
=
(
int
)
(
$this
->
db
->
selectOne
(
"SELECT value FROM system_config WHERE `key` = 'sa.absence_threshold'"
,
[]
)[
'value'
]
??
3
);
$players
=
$this
->
db
->
select
(
"SELECT gp.player_id, gp.group_id, p.full_name_ar, p.phone, p.guardian_phone,
g.name_ar as group_name,
(SELECT COUNT(*) FROM sa_attendance a
WHERE a.player_id = gp.player_id AND a.group_id = gp.group_id
AND a.session_date BETWEEN ? AND ? AND a.status = 'absent') as absence_count
FROM sa_group_players gp
INNER JOIN sa_players p ON p.id = gp.player_id
INNER JOIN sa_groups g ON g.id = gp.group_id
WHERE gp.status = 'active'
HAVING absence_count >= ?"
,
[
$from
,
$to
,
$threshold
]
);
$notified
=
0
;
foreach
(
$players
as
$player
)
{
$phone
=
$player
[
'guardian_phone'
]
?:
(
$player
[
'phone'
]
??
''
);
if
(
$phone
===
''
)
{
continue
;
}
EventBus
::
dispatch
(
'sa.player.absence_threshold'
,
[
'player_id'
=>
(
int
)
$player
[
'player_id'
],
'player_name'
=>
$player
[
'full_name_ar'
],
'group_name'
=>
$player
[
'group_name'
],
'absence_count'
=>
(
int
)
$player
[
'absence_count'
],
'month'
=>
$lastMonth
,
'phone'
=>
$phone
,
]);
$notified
++
;
}
Logger
::
info
(
"SaAttendanceReportJob for
{
$lastMonth
}
:
{
$notified
}
players exceeded threshold (
{
$threshold
}
)"
);
return
[
'month'
=>
$lastMonth
,
'threshold'
=>
$threshold
,
'notified'
=>
$notified
];
}
}
cron/jobs/SaCardExpiryJob.php
0 → 100644
View file @
c674d6f6
<?php
declare
(
strict_types
=
1
);
namespace
CronJobs
;
use
App\Core\Database
;
use
App\Core\EventBus
;
use
App\Core\Logger
;
class
SaCardExpiryJob
{
private
Database
$db
;
public
function
__construct
(
Database
$db
)
{
$this
->
db
=
$db
;
}
public
function
shouldRun
()
:
bool
{
return
true
;
}
public
function
run
()
:
array
{
$ts
=
date
(
'Y-m-d H:i:s'
);
$today
=
date
(
'Y-m-d'
);
$reminderDate
=
date
(
'Y-m-d'
,
strtotime
(
'+7 days'
));
$expired
=
$this
->
expireCards
(
$today
,
$ts
);
$reminded
=
$this
->
sendExpiryReminders
(
$today
,
$reminderDate
);
Logger
::
info
(
"SaCardExpiryJob:
{
$expired
}
expired,
{
$reminded
}
reminders sent"
);
return
[
'expired'
=>
$expired
,
'reminded'
=>
$reminded
];
}
private
function
expireCards
(
string
$today
,
string
$ts
)
:
int
{
$cards
=
$this
->
db
->
select
(
"SELECT id, player_id FROM sa_player_cards
WHERE status = 'active' AND valid_until IS NOT NULL AND valid_until < ?
AND is_archived = 0"
,
[
$today
]
);
foreach
(
$cards
as
$card
)
{
$this
->
db
->
update
(
'sa_player_cards'
,
[
'status'
=>
'expired'
,
'updated_at'
=>
$ts
,
],
'id = ?'
,
[(
int
)
$card
[
'id'
]]);
}
return
count
(
$cards
);
}
private
function
sendExpiryReminders
(
string
$today
,
string
$reminderDate
)
:
int
{
$cards
=
$this
->
db
->
select
(
"SELECT c.id, c.card_number, c.valid_until, c.player_id,
p.full_name_ar, p.phone, p.guardian_phone
FROM sa_player_cards c
INNER JOIN sa_players p ON p.id = c.player_id
WHERE c.status = 'active' AND c.valid_until BETWEEN ? AND ?
AND c.is_archived = 0"
,
[
$today
,
$reminderDate
]
);
$count
=
0
;
foreach
(
$cards
as
$card
)
{
$phone
=
$card
[
'guardian_phone'
]
?:
(
$card
[
'phone'
]
??
''
);
if
(
$phone
===
''
)
{
continue
;
}
EventBus
::
dispatch
(
'sa.card.expiry_reminder'
,
[
'player_id'
=>
(
int
)
$card
[
'player_id'
],
'player_name'
=>
$card
[
'full_name_ar'
],
'card_number'
=>
$card
[
'card_number'
],
'expiry_date'
=>
$card
[
'valid_until'
],
'phone'
=>
$phone
,
]);
$count
++
;
}
return
$count
;
}
}
cron/jobs/SaOverdueSubscriptionJob.php
0 → 100644
View file @
c674d6f6
<?php
declare
(
strict_types
=
1
);
namespace
CronJobs
;
use
App\Core\Database
;
use
App\Core\EventBus
;
use
App\Core\Logger
;
class
SaOverdueSubscriptionJob
{
private
Database
$db
;
public
function
__construct
(
Database
$db
)
{
$this
->
db
=
$db
;
}
public
function
shouldRun
()
:
bool
{
return
true
;
}
public
function
run
()
:
array
{
$ts
=
date
(
'Y-m-d H:i:s'
);
$today
=
date
(
'Y-m-d'
);
$marked
=
$this
->
markOverdue
(
$today
,
$ts
);
$notified
=
$this
->
notifyOverdue
();
Logger
::
info
(
"SaOverdueSubscriptionJob:
{
$marked
}
marked overdue,
{
$notified
}
notified"
);
return
[
'marked_overdue'
=>
$marked
,
'notified'
=>
$notified
];
}
private
function
markOverdue
(
string
$today
,
string
$ts
)
:
int
{
$subs
=
$this
->
db
->
select
(
"SELECT id FROM sa_subscriptions
WHERE payment_status = 'unpaid' AND period_end < ?"
,
[
$today
]
);
foreach
(
$subs
as
$sub
)
{
$this
->
db
->
update
(
'sa_subscriptions'
,
[
'payment_status'
=>
'overdue'
,
'updated_at'
=>
$ts
,
],
'id = ?'
,
[(
int
)
$sub
[
'id'
]]);
}
return
count
(
$subs
);
}
private
function
notifyOverdue
()
:
int
{
$subs
=
$this
->
db
->
select
(
"SELECT s.id, s.player_id, s.amount, s.period_start, s.period_end,
p.full_name_ar, p.phone, p.guardian_phone
FROM sa_subscriptions s
INNER JOIN sa_players p ON p.id = s.player_id
WHERE s.payment_status = 'overdue' AND s.overdue_notified_at IS NULL"
,
[]
);
$count
=
0
;
$ts
=
date
(
'Y-m-d H:i:s'
);
foreach
(
$subs
as
$sub
)
{
$phone
=
$sub
[
'guardian_phone'
]
?:
(
$sub
[
'phone'
]
??
''
);
if
(
$phone
===
''
)
{
$this
->
db
->
update
(
'sa_subscriptions'
,
[
'overdue_notified_at'
=>
$ts
],
'id = ?'
,
[(
int
)
$sub
[
'id'
]]);
continue
;
}
EventBus
::
dispatch
(
'sa.subscription.overdue'
,
[
'player_id'
=>
(
int
)
$sub
[
'player_id'
],
'player_name'
=>
$sub
[
'full_name_ar'
],
'amount'
=>
$sub
[
'amount'
],
'period'
=>
$sub
[
'period_start'
]
.
' - '
.
$sub
[
'period_end'
],
'phone'
=>
$phone
,
]);
$this
->
db
->
update
(
'sa_subscriptions'
,
[
'overdue_notified_at'
=>
$ts
],
'id = ?'
,
[(
int
)
$sub
[
'id'
]]);
$count
++
;
}
return
$count
;
}
}
database/migrations/Phase_76_001_sa_module_enhancements.php
0 → 100644
View file @
c674d6f6
<?php
declare
(
strict_types
=
1
);
return
[
'up'
=>
"ALTER TABLE `sa_groups`
ADD COLUMN `min_age` TINYINT UNSIGNED NULL AFTER `monthly_fee_nonmember`,
ADD COLUMN `max_age` TINYINT UNSIGNED NULL AFTER `min_age`;
ALTER TABLE `sa_subscriptions`
ADD COLUMN `overdue_notified_at` TIMESTAMP NULL AFTER `receipt_number`;
ALTER TABLE `sa_group_players`
ADD COLUMN `transferred_to_group_id` BIGINT UNSIGNED NULL AFTER `notes`,
ADD COLUMN `transfer_reason` VARCHAR(500) NULL AFTER `transferred_to_group_id`"
,
'down'
=>
"ALTER TABLE `sa_groups`
DROP COLUMN `max_age`,
DROP COLUMN `min_age`;
ALTER TABLE `sa_subscriptions`
DROP COLUMN `overdue_notified_at`;
ALTER TABLE `sa_group_players`
DROP COLUMN `transfer_reason`,
DROP COLUMN `transferred_to_group_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