feat: enhance cutting status management by adding verified status transition and refining user permissions for verification actions

This commit is contained in:
Yoga Pangestu 2026-06-28 22:02:21 +07:00
parent 573e81193a
commit aeb7d1f292
7 changed files with 370 additions and 192 deletions

View File

@ -46,7 +46,7 @@ public function canTransitionTo(self $status): bool
{ {
return match ($this) { return match ($this) {
self::IN_PROGRESS => $status === self::COMPLETED, self::IN_PROGRESS => $status === self::COMPLETED,
self::COMPLETED => in_array($status, [self::PENDING_VERIFICATION, self::REJECTED, self::IN_PROGRESS], true), self::COMPLETED => in_array($status, [self::PENDING_VERIFICATION, self::REJECTED, self::IN_PROGRESS, self::VERIFIED], true),
self::PENDING_VERIFICATION => in_array($status, [self::VERIFIED, self::REJECTED, self::COMPLETED], true), self::PENDING_VERIFICATION => in_array($status, [self::VERIFIED, self::REJECTED, self::COMPLETED], true),
self::REJECTED => $status === self::IN_PROGRESS, self::REJECTED => $status === self::IN_PROGRESS,
default => false, default => false,
@ -80,7 +80,15 @@ public function availableActions(): array
'icon_only' => false, 'icon_only' => false,
], ],
], ],
self::COMPLETED => [], self::COMPLETED => [
[
'status' => self::VERIFIED->value,
'label' => 'Verifikasi',
'destructive' => false,
'permission' => Permission::CUTTINGS_VERIFY->value,
'icon_only' => false,
],
],
self::PENDING_VERIFICATION => [], self::PENDING_VERIFICATION => [],
self::REJECTED => [ self::REJECTED => [
[ [

View File

@ -5,6 +5,7 @@
use App\Enums\CuttingStatus; use App\Enums\CuttingStatus;
use App\Enums\Permission; use App\Enums\Permission;
use App\Enums\PriceType; use App\Enums\PriceType;
use App\Enums\Role;
use App\Models\Cutting; use App\Models\Cutting;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@ -83,6 +84,14 @@ public function withValidator(Validator $validator): void
$validator->errors()->add('status', 'Status cutting tidak dapat diubah.'); $validator->errors()->add('status', 'Status cutting tidak dapat diubah.');
} }
// Only owner/developer can verify directly from COMPLETED status
if ($cutting->status === CuttingStatus::COMPLETED && $status === CuttingStatus::VERIFIED) {
$user = $this->user();
if (! $user || ! $user->hasRole([Role::OWNER->value, Role::DEVELOPER->value])) {
$validator->errors()->add('status', 'Hanya owner yang dapat langsung memverifikasi cutting.');
}
}
if ($status === CuttingStatus::REJECTED && trim((string) $this->input('reason')) === '') { if ($status === CuttingStatus::REJECTED && trim((string) $this->input('reason')) === '') {
$validator->errors()->add('reason', 'Alasan penolakan wajib diisi.'); $validator->errors()->add('reason', 'Alasan penolakan wajib diisi.');
} }

View File

@ -3,6 +3,7 @@
namespace App\Services\Manage; namespace App\Services\Manage;
use App\Enums\CuttingStatus; use App\Enums\CuttingStatus;
use App\Enums\Role;
use App\Models\Cutting; use App\Models\Cutting;
use App\Models\CuttingMaterial; use App\Models\CuttingMaterial;
use App\Models\CuttingResult; use App\Models\CuttingResult;
@ -58,12 +59,7 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
->paginate(25) ->paginate(25)
->withQueryString() ->withQueryString()
->through(function (Cutting $cutting) use ($user) { ->through(function (Cutting $cutting) use ($user) {
$actions = collect($cutting->status->availableActions()) $cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user));
->filter(fn (array $action) => $user->can($action['permission']))
->values()
->all();
$cutting->setAttribute('available_actions', $actions);
$cutting->setAttribute('is_editable', $cutting->status->isEditable()); $cutting->setAttribute('is_editable', $cutting->status->isEditable());
$this->appendCostPreview($cutting); $this->appendCostPreview($cutting);
@ -85,12 +81,7 @@ public function getInProgressCuttings(User $user): Collection
->latest() ->latest()
->get() ->get()
->each(function (Cutting $cutting) use ($user): void { ->each(function (Cutting $cutting) use ($user): void {
$actions = collect($cutting->status->availableActions()) $cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user));
->filter(fn (array $action) => $user->can($action['permission']))
->values()
->all();
$cutting->setAttribute('available_actions', $actions);
$cutting->setAttribute('is_editable', $cutting->status->isEditable()); $cutting->setAttribute('is_editable', $cutting->status->isEditable());
$this->appendCostPreview($cutting); $this->appendCostPreview($cutting);
}); });
@ -110,12 +101,7 @@ public function getCompletedCuttings(User $user): Collection
->latest() ->latest()
->get() ->get()
->each(function (Cutting $cutting) use ($user): void { ->each(function (Cutting $cutting) use ($user): void {
$actions = collect($cutting->status->availableActions()) $cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user));
->filter(fn (array $action) => $user->can($action['permission']))
->values()
->all();
$cutting->setAttribute('available_actions', $actions);
$cutting->setAttribute('is_editable', $cutting->status->isEditable()); $cutting->setAttribute('is_editable', $cutting->status->isEditable());
$this->appendCostPreview($cutting); $this->appendCostPreview($cutting);
}); });
@ -891,6 +877,25 @@ private function formatMaterialUsageSummary(Cutting $cutting): string
->join(', '); ->join(', ');
} }
/**
* @return list<array{status: string, label: string, destructive: bool, permission: string, icon_only: bool}>
*/
private function filterActionsForUser(Cutting $cutting, User $user): array
{
return collect($cutting->status->availableActions())
->filter(fn (array $action) => $user->can($action['permission']))
->filter(function (array $action) use ($cutting, $user): bool {
// Only owner/developer can verify directly from COMPLETED status
if ($cutting->status === CuttingStatus::COMPLETED && $action['status'] === CuttingStatus::VERIFIED->value) {
return $user->hasRole([Role::OWNER->value, Role::DEVELOPER->value]);
}
return true;
})
->values()
->all();
}
private function appendCostPreview(Cutting $cutting): void private function appendCostPreview(Cutting $cutting): void
{ {
$cutting->setAttribute('total_result_pieces', (int) $cutting->results->sum('cutting_result')); $cutting->setAttribute('total_result_pieces', (int) $cutting->results->sum('cutting_result'));

View File

@ -4,6 +4,7 @@
use App\Enums\OwnerVerificationAction; use App\Enums\OwnerVerificationAction;
use App\Enums\OwnerVerificationStatus; use App\Enums\OwnerVerificationStatus;
use App\Enums\Permission;
use App\Models\OwnerVerificationRequest; use App\Models\OwnerVerificationRequest;
use App\Models\Purchase; use App\Models\Purchase;
use App\Models\PurchaseItem; use App\Models\PurchaseItem;
@ -201,8 +202,10 @@ public function removeDraftItem(User $user, RawMaterialPrice $rawMaterialPrice):
public function create(array $validated, User $user): Purchase public function create(array $validated, User $user): Purchase
{ {
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
try { try {
$purchase = DB::transaction(function () use ($validated, $user): Purchase { $purchase = DB::transaction(function () use ($validated, $user, $isOwner): Purchase {
$draftItems = $this->draftItemsQuery($user) $draftItems = $this->draftItemsQuery($user)
->lockForUpdate() ->lockForUpdate()
@ -239,24 +242,30 @@ public function create(array $validated, User $user): Purchase
$purchase->load(['supplier', 'items.rawMaterialPrice.rawMaterial:id,name']); $purchase->load(['supplier', 'items.rawMaterialPrice.rawMaterial:id,name']);
OwnerVerificationRequest::create([ if ($isOwner) {
'action' => OwnerVerificationAction::CREATE, foreach ($purchase->items as $item) {
'status' => OwnerVerificationStatus::PENDING, $this->incrementStock($item);
'subject_type' => Purchase::class, }
'subject_id' => $purchase->id, } else {
'submitted_by_id' => $user->id, OwnerVerificationRequest::create([
'payload' => [ 'action' => OwnerVerificationAction::CREATE,
'old' => null, 'status' => OwnerVerificationStatus::PENDING,
'new' => $this->snapshotPurchase($purchase), 'subject_type' => Purchase::class,
], 'subject_id' => $purchase->id,
]); 'submitted_by_id' => $user->id,
'payload' => [
'old' => null,
'new' => $this->snapshotPurchase($purchase),
],
]);
}
return $purchase; return $purchase;
}); });
} catch (ValidationException $e) { } catch (ValidationException $e) {
throw $e; throw $e;
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error('Gagal mengajukan pembelian: '.$e->getMessage(), [ Log::error('Gagal membuat pembelian: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
]); ]);
@ -265,43 +274,55 @@ public function create(array $validated, User $user): Purchase
]); ]);
} }
$this->notifyForPendingRequest( if (! $isOwner) {
$user, $this->notifyForPendingRequest(
'Tambah Belanja', $user,
"Pengajuan belanja dari supplier {$purchase->supplier->name} senilai {$purchase->total_formatted} menunggu verifikasi owner.", 'Tambah Belanja',
route('admin.manage.purchases.index', ['search' => $purchase->supplier->name]), "Pengajuan belanja dari supplier {$purchase->supplier->name} senilai {$purchase->total_formatted} menunggu verifikasi owner.",
$purchase->supplier->name, route('admin.manage.purchases.index', ['search' => $purchase->supplier->name]),
); $purchase->supplier->name,
);
}
return $purchase; return $purchase;
} }
public function update(Purchase $purchase, array $validated, User $user): void public function update(Purchase $purchase, array $validated, User $user): void
{ {
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
$purchase->load(['supplier', 'items.rawMaterialPrice.rawMaterial:id,name']); $purchase->load(['supplier', 'items.rawMaterialPrice.rawMaterial:id,name']);
try { try {
DB::transaction(function () use ($purchase, $validated, $user): void { DB::transaction(function () use ($purchase, $validated, $user, $isOwner): void {
$verificationRequest = OwnerVerificationRequest::create([ if ($isOwner) {
'action' => OwnerVerificationAction::UPDATE, $payload = $this->buildPayloadFromValidated($validated);
'status' => OwnerVerificationStatus::PENDING, $this->applyPayloadToPurchase($purchase, $payload);
'subject_type' => Purchase::class,
'subject_id' => $purchase->id,
'submitted_by_id' => $user->id,
'payload' => [
'old' => $this->snapshotPurchase($purchase),
'new' => $this->buildPayloadFromValidated($validated),
],
]);
if (($validated['photos'] ?? null) !== null || ($validated['remove_media_ids'] ?? []) !== []) { if (($validated['photos'] ?? null) !== null || ($validated['remove_media_ids'] ?? []) !== []) {
$this->syncRequestPhotos($verificationRequest, $validated); $this->syncPhotos($purchase, $validated);
}
} else {
$verificationRequest = OwnerVerificationRequest::create([
'action' => OwnerVerificationAction::UPDATE,
'status' => OwnerVerificationStatus::PENDING,
'subject_type' => Purchase::class,
'subject_id' => $purchase->id,
'submitted_by_id' => $user->id,
'payload' => [
'old' => $this->snapshotPurchase($purchase),
'new' => $this->buildPayloadFromValidated($validated),
],
]);
if (($validated['photos'] ?? null) !== null || ($validated['remove_media_ids'] ?? []) !== []) {
$this->syncRequestPhotos($verificationRequest, $validated);
}
} }
}); });
} catch (ValidationException $e) { } catch (ValidationException $e) {
throw $e; throw $e;
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error('Gagal mengajukan perubahan belanja: '.$e->getMessage(), [ Log::error('Gagal memperbarui pembelian: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
]); ]);
@ -310,19 +331,29 @@ public function update(Purchase $purchase, array $validated, User $user): void
]); ]);
} }
$purchase->load('supplier'); if (! $isOwner) {
$purchase->load('supplier');
$this->notifyForPendingRequest( $this->notifyForPendingRequest(
$user, $user,
'Ubah Belanja', 'Ubah Belanja',
"Pengajuan ubah belanja dari supplier {$purchase->supplier->name} menunggu verifikasi owner.", "Pengajuan ubah belanja dari supplier {$purchase->supplier->name} menunggu verifikasi owner.",
route('admin.manage.purchases.index', ['search' => $purchase->supplier->name]), route('admin.manage.purchases.index', ['search' => $purchase->supplier->name]),
$purchase->supplier->name, $purchase->supplier->name,
); );
}
} }
public function delete(Purchase $purchase, User $user): void public function delete(Purchase $purchase, User $user): void
{ {
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
if ($isOwner) {
$this->executeDelete($purchase);
return;
}
$purchase->load(['supplier', 'items.rawMaterialPrice.rawMaterial:id,name']); $purchase->load(['supplier', 'items.rawMaterialPrice.rawMaterial:id,name']);
try { try {

View File

@ -4,6 +4,7 @@
use App\Enums\OwnerVerificationAction; use App\Enums\OwnerVerificationAction;
use App\Enums\OwnerVerificationStatus; use App\Enums\OwnerVerificationStatus;
use App\Enums\Permission;
use App\Models\OwnerVerificationRequest; use App\Models\OwnerVerificationRequest;
use App\Models\ProductVariant; use App\Models\ProductVariant;
use App\Models\StockRetailHistory; use App\Models\StockRetailHistory;
@ -19,7 +20,7 @@ public function __construct(
private readonly PushNotificationService $pushNotificationService, private readonly PushNotificationService $pushNotificationService,
) {} ) {}
public function transfer(int $variantId, int $quantity, User $user, ?string $notes = null): OwnerVerificationRequest public function transfer(int $variantId, int $quantity, User $user, ?string $notes = null): ?OwnerVerificationRequest
{ {
if ($quantity <= 0) { if ($quantity <= 0) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
@ -35,6 +36,14 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
]); ]);
} }
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
if ($isOwner) {
$this->executeTransfer($variant, $quantity, $user, $notes);
return null;
}
$existingPending = OwnerVerificationRequest::query() $existingPending = OwnerVerificationRequest::query()
->where('subject_type', ProductVariant::class) ->where('subject_type', ProductVariant::class)
->where('subject_id', $variant->id) ->where('subject_id', $variant->id)
@ -111,9 +120,20 @@ public function applyStockRetailTransfer(OwnerVerificationRequest $request): voi
return; return;
} }
DB::transaction(function () use ($request, $quantity, $newData): void { $variant = ProductVariant::query()->find($request->subject_id);
if (! $variant) {
return;
}
$this->executeTransfer($variant, $quantity, $request->submittedBy, $newData['notes'] ?? null);
}
private function executeTransfer(ProductVariant $variant, int $quantity, User $user, ?string $notes = null): void
{
DB::transaction(function () use ($variant, $quantity, $user, $notes): void {
$variant = ProductVariant::query() $variant = ProductVariant::query()
->whereKey($request->subject_id) ->whereKey($variant->id)
->lockForUpdate() ->lockForUpdate()
->firstOrFail(); ->firstOrFail();
@ -125,13 +145,13 @@ public function applyStockRetailTransfer(OwnerVerificationRequest $request): voi
StockRetailHistory::create([ StockRetailHistory::create([
'product_variant_id' => $variant->id, 'product_variant_id' => $variant->id,
'user_id' => $request->submitted_by_id, 'user_id' => $user->id,
'quantity' => $quantity, 'quantity' => $quantity,
'stock_before' => $stockBefore, 'stock_before' => $stockBefore,
'stock_retail_before' => $stockRetailBefore, 'stock_retail_before' => $stockRetailBefore,
'stock_after' => $stockBefore - $quantity, 'stock_after' => $stockBefore - $quantity,
'stock_retail_after' => $stockRetailBefore + $quantity, 'stock_retail_after' => $stockRetailBefore + $quantity,
'notes' => $newData['notes'] ?? null, 'notes' => $notes,
'created_at' => now(), 'created_at' => now(),
]); ]);
}); });

