refactor: remove owner verification index functionality and related components, streamline approval and rejection processes

This commit is contained in:
Yoga Pangestu 2026-06-27 21:51:48 +07:00
parent 2b51f42a7a
commit 1f27acc63a
22 changed files with 26 additions and 858 deletions

View File

@ -2,10 +2,7 @@
namespace App\Http\Controllers\Admin\Manage;
use App\Enums\OwnerVerificationAction;
use App\Enums\OwnerVerificationStatus;
use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Manage\ApproveVerificationRequest;
use App\Http\Requests\Admin\Manage\RejectVerificationRequest;
@ -14,45 +11,16 @@
use App\Services\Manage\OwnerVerificationService;
use App\Services\Manage\StockService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class OwnerVerificationController extends Controller
{
use FlashesEntityMessage, ParsesDataTableQuery;
use FlashesEntityMessage;
public function __construct(
private readonly OwnerVerificationService $ownerVerificationService,
private readonly StockService $stockService,
) {}
public function index(Request $request): Response
{
$tableQuery = $this->parseDataTableQuery($request);
$status = $request->string('status')->toString();
$subjectType = $request->string('subject_type')->toString();
$action = $request->string('action')->toString();
return Inertia::render('admin/manage/owner-verifications/Index', [
'verificationRequests' => $this->ownerVerificationService->paginateForIndex(
$request->user(),
$tableQuery,
$status,
$subjectType,
$action,
),
'filters' => $this->dataTableFilters($tableQuery, [
'status' => $status,
'subject_type' => $subjectType,
'action' => $action,
]),
'statusOptions' => OwnerVerificationStatus::selectOptions(),
'subjectOptions' => $this->ownerVerificationService->subjectTypeOptions($request->user()),
'actionOptions' => OwnerVerificationAction::selectOptions(),
]);
}
public function approveCutting(ApproveVerificationRequest $request, Cutting $cutting): RedirectResponse
{
$this->stockService->approveVerification(
@ -63,9 +31,7 @@ public function approveCutting(ApproveVerificationRequest $request, Cutting $cut
$this->flashSuccess('Verifikasi cutting berhasil disetujui. Stok produk telah ditambahkan ke toko.');
$fallback = route('admin.manage.owner_verifications.index');
return back(302, [], $fallback);
return back(302);
}
public function rejectCutting(RejectVerificationRequest $request, Cutting $cutting): RedirectResponse
@ -78,9 +44,7 @@ public function rejectCutting(RejectVerificationRequest $request, Cutting $cutti
$this->flashSuccess('Verifikasi cutting berhasil ditolak.');
$fallback = route('admin.manage.owner_verifications.index');
return back(302, [], $fallback);
return back(302);
}
public function approveRequest(
@ -95,9 +59,7 @@ public function approveRequest(
$this->flashSuccess('Verifikasi berhasil disetujui.');
$fallback = route('admin.manage.owner_verifications.index');
return back(302, [], $fallback);
return back(302);
}
public function rejectRequest(
@ -112,8 +74,6 @@ public function rejectRequest(
$this->flashSuccess('Verifikasi berhasil ditolak.');
$fallback = route('admin.manage.owner_verifications.index');
return back(302, [], $fallback);
return back(302);
}
}

View File

@ -5,7 +5,6 @@
use App\Models\Cutting;
use App\Models\EmployeeAdvance;
use App\Models\LeaveRequest;
use App\Services\Manage\OwnerVerificationService;
use App\Services\System\Setting\SystemService;
use App\Settings\SystemSettings;
use Illuminate\Http\Request;
@ -72,7 +71,6 @@ public function share(Request $request): array
'pendingLeaveRequests' => fn () => $this->pendingLeaveRequests($request),
'pendingEmployeeAdvances' => fn () => $this->pendingEmployeeAdvances($request),
'pendingCuttings' => fn () => $this->pendingCuttings($request),
'pendingOwnerVerifications' => fn () => $this->pendingOwnerVerifications($request),
];
}
@ -114,15 +112,4 @@ private function pendingCuttings(Request $request): int
->completed()
->count();
}
private function pendingOwnerVerifications(Request $request): int
{
$user = $request->user();
if ($user === null || ! $user->can('owner_verifications.view')) {
return 0;
}
return app(OwnerVerificationService::class)->pendingCountForUser($user);
}
}

View File

