Commit 8b3af3e5 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Deploy missing local changes: PlatformFeeService, PermissionService, Login, POS

These files were modified locally but never committed, causing production
errors (customerPays() method missing, LoginRedirectService not found).
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 38fce503
<?php
namespace App\Domain\Identity\Services;
use App\Models\User;
class LoginRedirectService
{
public function getRedirectRoute(User $user): string
{
$role = $user->primaryRole;
return match ($role?->slug) {
'parent' => 'guardian.dashboard',
'trainer', 'head_trainer' => 'trainer.dashboard',
'receptionist' => 'receptionist.dashboard',
'accountant' => 'financial.overview',
'data_entry' => 'participants.list',
default => 'dashboard',
};
}
}
...@@ -2,25 +2,31 @@ ...@@ -2,25 +2,31 @@
namespace App\Domain\Identity\Services; namespace App\Domain\Identity\Services;
use App\Domain\Identity\Models\Permission; use App\Domain\Attendance\Models\AttendanceRecord;
use App\Domain\Identity\Models\Role; use App\Domain\Financial\Models\Invoice;
use App\Domain\Identity\Models\Guardian;
use App\Domain\Participant\Models\Participant;
use App\Domain\Scheduling\Models\Assignment;
use App\Domain\Training\Models\Enrollment;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Models\TrainingSession;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class PermissionService class PermissionService
{ {
private array $cachedGroupIds = [];
private array $cachedChildIds = [];
public function can(User $user, string $permission, ?Model $record = null): bool public function can(User $user, string $permission, ?Model $record = null): bool
{ {
// Super admin bypasses all
if ($user->is_super_admin) { if ($user->is_super_admin) {
return true; return true;
} }
// Get user's role (primary role via role_id for performance)
$role = $user->primaryRole; $role = $user->primaryRole;
if (!$role) { if (!$role) {
// Fallback: check many-to-many roles
$role = $user->roles->first(); $role = $user->roles->first();
if (!$role) { if (!$role) {
return false; return false;
...@@ -30,23 +36,20 @@ public function can(User $user, string $permission, ?Model $record = null): bool ...@@ -30,23 +36,20 @@ public function can(User $user, string $permission, ?Model $record = null): bool
} }
} }
// Check if role has this permission
$rolePermission = $role->permissions->firstWhere('name', $permission); $rolePermission = $role->permissions->firstWhere('name', $permission);
if (!$rolePermission) { if (!$rolePermission) {
return false; return false;
} }
// If no specific record to check, permission exists = allowed
if ($record === null) { if ($record === null) {
return true; return true;
} }
// Scope check against the specific record
$scope = $rolePermission->pivot->scope; $scope = $rolePermission->pivot->scope;
return match ($scope) { return match ($scope) {
'all' => true, 'all' => true,
'academy' => true, // tenant scope already applied 'academy' => true,
'branch' => $this->checkBranchScope($user, $record), 'branch' => $this->checkBranchScope($user, $record),
'own' => $this->checkOwnScope($user, $record), 'own' => $this->checkOwnScope($user, $record),
'own_groups' => $this->checkOwnGroupsScope($user, $record), 'own_groups' => $this->checkOwnGroupsScope($user, $record),
...@@ -84,35 +87,185 @@ public function applyScope(Builder $query, User $user, string $permission): Buil ...@@ -84,35 +87,185 @@ public function applyScope(Builder $query, User $user, string $permission): Buil
'all', 'academy' => $query, 'all', 'academy' => $query,
'branch' => $query->where("{$table}.branch_id", $user->branch_id), 'branch' => $query->where("{$table}.branch_id", $user->branch_id),
'own' => $query->where("{$table}.created_by", $user->id), 'own' => $query->where("{$table}.created_by", $user->id),
'own_groups' => $this->applyOwnGroupsScope($query, $user, $table),
'own_children' => $this->applyOwnChildrenScope($query, $user, $table),
default => $query, default => $query,
}; };
} }
// --- Branch scope ---
private function checkBranchScope(User $user, Model $record): bool private function checkBranchScope(User $user, Model $record): bool
{ {
if (!property_exists($record, 'branch_id') && !isset($record->branch_id)) { if (!isset($record->branch_id)) {
return true; return true;
} }
return $record->branch_id === $user->branch_id; return $record->branch_id === $user->branch_id;
} }
// --- Own scope ---
private function checkOwnScope(User $user, Model $record): bool private function checkOwnScope(User $user, Model $record): bool
{ {
if (!property_exists($record, 'created_by') && !isset($record->created_by)) { if (!isset($record->created_by)) {
return true; return true;
} }
return $record->created_by === $user->id; return $record->created_by === $user->id;
} }
// --- Own Groups scope (trainers) ---
private function getAssignedGroupIds(User $user): array
{
if (isset($this->cachedGroupIds[$user->id])) {
return $this->cachedGroupIds[$user->id];
}
$ids = Assignment::active()
->forUser($user->id)
->where('assignable_type', TrainingGroup::class)
->pluck('assignable_id')
->toArray();
$this->cachedGroupIds[$user->id] = $ids;
return $ids;
}
private function checkOwnGroupsScope(User $user, Model $record): bool private function checkOwnGroupsScope(User $user, Model $record): bool
{ {
// Will be implemented when assignments exist (Phase 5) $groupIds = $this->getAssignedGroupIds($user);
if (empty($groupIds)) {
return false; return false;
} }
if ($record instanceof TrainingGroup) {
return in_array($record->id, $groupIds);
}
if ($record instanceof TrainingSession) {
return in_array($record->training_group_id, $groupIds);
}
if (isset($record->training_group_id)) {
return in_array($record->training_group_id, $groupIds);
}
if ($record instanceof Participant) {
return Enrollment::where('participant_id', $record->id)
->whereIn('training_group_id', $groupIds)
->whereIn('status', ['active', 'pending'])
->exists();
}
if ($record instanceof AttendanceRecord) {
$session = $record->session;
return $session && in_array($session->training_group_id, $groupIds);
}
return false;
}
private function applyOwnGroupsScope(Builder $query, User $user, string $table): Builder
{
$groupIds = $this->getAssignedGroupIds($user);
if (empty($groupIds)) {
return $query->whereRaw('1 = 0');
}
return match ($table) {
'training_groups' => $query->whereIn("{$table}.id", $groupIds),
'training_sessions' => $query->whereIn("{$table}.training_group_id", $groupIds),
'enrollments' => $query->whereIn("{$table}.training_group_id", $groupIds),
'evaluations' => $query->whereIn("{$table}.training_group_id", $groupIds),
'participants' => $query->whereIn("{$table}.id", function ($sub) use ($groupIds) {
$sub->select('participant_id')
->from('enrollments')
->whereIn('training_group_id', $groupIds)
->whereIn('status', ['active', 'pending']);
}),
'attendance_records' => $query->whereIn("{$table}.training_session_id", function ($sub) use ($groupIds) {
$sub->select('id')
->from('training_sessions')
->whereIn('training_group_id', $groupIds);
}),
default => $query->whereIn("{$table}.training_group_id", $groupIds),
};
}
// --- Own Children scope (parents/guardians) ---
private function getChildParticipantIds(User $user): array
{
if (isset($this->cachedChildIds[$user->id])) {
return $this->cachedChildIds[$user->id];
}
$guardian = Guardian::where('person_id', $user->person_id)
->orWhere('user_id', $user->id)
->first();
if (!$guardian) {
$this->cachedChildIds[$user->id] = [];
return [];
}
$ids = $guardian->participants()->pluck('participants.id')->toArray();
$this->cachedChildIds[$user->id] = $ids;
return $ids;
}
private function checkOwnChildrenScope(User $user, Model $record): bool private function checkOwnChildrenScope(User $user, Model $record): bool
{ {
// Will be implemented when participants + guardian_participant exist (Phase 2) $childIds = $this->getChildParticipantIds($user);
if (empty($childIds)) {
return false;
}
if ($record instanceof Participant) {
return in_array($record->id, $childIds);
}
if (isset($record->participant_id)) {
return in_array($record->participant_id, $childIds);
}
if ($record instanceof Invoice && $record->billable_type === Participant::class) {
return in_array($record->billable_id, $childIds);
}
if ($record instanceof AttendanceRecord && $record->subject_type === Participant::class) {
return in_array($record->subject_id, $childIds);
}
return false; return false;
} }
private function applyOwnChildrenScope(Builder $query, User $user, string $table): Builder
{
$childIds = $this->getChildParticipantIds($user);
if (empty($childIds)) {
return $query->whereRaw('1 = 0');
}
return match ($table) {
'participants' => $query->whereIn("{$table}.id", $childIds),
'enrollments' => $query->whereIn("{$table}.participant_id", $childIds),
'evaluations' => $query->whereIn("{$table}.participant_id", $childIds),
'invoices' => $query->where("{$table}.billable_type", Participant::class)
->whereIn("{$table}.billable_id", $childIds),
'payments' => $query->whereIn("{$table}.invoice_id", function ($sub) use ($childIds) {
$sub->select('id')
->from('invoices')
->where('billable_type', Participant::class)
->whereIn('billable_id', $childIds);
}),
'attendance_records' => $query->where("{$table}.subject_type", Participant::class)
->whereIn("{$table}.subject_id", $childIds),
default => $query->whereIn("{$table}.participant_id", $childIds),
};
}
} }
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
class PlatformFeeService class PlatformFeeService
{ {
/**
* Get the platform service fee percentage (from env only — not editable by any user).
*/
public function getPercentage(): float public function getPercentage(): float
{ {
return (float) env('PLATFORM_SERVICE_FEE_PERCENT', 3); return (float) env('PLATFORM_SERVICE_FEE_PERCENT', 3);
...@@ -26,11 +23,17 @@ public function calculate(int $amountPiasters): int ...@@ -26,11 +23,17 @@ public function calculate(int $amountPiasters): int
return (int) round($amountPiasters * $percent / 100); return (int) round($amountPiasters * $percent / 100);
} }
/**
* Whether the platform fee is active.
*/
public function isActive(): bool public function isActive(): bool
{ {
return $this->getPercentage() > 0; return $this->getPercentage() > 0;
} }
/**
* Whether the customer pays the fee (added to their total) or the academy absorbs it.
* true = customer pays (default), false = academy absorbs (deducted from academy revenue).
*/
public function customerPays(): bool
{
return (bool) app(SettingsService::class)->get('platform_fee_customer_pays', true);
}
} }
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace App\Livewire\Auth; namespace App\Livewire\Auth;
use App\Domain\Identity\Services\AuthService; use App\Domain\Identity\Services\AuthService;
use App\Domain\Identity\Services\LoginRedirectService;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
...@@ -61,7 +62,8 @@ public function login(AuthService $authService): void ...@@ -61,7 +62,8 @@ public function login(AuthService $authService): void
Auth::login($result->user, $this->remember); Auth::login($result->user, $this->remember);
session()->regenerate(); session()->regenerate();
$this->redirectIntended(route('dashboard')); $defaultRoute = app(LoginRedirectService::class)->getRedirectRoute($result->user);
$this->redirectIntended(route($defaultRoute));
} }
public function render() public function render()
......
...@@ -179,7 +179,11 @@ public function getCartSubtotal(): int ...@@ -179,7 +179,11 @@ public function getCartSubtotal(): int
public function getServiceFee(): int public function getServiceFee(): int
{ {
return app(\App\Domain\Shared\Services\PlatformFeeService::class)->calculate($this->getCartTotal()); $service = app(\App\Domain\Shared\Services\PlatformFeeService::class);
if (!$service->customerPays()) {
return 0;
}
return $service->calculate($this->getCartTotal());
} }
public function getCartGrandTotal(): int public function getCartGrandTotal(): int
......
...@@ -79,6 +79,15 @@ public function save(): void ...@@ -79,6 +79,15 @@ public function save(): void
{ {
$this->validate(); $this->validate();
if ($this->role_id) {
$targetRole = Role::find($this->role_id);
$currentUserLevel = auth()->user()->primaryRole?->level ?? 0;
if ($targetRole && $targetRole->level >= $currentUserLevel && !auth()->user()->is_super_admin) {
$this->addError('role_id', 'لا يمكنك تعيين دور بمستوى أعلى من أو يساوي مستواك');
return;
}
}
$data = [ $data = [
'name' => $this->name, 'name' => $this->name,
'name_ar' => $this->name_ar, 'name_ar' => $this->name_ar,
...@@ -106,8 +115,15 @@ public function save(): void ...@@ -106,8 +115,15 @@ public function save(): void
public function render() public function render()
{ {
$rolesQuery = Role::orderBy('level', 'desc');
if (!auth()->user()->is_super_admin) {
$currentUserLevel = auth()->user()->primaryRole?->level ?? 0;
$rolesQuery->where('level', '<', $currentUserLevel);
}
return view('livewire.users.user-form', [ return view('livewire.users.user-form', [
'roles' => Role::orderBy('level', 'desc')->get(['id', 'name_ar', 'slug']), 'roles' => $rolesQuery->get(['id', 'name_ar', 'slug', 'level']),
'people' => Person::orderBy('name_ar') 'people' => Person::orderBy('name_ar')
->get(['id', 'name_ar']) ->get(['id', 'name_ar'])
->map(fn ($p) => ['id' => $p->id, 'name' => $p->name_ar]), ->map(fn ($p) => ['id' => $p->id, 'name' => $p->name_ar]),
......
...@@ -39,8 +39,8 @@ public function run(): void ...@@ -39,8 +39,8 @@ public function run(): void
); );
$this->call(FinancialAccountsSeeder::class); $this->call(FinancialAccountsSeeder::class);
$this->call(RolesAndPermissionsSeeder::class); $this->call(RolesAndPermissionsSeeder::class); // Creates roles (needed before PermissionSeeder)
$this->call(PermissionSeeder::class); $this->call(PermissionSeeder::class); // Authoritative permission+scope assignments
$this->call(PaymentNotificationTemplateSeeder::class); $this->call(PaymentNotificationTemplateSeeder::class);
// Assign academy_owner role to the admin user // Assign academy_owner role to the admin user
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment