roleApplications()->with(['tenant:id,tenant_code,tenant_name', 'reviewer:id,name'])->latest('id')->get(); } public function submit(array $data, User $user): RoleApplication { $type = $data['application_type']; if (RoleApplication::where('applicant_user_id', $user->id)->where('application_type', $type)->where('status', 'pending')->exists()) { throw ValidationException::withMessages(['application_type' => 'Masih ada pengajuan sejenis yang menunggu persetujuan.']); } if ($type === 'tenant' && $user->userTenants()->where('role', 'tenant_owner')->exists()) { throw ValidationException::withMessages(['application_type' => 'Satu user hanya boleh menjadi Tenant Owner pada satu tenant.']); } if ($type === 'agent' && VoucherAgent::where('user_id', $user->id)->where('tenant_id', $data['tenant_id'])->where('status', 'active')->exists()) { throw ValidationException::withMessages(['tenant_id' => 'User sudah menjadi agen aktif pada tenant tersebut.']); } if ($type === 'staff' && $user->userTenants()->where('tenant_id', $data['tenant_id'])->where('role', 'tenant_owner')->exists()) { throw ValidationException::withMessages(['tenant_id' => 'Tenant Owner tidak dapat mengajukan diri sebagai staff pada tenant miliknya.']); } return RoleApplication::create([ 'uuid' => Str::uuid(), 'applicant_user_id' => $user->id, 'tenant_id' => in_array($type, ['agent', 'staff'], true) ? $data['tenant_id'] : null, 'application_type' => $type, 'status' => 'pending', 'payload' => collect($data)->except(['application_type', 'tenant_id'])->all(), 'submitted_at' => now(), ])->load('tenant:id,tenant_code,tenant_name'); } public function reviewList(string $type, array $filters, User $reviewer, ?int $tenantId) { $this->authorizeReviewer($type, $reviewer, $tenantId); return RoleApplication::with(['applicant:id,name,username,email,whatsapp_number', 'tenant:id,tenant_code,tenant_name', 'reviewer:id,name']) ->where('application_type', $type) ->when(in_array($type, ['agent', 'staff'], true) && ! $reviewer->isMasterAdmin(), fn ($q) => $q->where('tenant_id', $tenantId)) ->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status)) ->when($filters['search'] ?? null, fn ($q, $search) => $q->whereHas('applicant', fn ($x) => $x->where('name', 'ilike', "%{$search}%")->orWhere('email', 'ilike', "%{$search}%"))) ->latest('id')->paginate($filters['per_page'] ?? 10); } public function tenantMenuGroupOptions(User $reviewer): array { $this->authorizeReviewer('tenant', $reviewer, null); return MenuGroup::query() ->whereNull('tenant_id') ->where('is_system', true) ->where('is_active', true) ->withCount('menus') ->orderBy('name') ->get(['id', 'name', 'description']) ->map(fn (MenuGroup $group) => [ 'id' => $group->id, 'name' => $group->name, 'description' => $group->description, 'menus_count' => $group->menus_count, ])->all(); } public function staffMenuGroupOptions(User $reviewer, ?int $tenantId): array { $this->authorizeReviewer('staff', $reviewer, $tenantId); $assignedGroupIds = UserMenuGroup::query() ->where('user_id', $reviewer->id) ->where('tenant_id', $tenantId) ->pluck('menu_group_id'); return MenuGroup::query() ->where('is_active', true) ->where(function ($query) use ($tenantId, $assignedGroupIds) { $query->where('tenant_id', $tenantId) ->orWhereIn('id', $assignedGroupIds); }) ->withCount('menus') ->orderBy('name') ->get(['id', 'name', 'description', 'tenant_id']) ->map(fn (MenuGroup $group) => [ 'id' => $group->id, 'name' => $group->name, 'description' => $group->description, 'menus_count' => $group->menus_count, 'is_tenant_group' => (int) $group->tenant_id === (int) $tenantId, ])->all(); } public function review(RoleApplication $application, array $data, User $reviewer, ?int $tenantId): RoleApplication { $this->authorizeReviewer($application->application_type, $reviewer, $tenantId, $application->tenant_id); if ($application->status !== 'pending') { throw ValidationException::withMessages(['application' => 'Pengajuan ini sudah ditinjau.']); } return DB::transaction(function () use ($application, $data, $reviewer) { if ($data['decision'] === 'approved') { if ($application->application_type === 'agent' && empty($data['payment_type'])) { throw ValidationException::withMessages(['payment_type' => 'Kategori pembayaran agen wajib dipilih.']); } match ($application->application_type) { 'tenant' => $this->approveTenant($application, (int) $data['menu_group_id']), 'staff' => $this->approveStaff($application, (int) $data['menu_group_id'], $reviewer), default => $this->approveAgent($application, $data), }; } $application->update([ 'status' => $data['decision'], 'reviewed_by' => $reviewer->id, 'review_notes' => $data['review_notes'] ?? null, 'reviewed_at' => now(), ]); return $application->fresh(['applicant:id,name,username,email', 'tenant:id,tenant_code,tenant_name', 'reviewer:id,name']); }); } public function linkCustomer(array $data, User $reviewer, int $tenantId): CustomerUserLink { abort_unless($reviewer->isMasterAdmin() || in_array($reviewer->tenantRole($tenantId), ['tenant_owner', 'staff'], true), 403); $customer = Customer::whereKey($data['customer_id'])->where('tenant_id', $tenantId)->firstOrFail(); $link = CustomerUserLink::updateOrCreate(['customer_id' => $customer->id, 'user_id' => $data['user_id']], [ 'tenant_id' => $tenantId, 'linked_by' => $reviewer->id, 'relationship' => $data['relationship'] ?? 'owner', 'is_primary' => $data['is_primary'] ?? true, ]); if (! $customer->user_id && ($data['is_primary'] ?? true)) { $customer->update(['user_id' => $data['user_id']]); } return $link->load(['customer:id,tenant_id,customer_code,name', 'user:id,name,username,email']); } private function approveTenant(RoleApplication $application, int $templateGroupId): void { $payload = $application->payload; $group = MenuGroup::query() ->whereNull('tenant_id') ->where('is_system', true) ->where('is_active', true) ->findOrFail($templateGroupId); $tenant = Tenant::create([ 'tenant_code' => $this->uniqueTenantCode($payload['tenant_code'] ?? null), 'tenant_name' => $payload['tenant_name'], 'phone' => $payload['phone'] ?? null, 'email' => $payload['email'] ?? null, 'address' => $payload['address'] ?? null, 'status' => 'active', ]); $application->tenant_id = $tenant->id; if (UserTenant::where('user_id', $application->applicant_user_id)->where('role', 'tenant_owner')->exists()) { throw ValidationException::withMessages([ 'application' => 'User sudah menjadi Tenant Owner pada tenant lain.', ]); } UserTenant::where('user_id', $application->applicant_user_id)->update(['is_default' => false]); UserTenant::updateOrCreate( ['user_id' => $application->applicant_user_id, 'tenant_id' => $tenant->id], ['role' => 'tenant_owner', 'is_default' => true] ); $application->applicant()->update(['access_level' => 'tenant_owner']); UserMenuGroup::create([ 'user_id' => $application->applicant_user_id, 'tenant_id' => $tenant->id, 'menu_group_id' => $group->id, ]); $application->payload = array_merge($payload, [ 'approved_menu_group_id' => $group->id, 'approved_menu_group_name' => $group->name, ]); } private function approveStaff(RoleApplication $application, int $menuGroupId, User $reviewer): void { $availableGroupIds = collect($this->staffMenuGroupOptions($reviewer, $application->tenant_id))->pluck('id'); if (! $availableGroupIds->contains($menuGroupId)) { throw ValidationException::withMessages([ 'menu_group_id' => 'Group Menu tidak tersedia pada tenant aktif.', ]); } $existingTenantRole = UserTenant::query() ->where('user_id', $application->applicant_user_id) ->where('tenant_id', $application->tenant_id) ->value('role'); if ($existingTenantRole === 'tenant_owner') { throw ValidationException::withMessages([ 'application' => 'Tenant Owner tidak dapat diubah menjadi staff pada tenant miliknya.', ]); } UserTenant::updateOrCreate( ['user_id' => $application->applicant_user_id, 'tenant_id' => $application->tenant_id], ['role' => 'staff', 'is_default' => ! UserTenant::where('user_id', $application->applicant_user_id)->exists()] ); if (! $application->applicant->userTenants()->where('role', 'tenant_owner')->exists()) { $application->applicant()->update(['access_level' => 'staff']); } UserMenuGroup::where('user_id', $application->applicant_user_id) ->where('tenant_id', $application->tenant_id) ->delete(); UserMenuGroup::create([ 'user_id' => $application->applicant_user_id, 'tenant_id' => $application->tenant_id, 'menu_group_id' => $menuGroupId, ]); $group = MenuGroup::findOrFail($menuGroupId); $application->payload = array_merge($application->payload, [ 'approved_menu_group_id' => $group->id, 'approved_menu_group_name' => $group->name, ]); } private function approveAgent(RoleApplication $application, array $data): void { UserTenant::updateOrCreate(['user_id' => $application->applicant_user_id, 'tenant_id' => $application->tenant_id], [ 'is_default' => ! UserTenant::where('user_id', $application->applicant_user_id)->exists(), ]); VoucherAgent::updateOrCreate(['tenant_id' => $application->tenant_id, 'user_id' => $application->applicant_user_id], [ 'uuid' => Str::uuid(), 'agent_code' => 'AGT-'.$application->tenant_id.'-'.Str::upper(Str::random(6)), 'payment_type' => $data['payment_type'], 'price_mode' => $data['price_mode'] ?? 'discount', 'discount_percent' => $data['discount_percent'] ?? 0, 'commission_percent' => $data['commission_percent'] ?? 0, 'credit_limit' => $data['payment_type'] === 'postpaid' ? ($data['credit_limit'] ?? 0) : 0, 'status' => 'active', 'notes' => $data['review_notes'] ?? null, ]); $this->assignDefaultGroup($application->applicant, Tenant::findOrFail($application->tenant_id), 'agent'); } private function assignDefaultGroup(User $user, Tenant $tenant, string $role): void { $name = $role === 'tenant_owner' ? 'Tenant Owner Default' : 'Agen Voucher Default'; $group = MenuGroup::firstOrCreate(['tenant_id' => $tenant->id, 'name' => $name], [ 'description' => 'Dibuat otomatis dari approval pengajuan.', 'is_system' => true, 'is_active' => true, ]); $slugs = $role === 'tenant_owner' ? MenuList::where('is_active', true)->whereNotIn('slug', ['tenants', 'role-applications-tenants'])->pluck('id') : MenuList::whereIn('slug', ['voucher-hotspot', 'voucher-hotspot-sales', 'wallet'])->pluck('id'); $group->menus()->syncWithoutDetaching($slugs->mapWithKeys(fn ($id) => [$id => [ 'can_view' => true, 'can_create' => true, 'can_update' => $role === 'tenant_owner', 'can_delete' => $role === 'tenant_owner', 'can_approve' => $role === 'tenant_owner', 'can_export' => true, ]])->all()); UserMenuGroup::firstOrCreate(['user_id' => $user->id, 'tenant_id' => $tenant->id, 'menu_group_id' => $group->id]); } private function authorizeReviewer(string $type, User $reviewer, ?int $tenantId, ?int $applicationTenantId = null): void { if ($type === 'tenant') { abort_unless($reviewer->isMasterAdmin() || $reviewer->is_platform_staff, 403); } if ($type === 'agent' && ! $reviewer->isMasterAdmin()) { abort_unless($tenantId && (! $applicationTenantId || $tenantId === $applicationTenantId) && in_array($reviewer->tenantRole($tenantId), ['tenant_owner', 'staff'], true), 403); } if ($type === 'staff') { abort_unless($tenantId && (! $applicationTenantId || $tenantId === $applicationTenantId) && $reviewer->isTenantOwner($tenantId), 403); } } private function uniqueTenantCode(?string $requested): string { $base = Str::upper(Str::slug($requested ?: 'TNT-'.Str::random(6), '')); $code = $base; $counter = 1; while (Tenant::where('tenant_code', $code)->exists()) { $code = $base.'-'.$counter++; } return $code; } }