@ -2,19 +2,15 @@
namespace App\Http\Requests\Admin\Manage;
use App\Enums\Permission;
use Illuminate\Foundation\Http\FormRequest;
class ApproveVerificationRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can(Permission::OWNER_VERIFICATIONS_VERIFY->value) ?? false;
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [

View File

@ -2,19 +2,15 @@
namespace App\Http\Requests\Admin\Manage;
use App\Enums\Permission;
use Illuminate\Foundation\Http\FormRequest;
class RejectVerificationRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can(Permission::OWNER_VERIFICATIONS_REJECT->value) ?? false;
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [

View File

@ -265,7 +265,7 @@ public function rejectRequest(
'❌ Pengajuan Ditolak',
"Pengajuan {$request->action->label()} {$subjectLabel} '{$title}' ditolak dengan alasan: '{$reason}'.",
['owner', 'developer'],
route('admin.manage.owner_verifications.index'),
route('admin.dashboard'),
);
$this->notifyRequestSubmitter(
@ -527,7 +527,7 @@ private function notifyRequestSubmitter(
RawMaterial::class => route('admin.master.raw_materials.index'),
Purchase::class => route('admin.manage.purchases.index'),
MarketplaceSettings::class => route('admin.system.settings.index'),
default => route('admin.manage.owner_verifications.index'),
default => route('admin.dashboard'),
};
$this->pushNotificationService->sendToUser(

View File

@ -544,7 +544,7 @@ private function notifyForPendingRequest(User $user, string $typeLabel, string $
"📦 {$typeLabel} Menunggu Persetujuan Owner",
$body,
['owner', 'developer'],
route('admin.manage.owner_verifications.index'),
route('admin.manage.purchases.index'),
);
$this->pushNotificationService->sendToUser(

View File

@ -134,7 +134,7 @@ public function submitVerification(
'📦 Verifikasi Stok Menunggu Persetujuan',
"Cutting dengan deskripsi '{$description}' telah diajukan verifikasi dan menunggu persetujuan owner.",
['owner', 'developer'],
route('admin.manage.owner_verifications.index'),
route('admin.manage.stocks.index'),
);
}
@ -187,7 +187,7 @@ public function approveVerification(
'📦 Stok Cutting Diverifikasi',
"Cutting dengan deskripsi '{$description}' telah disetujui owner dan stok produk telah ditambahkan ke toko.",
['owner', 'developer'],
route('admin.manage.owner_verifications.index'),
route('admin.manage.stocks.index'),
);
}
@ -232,7 +232,7 @@ public function rejectVerification(
'📦 Verifikasi Cutting Ditolak Owner',
"Cutting dengan deskripsi '{$description}' ditolak oleh owner dengan alasan: '{$reason}'.",
['owner', 'developer'],
route('admin.manage.owner_verifications.index'),
route('admin.manage.stocks.index'),
);
if ($cutting->submitted_by_id) {
@ -240,7 +240,7 @@ public function rejectVerification(
'📦 Verifikasi Cutting Ditolak Owner',
"Cutting dengan deskripsi '{$description}' yang Anda verifikasi ditolak oleh owner dengan alasan: '{$reason}'.",
$cutting->submitted_by_id,
route('admin.manage.owner_verifications.index'),
route('admin.manage.stocks.index'),
);
}
}

View File

@ -432,7 +432,7 @@ private function notifyForPendingRequest(User $user, string $typeLabel, string $
"📦 {$typeLabel} Menunggu Persetujuan Owner",
$body,
['owner', 'developer'],
route('admin.manage.owner_verifications.index'),
route('admin.master.products.index'),
);
$this->pushNotificationService->sendToUser(

View File

@ -307,7 +307,7 @@ private function notifyForPendingRequest(User $user, string $typeLabel, string $
"📦 {$typeLabel} Menunggu Persetujuan Owner",
$body,
['owner', 'developer'],
route('admin.manage.owner_verifications.index'),
route('admin.master.raw_materials.index'),
);
$this->pushNotificationService->sendToUser(

View File

@ -125,7 +125,7 @@ public function updateMarketplace(array $validated, User $user): void
'⚙️ Pengaturan Marketplace Menunggu Persetujuan Owner',
"Pengajuan ubah pengaturan marketplace oleh '{$user->username}' menunggu verifikasi owner.",
['owner', 'developer'],
route('admin.manage.owner_verifications.index'),
route('admin.system.settings.index'),
);
$this->pushNotificationService->sendToUser(

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Link, usePage } from '@inertiajs/vue3';
import { Banknote, CalendarDays, CheckCircle, Clock, FolderTree, History, Layers, LayoutDashboard, Package, Receipt, Scissors, Settings2, Shield, ShoppingBag, ShoppingCart, TrendingUp, User, UserCheck, Users, Wallet, WalletCards, Warehouse } from '@lucide/vue';
import { Banknote, CalendarDays, Clock, FolderTree, History, Layers, LayoutDashboard, Package, Receipt, Scissors, Settings2, Shield, ShoppingBag, ShoppingCart, TrendingUp, User, UserCheck, Users, Wallet, WalletCards, Warehouse } from '@lucide/vue';
import {
Sidebar,
SidebarContent,
@ -25,7 +25,7 @@ interface MenuItem {
href: string;
icon: any;
permission?: string;
badgeKey?: 'pendingLeaveRequests' | 'pendingEmployeeAdvances' | 'pendingCuttings' | 'pendingOwnerVerifications';
badgeKey?: 'pendingLeaveRequests' | 'pendingEmployeeAdvances' | 'pendingCuttings';
}
interface MenuGroup {
@ -57,7 +57,6 @@ const menuGroups: MenuGroup[] = [
{ title: 'Belanja', href: admin.manage.purchases.index.url(), icon: ShoppingBag, permission: 'purchases.view' },
{ title: 'Cutting', href: admin.manage.cuttings.index.url(), icon: Scissors, permission: 'cuttings.view' },
{ title: 'Stok Gudang', href: admin.manage.stocks.index.url(), icon: Warehouse, permission: 'stocks.view', badgeKey: 'pendingCuttings' },
{ title: 'Verifikasi Owner', href: admin.manage.owner_verifications.index.url(), icon: CheckCircle, permission: 'owner_verifications.view', badgeKey: 'pendingOwnerVerifications' },
{ title: 'Pesanan', href: admin.manage.orders.index.url(), icon: ShoppingCart, permission: 'orders.view' },
],
},

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Link, usePage } from '@inertiajs/vue3';
import { CalendarDays, CheckCircle, Clock, ShoppingCart } from '@lucide/vue';
import { CalendarDays, Clock, ShoppingCart } from '@lucide/vue';
import { useCan } from '@/composables/useCan';
import admin from '@/routes/admin';
@ -12,7 +12,7 @@ interface NavItem {
href: string;
icon: any;
permission?: string;
badgeKey?: 'pendingLeaveRequests' | 'pendingOwnerVerifications' | 'pendingCuttings';
badgeKey?: 'pendingLeaveRequests' | 'pendingCuttings';
}
const navItems: NavItem[] = [
@ -22,13 +22,6 @@ const navItems: NavItem[] = [
icon: ShoppingCart,
permission: 'orders.view',
},
{
title: 'Verifikasi',
href: admin.manage.owner_verifications.index.url(),
icon: CheckCircle,
permission: 'owner_verifications.view',
badgeKey: 'pendingOwnerVerifications',
},
{
title: 'Presensi',
href: admin.hr.attendances.index.url(),

View File

@ -1,143 +0,0 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { computed, ref, watch } from 'vue';
import { DataTable } from '@/components/data-table';
import { Card, CardContent } from '@/components/ui/card';
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
import AdminLayout from '@/layouts/AdminLayout.vue';
import type {
OwnerVerificationItem,
PaginatedOwnerVerificationRequests,
SelectOption,
} from '@/types/owner-verification';
import type { DataTableSort } from '@/types/data-table';
import OwnerVerificationDetailModal from './table/OwnerVerificationDetailModal.vue';
import { createColumns } from './table/columns';
import { index } from '@/routes/admin/manage/owner_verifications';
const props = defineProps<{
verificationRequests: PaginatedOwnerVerificationRequests;
filters: {
search: string;
sort?: string;
direction?: 'asc' | 'desc';
status?: string;
subject_type?: string;
action?: string;
};
statusOptions: SelectOption[];
subjectOptions: SelectOption[];
actionOptions: SelectOption[];
}>();
const search = ref(props.filters.search ?? '');
const detailModalOpen = ref(false);
const selectedItem = ref<OwnerVerificationItem | null>(null);
const { query, setSearch, setSort, setFilter, resetFilters, syncFromServer } = useDataTableQuery({
url: index.url(),
initial: { ...props.filters },
filterKeys: ['status', 'subject_type', 'action'],
});
useDataTableQuerySync(() => props.filters, syncFromServer);
const columns = computed(() => createColumns(openDetailModal));
const filterDefs = computed(() => [
{
key: 'status',
label: 'Status',
type: 'select' as const,
options: props.statusOptions,
},
{
key: 'subject_type',
label: 'Modul',
type: 'select' as const,
options: props.subjectOptions,
},
{
key: 'action',
label: 'Aksi',
type: 'select' as const,
options: props.actionOptions,
},
]);
const filterValues = computed(() => ({
status: query.value.status ?? '',
subject_type: query.value.subject_type ?? '',
action: query.value.action ?? '',
}));
const currentSort = computed<DataTableSort | null>(() => {
if (!query.value.sort || !query.value.direction) {
return null;
}
return {
column: query.value.sort,
direction: query.value.direction,
};
});
const pagination = computed(() => ({
currentPage: props.verificationRequests.current_page,
perPage: props.verificationRequests.per_page,
lastPage: props.verificationRequests.last_page,
total: props.verificationRequests.total,
}));
function openDetailModal(item: OwnerVerificationItem) {
selectedItem.value = item;
detailModalOpen.value = true;
}
watch(search, (value) => {
setSearch(value);
});
watch(
() => props.filters.search,
(value) => {
search.value = value ?? '';
},
);
</script>
<template>
<Head title="Verifikasi Owner" />
<AdminLayout>
<div class="space-y-1">
<h2 class="text-2xl font-bold tracking-tight">
Verifikasi Owner
</h2>
</div>
<Card class="min-w-0">
<CardContent class="min-w-0">
<DataTable
v-model:search="search"
:columns="columns"
:data="verificationRequests.data"
:pagination="pagination"
:pagination-links="verificationRequests.links"
:sort="currentSort"
:filter-defs="filterDefs"
:filter-values="filterValues"
search-placeholder="Cari verifikasi..."
@sort-change="setSort"
@filter-change="setFilter"
@filters-reset="resetFilters"
/>
</CardContent>
</Card>
<OwnerVerificationDetailModal
v-model:open="detailModalOpen"
:item="selectedItem"
/>
</AdminLayout>
</template>

View File

@ -1,120 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import type { OwnerVerificationItem } from '@/types/owner-verification';
const open = defineModel<boolean>('open', { default: false });
const props = defineProps<{
item?: OwnerVerificationItem | null;
}>();
const hasChanges = computed(() => (props.item?.changes.length ?? 0) > 0);
function formatValue(value: unknown): string {
if (value === null || value === undefined || value === '') {
return '-';
}
if (typeof value === 'boolean') {
return value ? 'Aktif' : 'Nonaktif';
}
if (typeof value === 'object') {
return JSON.stringify(value);
}
return String(value);
}
</script>
<template>
<Dialog v-model:open="open">
<DialogContent class="max-h-[85vh] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Detail Verifikasi</DialogTitle>
</DialogHeader>
<div v-if="item" class="space-y-4 text-sm">
<div class="grid gap-3 sm:grid-cols-2">
<div>
<p class="text-muted-foreground">Waktu</p>
<p class="font-medium">{{ item.created_at_formatted ?? '-' }}</p>
</div>
<div>
<p class="text-muted-foreground">Diajukan Oleh</p>
<p class="font-medium">{{ item.submitted_by_name }}</p>
</div>
<div>
<p class="text-muted-foreground">Aksi</p>
<p class="font-medium">{{ item.action_label }}</p>
</div>
<div>
<p class="text-muted-foreground">Modul</p>
<p class="font-medium">{{ item.subject_label }}</p>
</div>
<div>
<p class="text-muted-foreground">Item</p>
<p class="font-medium">{{ item.title }}</p>
</div>
<div>
<p class="text-muted-foreground">Status</p>
<Badge
:variant="item.status === 'approved' ? 'default' : item.status === 'rejected' ? 'destructive' : 'outline'"
>
{{ item.status_label }}
</Badge>
</div>
<div v-if="item.verified_by_name">
<p class="text-muted-foreground">Diverifikasi Oleh</p>
<p class="font-medium">{{ item.verified_by_name }}</p>
</div>
<div v-if="item.verified_at_formatted">
<p class="text-muted-foreground">Waktu Verifikasi</p>
<p class="font-medium">{{ item.verified_at_formatted }}</p>
</div>
</div>
<div v-if="item.summary" class="rounded-md border p-3">
<p class="text-muted-foreground">Ringkasan</p>
<p class="font-medium">{{ item.summary }}</p>
</div>
<div v-if="item.rejection_reason" class="rounded-md border border-destructive/30 bg-destructive/5 p-3">
<p class="text-muted-foreground">Alasan Penolakan</p>
<p class="font-medium">{{ item.rejection_reason }}</p>
</div>
<div v-if="hasChanges" class="space-y-2">
<p class="font-medium">Perubahan Data</p>
<div class="overflow-hidden rounded-md border">
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr>
<th class="px-3 py-2 text-left font-medium">Field</th>
<th class="px-3 py-2 text-left font-medium">Sebelum</th>
<th class="px-3 py-2 text-left font-medium">Sesudah</th>
</tr>
</thead>
<tbody>
<tr v-for="change in item.changes" :key="change.field" class="border-t">
<td class="px-3 py-2 align-top font-medium">{{ change.field }}</td>
<td class="px-3 py-2 align-top text-muted-foreground">
{{ formatValue(change.old) }}
</td>
<td class="px-3 py-2 align-top">{{ formatValue(change.new) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</DialogContent>
</Dialog>
</template>

View File

@ -1,70 +0,0 @@
import type { ColumnDef } from '@tanstack/vue-table';
import { h } from 'vue';
import { Badge } from '@/components/ui/badge';
import { DataTableColumnHeader } from '@/components/data-table';
import type { OwnerVerificationItem } from '@/types/owner-verification';
import DataTableActions from './data-table-actions.vue';
function statusBadgeVariant(status: string): 'default' | 'destructive' | 'outline' | 'secondary' {
if (status === 'approved') {
return 'default';
}
if (status === 'rejected') {
return 'destructive';
}
return 'outline';
}
export function createColumns(
onView: (item: OwnerVerificationItem) => void,
): ColumnDef<OwnerVerificationItem>[] {
return [
{
accessorKey: 'created_at_formatted',
enableSorting: true,
header: () => h(DataTableColumnHeader, { title: 'Waktu', column: 'created_at' }),
cell: ({ row }) => row.original.created_at_formatted ?? '-',
},
{
accessorKey: 'submitted_by_name',
enableSorting: false,
header: 'Diajukan Oleh',
},
{
accessorKey: 'action_label',
enableSorting: true,
header: () => h(DataTableColumnHeader, { title: 'Aksi', column: 'action' }),
},
{
accessorKey: 'subject_label',
enableSorting: false,
header: 'Modul',
},
{
accessorKey: 'title',
enableSorting: false,
header: 'Item',
},
{
accessorKey: 'status_label',
enableSorting: true,
header: () => h(DataTableColumnHeader, { title: 'Status', column: 'status' }),
cell: ({ row }) => h(
Badge,
{ variant: statusBadgeVariant(row.original.status) },
() => row.original.status_label,
),
},
{
id: 'actions',
enableSorting: false,
enableHiding: false,
cell: ({ row }) => h(DataTableActions, {
item: row.original,
onView: () => onView(row.original),
}),
},
];
}

View File

@ -1,22 +0,0 @@
<script setup lang="ts">
import { RowDetailAction } from '@/components/button';
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
import type { OwnerVerificationItem } from '@/types/owner-verification';
const props = defineProps<{
item: OwnerVerificationItem;
}>();
const emit = defineEmits<{
view: [];
}>();
</script>
<template>
<div class="flex items-center justify-end gap-1">
<RowDetailAction tooltip="Lihat" @click="emit('view')" />
<OwnerVerificationRowActions v-if="item.is_pending" :type="item.source === 'cutting' ? 'cutting' : 'request'"
:id="item.id" :title="item.title" :subject-label="item.subject_label" :action-label="item.action_label" />
</div>
</template>

View File

@ -1,51 +0,0 @@
export type OwnerVerificationSource = 'cutting' | 'request';
export interface VerificationChange {
field: string;
old: unknown;
new: unknown;
}
export interface OwnerVerificationItem {
id: number;
source: OwnerVerificationSource;
subject_type: string | null;
subject_label: string;
subject_id: number | null;
action: string;
action_label: string;
status: string;
status_label: string;
title: string;
summary?: string;
submitted_by_name: string;
verified_by_name?: string | null;
verified_at_formatted?: string | null;
created_at?: string | null;
created_at_formatted: string | null;
changes: VerificationChange[];
rejection_reason?: string | null;
is_pending: boolean;
detail?: Record<string, unknown>;
}
/** @deprecated Use OwnerVerificationItem */
export type OwnerVerificationRequestItem = OwnerVerificationItem;
export interface SelectOption {
value: string;
label: string;
}
export interface PaginatedOwnerVerificationRequests {
data: OwnerVerificationItem[];
current_page: number;
per_page: number;
last_page: number;
total: number;
links: Array<{
url: string | null;
label: string;
active: boolean;
}>;
}

View File

@ -356,10 +356,7 @@
});
Route::prefix('owner-verifications')->name('owner_verifications.')
->middleware('permission:'.Permission::OWNER_VERIFICATIONS_VIEW->value)
->group(function () {
Route::get('/', [OwnerVerificationController::class, 'index'])->name('index');
Route::post('cuttings/{cutting}/approve', [OwnerVerificationController::class, 'approveCutting'])
->middleware('permission:'.Permission::OWNER_VERIFICATIONS_VERIFY->value)
->name('approve');

View File

@ -1,354 +0,0 @@
<?php
use App\Enums\CuttingStatus;
use App\Enums\OwnerVerificationAction;
use App\Enums\OwnerVerificationStatus;
use App\Enums\Permission as PermissionEnum;
use App\Enums\Role as RoleEnum;
use App\Models\Cutting;
use App\Models\OwnerVerificationRequest;
use App\Models\Product;
use App\Models\Purchase;
use App\Models\User;
use App\Support\ActivityLog\ModelLabel;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->seed(RolePermissionSeeder::class);
});
function createOwnerVerificationRequestFor(User $submitter, ?Product $product = null): OwnerVerificationRequest
{
$product ??= Product::factory()->create(['is_active' => false]);
return OwnerVerificationRequest::query()->create([
'action' => OwnerVerificationAction::CREATE,
'status' => OwnerVerificationStatus::PENDING,
'subject_type' => Product::class,
'subject_id' => $product->id,
'submitted_by_id' => $submitter->id,
'payload' => ['old' => null, 'new' => ['name' => $product->name]],
]);
}
function createOwnerVerifierUser(): User
{
$user = User::factory()->create();
$user->assignRole(RoleEnum::OWNER->value);
$user->forgetCachedPermissions();
return $user;
}
function createPendingCutting(?User $submitter = null): Cutting
{
$submitter ??= User::factory()->create();
return Cutting::factory()->create([
'status' => CuttingStatus::PENDING_VERIFICATION,
'submitted_by_id' => $submitter->id,
'created_by_id' => $submitter->id,
'description' => 'Cutting test pending',
]);
}
describe('Owner Verification Index', function () {
test('submitter with view permission only sees own verification requests', function () {
$submitter = User::factory()->create();
$submitter->assignRole(RoleEnum::ADMIN_TOKO->value);
$submitter->forgetCachedPermissions();
$otherUser = User::factory()->create();
$otherUser->assignRole(RoleEnum::ADMIN_TOKO->value);
$otherUser->forgetCachedPermissions();
$ownRequest = createOwnerVerificationRequestFor($submitter);
createOwnerVerificationRequestFor($otherUser);
$response = $this->actingAs($submitter)
->get(route('admin.manage.owner_verifications.index'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('admin/manage/owner-verifications/Index')
->has('verificationRequests.data', 1)
->where('verificationRequests.data.0.id', $ownRequest->id)
->where('verificationRequests.data.0.source', 'request')
);
});
test('verifier sees all verification requests', function () {
$submitter = User::factory()->create();
$submitter->assignRole(RoleEnum::ADMIN_TOKO->value);
$submitter->forgetCachedPermissions();
$verifier = createOwnerVerifierUser();
createOwnerVerificationRequestFor($submitter);
createOwnerVerificationRequestFor($submitter);
$response = $this->actingAs($verifier)
->get(route('admin.manage.owner_verifications.index'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('admin/manage/owner-verifications/Index')
->has('verificationRequests.data', 2)
);
});
test('submitter cannot approve verification request', function () {
$submitter = User::factory()->create();
$submitter->assignRole(RoleEnum::ADMIN_TOKO->value);
$submitter->forgetCachedPermissions();
$request = createOwnerVerificationRequestFor($submitter);
$this->actingAs($submitter)
->post(route('admin.manage.owner_verifications.approve_request', $request))
->assertForbidden();
});
test('admin toko role has owner verification view permission', function () {
$user = User::factory()->create();
$user->assignRole(RoleEnum::ADMIN_TOKO->value);
$user->forgetCachedPermissions();
expect($user->can(PermissionEnum::OWNER_VERIFICATIONS_VIEW->value))->toBeTrue();
expect($user->can(PermissionEnum::OWNER_VERIFICATIONS_VERIFY->value))->toBeFalse();
});
test('admin bahan baku role has owner verification view permission', function () {
$user = User::factory()->create();
$user->assignRole(RoleEnum::ADMIN_BAHAN_BAKU->value);
$user->forgetCachedPermissions();
expect($user->can(PermissionEnum::OWNER_VERIFICATIONS_VIEW->value))->toBeTrue();
expect($user->can(PermissionEnum::OWNER_VERIFICATIONS_VERIFY->value))->toBeFalse();
});
});
describe('Owner Verification Unified Cutting List', function () {
test('verifier sees pending cuttings in the same verification table', function () {
$verifier = createOwnerVerifierUser();
$cutting = createPendingCutting();
$request = createOwnerVerificationRequestFor(User::factory()->create());
$response = $this->actingAs($verifier)
->get(route('admin.manage.owner_verifications.index'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('admin/manage/owner-verifications/Index')
->has('verificationRequests.data', 2)
->where('verificationRequests.data.0.source', 'cutting')
->where('verificationRequests.data.0.id', $cutting->id)
->where('verificationRequests.data.0.action', OwnerVerificationAction::STOCK_VERIFY->value)
->where('verificationRequests.data.0.subject_type', Cutting::class)
->where('verificationRequests.data.1.source', 'request')
->where('verificationRequests.data.1.id', $request->id)
);
});
test('submitter does not see pending cuttings in verification table', function () {
$submitter = User::factory()->create();
$submitter->assignRole(RoleEnum::ADMIN_TOKO->value);
$submitter->forgetCachedPermissions();
createPendingCutting();
createOwnerVerificationRequestFor($submitter);
$response = $this->actingAs($submitter)
->get(route('admin.manage.owner_verifications.index'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('admin/manage/owner-verifications/Index')
->has('verificationRequests.data', 1)
->where('verificationRequests.data.0.source', 'request')
);
});
test('filtering by cutting module shows only pending cuttings', function () {
$verifier = createOwnerVerifierUser();
$cutting = createPendingCutting();
createOwnerVerificationRequestFor(User::factory()->create());
$response = $this->actingAs($verifier)
->get(route('admin.manage.owner_verifications.index', [
'subject_type' => Cutting::class,
]));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->has('verificationRequests.data', 1)
->where('verificationRequests.data.0.source', 'cutting')
->where('verificationRequests.data.0.id', $cutting->id)
);
});
test('filtering by stock verify action shows only pending cuttings', function () {
$verifier = createOwnerVerifierUser();
$cutting = createPendingCutting();
createOwnerVerificationRequestFor(User::factory()->create());
$response = $this->actingAs($verifier)
->get(route('admin.manage.owner_verifications.index', [
'action' => OwnerVerificationAction::STOCK_VERIFY->value,
]));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->has('verificationRequests.data', 1)
->where('verificationRequests.data.0.source', 'cutting')
->where('verificationRequests.data.0.id', $cutting->id)
);
});
test('filtering by approved status hides pending cuttings', function () {
$verifier = createOwnerVerifierUser();
createPendingCutting();
OwnerVerificationRequest::query()->create([
'action' => OwnerVerificationAction::CREATE,
'status' => OwnerVerificationStatus::APPROVED,
'subject_type' => Product::class,
'subject_id' => Product::factory()->create()->id,
'submitted_by_id' => User::factory()->create()->id,
'payload' => ['old' => null, 'new' => ['name' => 'Approved Product']],
'verified_by_id' => $verifier->id,
'verified_at' => now(),
]);
$response = $this->actingAs($verifier)
->get(route('admin.manage.owner_verifications.index', [
'status' => OwnerVerificationStatus::APPROVED->value,
]));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->has('verificationRequests.data', 1)
->where('verificationRequests.data.0.source', 'request')
->where('verificationRequests.data.0.status', OwnerVerificationStatus::APPROVED->value)
);
});
test('subject options include cutting when pending cuttings exist', function () {
$verifier = createOwnerVerifierUser();
createPendingCutting();
$response = $this->actingAs($verifier)
->get(route('admin.manage.owner_verifications.index'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->where('subjectOptions', fn ($options) => collect($options)->contains(
fn (array $option) => $option['value'] === Cutting::class
&& $option['label'] === ModelLabel::for(Cutting::class),
))
);
});
test('verifier can approve pending cutting from verification page', function () {
$verifier = createOwnerVerifierUser();
$cutting = createPendingCutting();
$this->actingAs($verifier)
->post(route('admin.manage.owner_verifications.approve', $cutting))
->assertRedirect(route('admin.manage.owner_verifications.index'));
expect($cutting->fresh()->status)->toBe(CuttingStatus::VERIFIED);
});
test('verifier can reject pending cutting from verification page', function () {
$verifier = createOwnerVerifierUser();
$cutting = createPendingCutting();
$this->actingAs($verifier)
->post(route('admin.manage.owner_verifications.reject', $cutting), [
'reason' => 'Data tidak sesuai',
])
->assertRedirect(route('admin.manage.owner_verifications.index'));
$cutting->refresh();
expect($cutting->status)->toBe(CuttingStatus::COMPLETED);
expect($cutting->rejection?->reason)->toBe('Data tidak sesuai');
expect($cutting->rejection?->rejected_by_id)->toBe($verifier->id);
});
});
describe('Owner Verification Purchase Module', function () {
test('verifier sees purchase verification requests in index', function () {
$verifier = createOwnerVerifierUser();
$submitter = User::factory()->create();
$purchase = Purchase::factory()->create();
OwnerVerificationRequest::query()->create([
'action' => OwnerVerificationAction::CREATE,
'status' => OwnerVerificationStatus::PENDING,
'subject_type' => Purchase::class,
'subject_id' => $purchase->id,
'submitted_by_id' => $submitter->id,
'payload' => [
'old' => null,
'new' => ['supplier_name' => $purchase->supplier?->name ?? 'Supplier'],
],
]);
$response = $this->actingAs($verifier)
->get(route('admin.manage.owner_verifications.index', [
'subject_type' => Purchase::class,
]));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->has('verificationRequests.data', 1)
->where('verificationRequests.data.0.source', 'request')
->where('verificationRequests.data.0.subject_type', Purchase::class)
->where('verificationRequests.data.0.subject_id', $purchase->id)
);
});
test('purchase submitter sees only own purchase verification requests', function () {
$submitter = User::factory()->create();
$submitter->assignRole(RoleEnum::ADMIN_BAHAN_BAKU->value);
$submitter->forgetCachedPermissions();
$otherUser = User::factory()->create();
$ownPurchase = Purchase::factory()->create(['created_by_id' => $submitter->id]);
$otherPurchase = Purchase::factory()->create(['created_by_id' => $otherUser->id]);
OwnerVerificationRequest::query()->create([
'action' => OwnerVerificationAction::CREATE,
'status' => OwnerVerificationStatus::PENDING,
'subject_type' => Purchase::class,
'subject_id' => $ownPurchase->id,
'submitted_by_id' => $submitter->id,
'payload' => ['old' => null, 'new' => ['supplier_name' => 'Own']],
]);
OwnerVerificationRequest::query()->create([
'action' => OwnerVerificationAction::CREATE,
'status' => OwnerVerificationStatus::PENDING,
'subject_type' => Purchase::class,
'subject_id' => $otherPurchase->id,
'submitted_by_id' => $otherUser->id,
'payload' => ['old' => null, 'new' => ['supplier_name' => 'Other']],
]);
$response = $this->actingAs($submitter)
->get(route('admin.manage.owner_verifications.index', [
'subject_type' => Purchase::class,
]));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->has('verificationRequests.data', 1)
->where('verificationRequests.data.0.subject_id', $ownPurchase->id)
);
});
});

View File

@ -56,7 +56,7 @@ function approveLatestPurchaseVerificationRequest(User $verifier): OwnerVerifica
test()->actingAs($verifier)
->post(route('admin.manage.owner_verifications.approve_request', $verificationRequest))
->assertRedirect(route('admin.manage.owner_verifications.index'));
->assertRedirect();
return $verificationRequest->fresh();
}
@ -263,7 +263,7 @@ function approveLatestPurchaseVerificationRequest(User $verifier): OwnerVerifica
->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest), [
'reason' => 'Tidak sesuai',
])
->assertRedirect(route('admin.manage.owner_verifications.index'));
->assertRedirect();
expect(Purchase::query()->whereKey($purchase->id)->exists())->toBeFalse();
expect(PurchaseItem::query()->where('purchase_id', $purchase->id)->exists())->toBeFalse();

View File

@ -76,7 +76,7 @@ function approveLatestOwnerVerificationRequest(User $verifier): OwnerVerificatio
test()->actingAs($verifier)
->post(route('admin.manage.owner_verifications.approve_request', $verificationRequest))
->assertRedirect(route('admin.manage.owner_verifications.index'));
->assertRedirect();
return $verificationRequest->fresh();
}
@ -431,7 +431,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest), [
'reason' => 'Tidak sesuai standar',
])
->assertRedirect(route('admin.manage.owner_verifications.index'));
->assertRedirect();
$this->assertSoftDeleted('products', ['id' => $product->id]);
});

View File

@ -84,7 +84,7 @@ function approveLatestOwnerVerificationRequest(User $verifier): OwnerVerificatio
test()->actingAs($verifier)
->post(route('admin.manage.owner_verifications.approve_request', $verificationRequest))
->assertRedirect(route('admin.manage.owner_verifications.index'));
->assertRedirect();
return $verificationRequest->fresh();
}