update api register

This commit is contained in:
Wian Drs
2026-08-01 11:42:42 +07:00
parent b0d08211ec
commit 75342dc7b7
55 changed files with 2254 additions and 35 deletions
@@ -0,0 +1,288 @@
<?php
namespace App\Services\Access;
use App\Models\Customer;
use App\Models\CustomerUserLink;
use App\Models\MenuGroup;
use App\Models\MenuList;
use App\Models\RoleApplication;
use App\Models\Tenant;
use App\Models\User;
use App\Models\UserMenuGroup;
use App\Models\UserTenant;
use App\Models\VoucherAgent;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class RoleApplicationService
{
public function mine(User $user)
{
return $user->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;
}
}