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 @@
namespace App\Domain\Identity\Services;
use App\Domain\Identity\Models\Permission;
use App\Domain\Identity\Models\Role;
use App\Domain\Attendance\Models\AttendanceRecord;
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 Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class PermissionService
{
private array $cachedGroupIds = [];
private array $cachedChildIds = [];
public function can(User $user, string $permission, ?Model $record = null): bool
{
// Super admin bypasses all
if ($user->is_super_admin) {
return true;
}
// Get user's role (primary role via role_id for performance)
$role = $user->primaryRole;
if (!$role) {
// Fallback: check many-to-many roles
$role = $user->roles->first();
if (!$role) {
return false;
......@@ -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);
if (!$rolePermission) {
return false;
}
// If no specific record to check, permission exists = allowed
if ($record === null) {
return true;
}
// Scope check against the specific record
$scope = $rolePermission->pivot->scope;
return match ($scope) {
'all' => true,
'academy' => true, // tenant scope already applied
'academy' => true,
'branch' => $this->checkBranchScope($user, $record),
'own' => $this->checkOwnScope($user, $record),
'own_groups' => $this->checkOwnGroupsScope($user, $record),
......@@ -84,35 +87,185 @@ public function applyScope(Builder $query, User $user, string $permission): Buil
'all', 'academy' => $query,
'branch' => $query->where("{$table}.branch_id", $user->branch_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,
};
}
// --- Branch scope ---
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 $record->branch_id === $user->branch_id;
}
// --- Own scope ---
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 $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
{
// Will be implemented when assignments exist (Phase 5)
$groupIds = $this->getAssignedGroupIds($user);
if (empty($groupIds)) {
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
{
// 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;
}
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 @@
class PlatformFeeService
{
/**
* Get the platform service fee percentage (from env only — not editable by any user).
*/
public function getPercentage(): float
{
return (float) env('PLATFORM_SERVICE_FEE_PERCENT', 3);
......@@ -26,11 +23,17 @@ public function calculate(int $amountPiasters): int
return (int) round($amountPiasters * $percent / 100);
}
/**
* Whether the platform fee is active.
*/
public function isActive(): bool
{
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 @@
namespace App\Livewire\Auth;
use App\Domain\Identity\Services\AuthService;
use App\Domain\Identity\Services\LoginRedirectService;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
......@@ -61,7 +62,8 @@ public function login(AuthService $authService): void
Auth::login($result->user, $this->remember);
session()->regenerate();
$this->redirectIntended(route('dashboard'));
$defaultRoute = app(LoginRedirectService::class)->getRedirectRoute($result->user);
$this->redirectIntended(route($defaultRoute));
}
public function render()
......
......@@ -179,7 +179,11 @@ public function getCartSubtotal(): 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
......
......@@ -79,6 +79,15 @@ public function save(): void
{
$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 = [
'name' => $this->name,
'name_ar' => $this->name_ar,
......@@ -106,8 +115,15 @@ public function save(): void
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', [
'roles' => Role::orderBy('level', 'desc')->get(['id', 'name_ar', 'slug']),
'roles' => $rolesQuery->get(['id', 'name_ar', 'slug', 'level']),
'people' => Person::orderBy('name_ar')
->get(['id', 'name_ar'])
->map(fn ($p) => ['id' => $p->id, 'name' => $p->name_ar]),
......
......@@ -39,8 +39,8 @@ public function run(): void
);
$this->call(FinancialAccountsSeeder::class);
$this->call(RolesAndPermissionsSeeder::class);
$this->call(PermissionSeeder::class);
$this->call(RolesAndPermissionsSeeder::class); // Creates roles (needed before PermissionSeeder)
$this->call(PermissionSeeder::class); // Authoritative permission+scope assignments
$this->call(PaymentNotificationTemplateSeeder::class);
// 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