View File

@ -4,6 +4,7 @@
use App\Enums\OwnerVerificationAction; use App\Enums\OwnerVerificationAction;
use App\Enums\OwnerVerificationStatus; use App\Enums\OwnerVerificationStatus;
use App\Enums\Permission;
use App\Models\Category; use App\Models\Category;
use App\Models\OwnerVerificationRequest; use App\Models\OwnerVerificationRequest;
use App\Models\Product; use App\Models\Product;
@ -115,12 +116,14 @@ public function findForEdit(Product $product): Product
public function create(array $validated, User $user): void public function create(array $validated, User $user): void
{ {
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
try { try {
DB::transaction(function () use ($validated, $user): void { DB::transaction(function () use ($validated, $user, $isOwner): void {
$product = Product::create([ $product = Product::create([
'name' => $validated['name'], 'name' => $validated['name'],
'description' => $validated['description'] ?? null, 'description' => $validated['description'] ?? null,
'is_active' => false, 'is_active' => $isOwner,
]); ]);
$product->categories()->sync($validated['category_ids'] ?? []); $product->categories()->sync($validated['category_ids'] ?? []);
@ -135,64 +138,24 @@ public function create(array $validated, User $user): void
$this->syncVariantImages($variant, $variantData, $index); $this->syncVariantImages($variant, $variantData, $index);
} }
OwnerVerificationRequest::create([ if (! $isOwner) {
'action' => OwnerVerificationAction::CREATE, OwnerVerificationRequest::create([
'status' => OwnerVerificationStatus::PENDING, 'action' => OwnerVerificationAction::CREATE,
'subject_type' => Product::class, 'status' => OwnerVerificationStatus::PENDING,
'subject_id' => $product->id, 'subject_type' => Product::class,
'submitted_by_id' => $user->id, 'subject_id' => $product->id,
'payload' => [ 'submitted_by_id' => $user->id,
'old' => null, 'payload' => [
'new' => $this->snapshotProduct($product->fresh(['categories', 'variants'])), 'old' => null,
], 'new' => $this->snapshotProduct($product->fresh(['categories', 'variants'])),
]); ],
}); ]);
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal mengajukan pembuatan produk: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
$this->notifyForPendingRequest(
$user,
'Tambah Produk',
"Pengajuan tambah produk '{$validated['name']}' menunggu verifikasi owner.",
route('admin.master.products.index', ['search' => $validated['name']]),
$validated['name'],
);
}
public function update(Product $product, array $validated, User $user): void
{
try {
DB::transaction(function () use ($validated, $product, $user): void {
$verificationRequest = OwnerVerificationRequest::create([
'action' => OwnerVerificationAction::UPDATE,
'status' => OwnerVerificationStatus::PENDING,
'subject_type' => Product::class,
'subject_id' => $product->id,
'submitted_by_id' => $user->id,
'payload' => [
'old' => $this->snapshotProduct($product),
'new' => $this->enrichPayload($this->buildPayloadFromValidated($validated)),
],
]);
foreach ($validated['variants'] as $index => $variantData) {
$isNewVariant = empty($variantData['id']);
$this->syncRequestVariantImages($verificationRequest, $variantData, $index, required: $isNewVariant);
} }
}); });
} catch (ValidationException $e) { } catch (ValidationException $e) {
throw $e; throw $e;
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error('Gagal mengajukan perubahan produk: '.$e->getMessage(), [ Log::error('Gagal membuat produk: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
]); ]);
@ -201,17 +164,94 @@ public function update(Product $product, array $validated, User $user): void
]); ]);
} }
$this->notifyForPendingRequest( if (! $isOwner) {
$user, $this->notifyForPendingRequest(
'Ubah Produk', $user,
"Pengajuan ubah produk '{$product->name}' menunggu verifikasi owner.", 'Tambah Produk',
route('admin.master.products.index', ['search' => $product->name]), "Pengajuan tambah produk '{$validated['name']}' menunggu verifikasi owner.",
$product->name, route('admin.master.products.index', ['search' => $validated['name']]),
); $validated['name'],
);
}
}
public function update(Product $product, array $validated, User $user): void
{
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
try {
DB::transaction(function () use ($validated, $product, $user, $isOwner): void {
if ($isOwner) {
$payload = $this->enrichPayload($this->buildPayloadFromValidated($validated));
$this->applyPayloadToProduct($product, $payload);
foreach ($validated['variants'] as $index => $variantData) {
if (! empty($variantData['id'])) {
$variant = $product->variants()->find($variantData['id']);
if ($variant) {
$this->syncVariantImages($variant, $variantData, $index);
}
} else {
$variant = $product->variants()->create([
'name' => $variantData['name'],
'stock' => $variantData['stock'],
'stock_retail' => $variantData['stock_retail'],
]);
$this->syncVariantImages($variant, $variantData, $index);
}
}
} else {
$verificationRequest = OwnerVerificationRequest::create([
'action' => OwnerVerificationAction::UPDATE,
'status' => OwnerVerificationStatus::PENDING,
'subject_type' => Product::class,
'subject_id' => $product->id,
'submitted_by_id' => $user->id,
'payload' => [
'old' => $this->snapshotProduct($product),
'new' => $this->enrichPayload($this->buildPayloadFromValidated($validated)),
],
]);
foreach ($validated['variants'] as $index => $variantData) {
$isNewVariant = empty($variantData['id']);
$this->syncRequestVariantImages($verificationRequest, $variantData, $index, required: $isNewVariant);
}
}
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal memperbarui produk: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
if (! $isOwner) {
$this->notifyForPendingRequest(
$user,
'Ubah Produk',
"Pengajuan ubah produk '{$product->name}' menunggu verifikasi owner.",
route('admin.master.products.index', ['search' => $product->name]),
$product->name,
);
}
} }
public function delete(Product $product, User $user): void public function delete(Product $product, User $user): void
{ {
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
if ($isOwner) {
$this->applyDeleteSubject($product);
return;
}
try { try {
OwnerVerificationRequest::create([ OwnerVerificationRequest::create([
'action' => OwnerVerificationAction::DELETE, 'action' => OwnerVerificationAction::DELETE,
@ -245,6 +285,16 @@ public function delete(Product $product, User $user): void
public function toggleStatus(Product $product, array $validated, User $user): void public function toggleStatus(Product $product, array $validated, User $user): void
{ {
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
if ($isOwner) {
$product->update([
'is_active' => (bool) $validated['is_active'],
]);
return;
}
try { try {
OwnerVerificationRequest::create([ OwnerVerificationRequest::create([
'action' => OwnerVerificationAction::TOGGLE_STATUS, 'action' => OwnerVerificationAction::TOGGLE_STATUS,
@ -418,6 +468,11 @@ public function applyDelete(OwnerVerificationRequest $verificationRequest): void
]); ]);
} }
$this->applyDeleteSubject($product);
}
private function applyDeleteSubject(Product $product): void
{
DB::transaction(function () use ($product): void { DB::transaction(function () use ($product): void {
$product->variants()->each(function (ProductVariant $variant): void { $product->variants()->each(function (ProductVariant $variant): void {
$variant->clearMediaCollection('images'); $variant->clearMediaCollection('images');

View File

@ -4,6 +4,7 @@
use App\Enums\OwnerVerificationAction; use App\Enums\OwnerVerificationAction;
use App\Enums\OwnerVerificationStatus; use App\Enums\OwnerVerificationStatus;
use App\Enums\Permission;
use App\Enums\RawMaterialUnit; use App\Enums\RawMaterialUnit;
use App\Models\OwnerVerificationRequest; use App\Models\OwnerVerificationRequest;
use App\Models\RawMaterial; use App\Models\RawMaterial;
@ -109,76 +110,38 @@ public function findForEdit(RawMaterial $rawMaterial): RawMaterial
public function create(array $validated, User $user): void public function create(array $validated, User $user): void
{ {
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
try { try {
DB::transaction(function () use ($validated, $user): void { DB::transaction(function () use ($validated, $user, $isOwner): void {
$rawMaterial = RawMaterial::create([ $rawMaterial = RawMaterial::create([
'name' => $validated['name'], 'name' => $validated['name'],
'unit' => $validated['unit'], 'unit' => $validated['unit'],
'is_active' => false, 'is_active' => $isOwner,
]); ]);
foreach ($validated['prices'] as $index => $priceData) { foreach ($validated['prices'] as $index => $priceData) {
$this->createPrice($rawMaterial, $priceData, $index); $this->createPrice($rawMaterial, $priceData, $index);
} }
OwnerVerificationRequest::create([ if (! $isOwner) {
'action' => OwnerVerificationAction::CREATE, OwnerVerificationRequest::create([
'status' => OwnerVerificationStatus::PENDING, 'action' => OwnerVerificationAction::CREATE,
'subject_type' => RawMaterial::class, 'status' => OwnerVerificationStatus::PENDING,
'subject_id' => $rawMaterial->id, 'subject_type' => RawMaterial::class,
'submitted_by_id' => $user->id, 'subject_id' => $rawMaterial->id,
'payload' => [ 'submitted_by_id' => $user->id,
'old' => null, 'payload' => [
'new' => $this->snapshotRawMaterial($rawMaterial->fresh(['prices'])), 'old' => null,
], 'new' => $this->snapshotRawMaterial($rawMaterial->fresh(['prices'])),
]); ],
}); ]);
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal mengajukan pembuatan bahan baku: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
$this->notifyForPendingRequest(
$user,
'Tambah Bahan Baku',
"Pengajuan tambah bahan baku '{$validated['name']}' menunggu verifikasi owner.",
route('admin.master.raw_materials.index', ['search' => $validated['name']]),
$validated['name'],
);
}
public function update(RawMaterial $rawMaterial, array $validated, User $user): void
{
try {
DB::transaction(function () use ($validated, $rawMaterial, $user): void {
$verificationRequest = OwnerVerificationRequest::create([
'action' => OwnerVerificationAction::UPDATE,
'status' => OwnerVerificationStatus::PENDING,
'subject_type' => RawMaterial::class,
'subject_id' => $rawMaterial->id,
'submitted_by_id' => $user->id,
'payload' => [
'old' => $this->snapshotRawMaterial($rawMaterial),
'new' => $this->enrichPayload($this->buildPayloadFromValidated($validated)),
],
]);
foreach ($validated['prices'] as $index => $priceData) {
$isNewPrice = empty($priceData['id']);
$this->syncRequestPriceImages($verificationRequest, $priceData, $index, required: $isNewPrice);
} }
}); });
} catch (ValidationException $e) { } catch (ValidationException $e) {
throw $e; throw $e;
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error('Gagal mengajukan perubahan bahan baku: '.$e->getMessage(), [ Log::error('Gagal membuat bahan baku: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
]); ]);
@ -187,17 +150,89 @@ public function update(RawMaterial $rawMaterial, array $validated, User $user):
]); ]);
} }
$this->notifyForPendingRequest( if (! $isOwner) {
$user, $this->notifyForPendingRequest(
'Ubah Bahan Baku', $user,
"Pengajuan ubah bahan baku '{$rawMaterial->name}' menunggu verifikasi owner.", 'Tambah Bahan Baku',
route('admin.master.raw_materials.index', ['search' => $rawMaterial->name]), "Pengajuan tambah bahan baku '{$validated['name']}' menunggu verifikasi owner.",
$rawMaterial->name, route('admin.master.raw_materials.index', ['search' => $validated['name']]),
); $validated['name'],
);
}
}
public function update(RawMaterial $rawMaterial, array $validated, User $user): void
{
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
try {
DB::transaction(function () use ($validated, $rawMaterial, $user, $isOwner): void {
if ($isOwner) {
$payload = $this->enrichPayload($this->buildPayloadFromValidated($validated));
$this->applyPayloadToRawMaterial($rawMaterial, $payload);
foreach ($validated['prices'] as $index => $priceData) {
if (! empty($priceData['id'])) {
$price = $rawMaterial->prices()->find($priceData['id']);
if ($price) {
$this->syncPriceImages($price, $priceData, $index);
}
} else {
$this->createPrice($rawMaterial, $priceData, $index);
}
}
} else {
$verificationRequest = OwnerVerificationRequest::create([
'action' => OwnerVerificationAction::UPDATE,
'status' => OwnerVerificationStatus::PENDING,
'subject_type' => RawMaterial::class,
'subject_id' => $rawMaterial->id,
'submitted_by_id' => $user->id,
'payload' => [
'old' => $this->snapshotRawMaterial($rawMaterial),
'new' => $this->enrichPayload($this->buildPayloadFromValidated($validated)),
],
]);
foreach ($validated['prices'] as $index => $priceData) {
$isNewPrice = empty($priceData['id']);
$this->syncRequestPriceImages($verificationRequest, $priceData, $index, required: $isNewPrice);
}
}
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal memperbarui bahan baku: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
if (! $isOwner) {
$this->notifyForPendingRequest(
$user,
'Ubah Bahan Baku',
"Pengajuan ubah bahan baku '{$rawMaterial->name}' menunggu verifikasi owner.",
route('admin.master.raw_materials.index', ['search' => $rawMaterial->name]),
$rawMaterial->name,
);
}
} }
public function delete(RawMaterial $rawMaterial, User $user): void public function delete(RawMaterial $rawMaterial, User $user): void
{ {
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
if ($isOwner) {
$this->applyDeleteSubject($rawMaterial);
return;
}
try { try {
OwnerVerificationRequest::create([ OwnerVerificationRequest::create([
'action' => OwnerVerificationAction::DELETE, 'action' => OwnerVerificationAction::DELETE,
@ -231,6 +266,16 @@ public function delete(RawMaterial $rawMaterial, User $user): void
public function toggleStatus(RawMaterial $rawMaterial, array $validated, User $user): void public function toggleStatus(RawMaterial $rawMaterial, array $validated, User $user): void
{ {
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
if ($isOwner) {
$rawMaterial->update([
'is_active' => (bool) $validated['is_active'],
]);
return;
}
try { try {
OwnerVerificationRequest::create([ OwnerVerificationRequest::create([
'action' => OwnerVerificationAction::TOGGLE_STATUS, 'action' => OwnerVerificationAction::TOGGLE_STATUS,
@ -366,6 +411,11 @@ public function applyDelete(OwnerVerificationRequest $verificationRequest): void
]); ]);
} }
$this->applyDeleteSubject($rawMaterial);
}
private function applyDeleteSubject(RawMaterial $rawMaterial): void
{
DB::transaction(function () use ($rawMaterial): void { DB::transaction(function () use ($rawMaterial): void {
$rawMaterial->prices()->each(function (RawMaterialPrice $price): void { $rawMaterial->prices()->each(function (RawMaterialPrice $price): void {
$price->clearMediaCollection('images'); $price->clearMediaCollection('images');