with([ 'supplier:id,name', 'createdBy.profile', 'pendingOwnerVerificationRequest.submittedBy.profile', 'items.rawMaterialPrice.rawMaterial:id,name,unit', 'media', ]) ->when(($tableQuery['search_id'] ?? '') !== '', function (Builder $query) use ($tableQuery): void { $query->where('purchases.id', $tableQuery['search_id']); }) ->when(($tableQuery['search_id'] ?? '') === '' && $tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void { $search = $tableQuery['search']; $query->where(function (Builder $query) use ($search): void { $query->where('notes', 'like', "%{$search}%") ->orWhereHas('supplier', fn (Builder $query) => $query->where('name', 'like', "%{$search}%")) ->orWhereHas('items.rawMaterialPrice', function (Builder $query) use ($search): void { $query->where('variant', 'like', "%{$search}%") ->orWhereHas('rawMaterial', fn (Builder $query) => $query->where('name', 'like', "%{$search}%")); }); }); }); $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']); return $query ->paginate(25) ->withQueryString() ->through(function (Purchase $purchase) { $purchase->setAttribute( 'photos', MediaPresenter::first($purchase, 'photos'), ); $pendingRequest = $purchase->pendingOwnerVerificationRequest; $purchase->setAttribute('has_pending_request', $pendingRequest !== null); $purchase->setAttribute('pending_request_id', $pendingRequest?->id); $purchase->setAttribute('pending_request_action', $pendingRequest?->action->value); $purchase->setAttribute('pending_request_action_label', $pendingRequest?->action->label()); $purchase->setAttribute('pending_request_submitted_by_name', $pendingRequest?->submittedBy?->profile?->full_name ?? $pendingRequest?->submittedBy?->username); $purchase->items->each(fn (PurchaseItem $item) => $this->breakItemCircularReference($item)); return $purchase; }); } public function supplierOptions(): array { return Supplier::query() ->orderBy('name') ->get(['id', 'name']) ->map(fn (Supplier $supplier) => [ 'value' => $supplier->id, 'label' => $supplier->name, ]) ->all(); } public function catalogItems(?Purchase $purchase = null, ?User $user = null): Collection { $purchasePriceIds = $purchase ? $purchase->items()->pluck('raw_material_price_id')->all() : ($user ? $this->draftItemsQuery($user)->pluck('raw_material_price_id')->all() : []); return RawMaterial::query() ->with([ 'prices' => fn ($query) => $query ->orderBy('created_at') ->with('media'), ]) ->where(function (Builder $query) use ($purchasePriceIds): void { $query->active(); if ($purchasePriceIds !== []) { $query->orWhereHas( 'prices', fn (Builder $query) => $query->whereIn('id', $purchasePriceIds), ); } }) ->orderBy('name') ->get() ->each(function (RawMaterial $rawMaterial): void { $rawMaterial->prices->each(function (RawMaterialPrice $price) use ($rawMaterial): void { $price->setAttribute('images', MediaPresenter::collection($price, 'images')); $price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation()); $price->unsetRelation('rawMaterial'); }); }); } public function findForEdit(Purchase $purchase): Purchase { $purchase->load([ 'items.rawMaterialPrice.rawMaterial:id,name,unit', 'media', ]); $purchase->setAttribute( 'photos', MediaPresenter::first($purchase, 'photos'), ); $purchase->items->each(function (PurchaseItem $item): void { $price = $item->rawMaterialPrice; if ($price) { $price->setAttribute('images', MediaPresenter::collection($price, 'images')); } $this->breakItemCircularReference($item); }); return $purchase; } public function draftItemsForUser(User $user): array { return $this->draftItemsQuery($user) ->with([ 'rawMaterialPrice.rawMaterial:id,name,unit', 'rawMaterialPrice.media', ]) ->get() ->map(function (PurchaseItem $item) { $result = $this->presentDraftItem($item); $this->breakItemCircularReference($item); return $result; }) ->values() ->all(); } public function syncDraftItem(array $validated, User $user): array { $price = RawMaterialPrice::query() ->with('rawMaterial:id,name,unit') ->findOrFail($validated['raw_material_price_id']); $quantity = (float) $validated['quantity']; $unitPrice = (int) $price->price; $subtotal = (int) round($quantity * $unitPrice); $item = PurchaseItem::query()->updateOrCreate( [ 'user_id' => $user->id, 'raw_material_price_id' => $price->id, 'purchase_id' => null, ], [ 'quantity' => $quantity, 'unit_price' => $unitPrice, 'subtotal' => $subtotal, ], ); $item->load([ 'rawMaterialPrice.rawMaterial:id,name,unit', 'rawMaterialPrice.media', ]); $result = $this->presentDraftItem($item); $this->breakItemCircularReference($item); return $result; } public function createVariantAndDraft(array $validated, User $user): array { $rawMaterial = RawMaterial::query()->findOrFail($validated['raw_material_id']); $price = RawMaterialPrice::create([ 'raw_material_id' => $rawMaterial->id, 'variant' => $validated['variant'], 'price' => (int) $validated['price'], 'stock' => (float) ($validated['stock'] ?? 0), ]); if (! empty($validated['photos']) || ! empty($validated['s3_keys'])) { $this->mediaService->syncCollection( $price, 'images', $validated['photos'] ?? null, null, 5, required: false, errorKey: 'photos', s3Keys: $validated['s3_keys'] ?? null, ); } $price->setAttribute('images', MediaPresenter::collection($price, 'images')); $this->pushNotificationService->sendToRoles( '🆕 Varian Baru Ditambahkan', "{$user->name} menambahkan varian \"{$price->variant}\" ke bahan baku \"{$rawMaterial->name}\".", ['owner', 'developer', 'direktur'], route('admin.master.raw_materials.index', ['search_id' => $rawMaterial->id]), ); return [ 'id' => $price->id, 'variant' => $price->variant, 'price' => $price->price, 'price_formatted' => $price->price_formatted, 'price_input' => $price->price_input, 'stock' => $price->stock, 'stock_formatted' => $price->stock_formatted, 'stock_input' => $price->stock_input, 'images' => $price->images, ]; } public function createRawMaterialAndDraft(array $validated, User $user): array { $data = $this->runInTransaction( function () use ($validated, $user): array { $isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value); $rawMaterial = RawMaterial::withTrashed() ->where('name', $validated['name']) ->where('unit', $validated['unit']) ->first(); if ($rawMaterial) { if ($rawMaterial->trashed()) { $rawMaterial->restore(); } if ($isOwner && ! $rawMaterial->is_active) { $rawMaterial->update(['is_active' => true]); } } else { $rawMaterial = RawMaterial::create([ 'name' => $validated['name'], 'unit' => $validated['unit'], 'is_active' => $isOwner, ]); } $price = RawMaterialPrice::withTrashed() ->where('raw_material_id', $rawMaterial->id) ->where('variant', $validated['variant']) ->first(); if ($price) { if ($price->trashed()) { $price->restore(); } $price->update([ 'price' => (int) $validated['price'], ]); } else { $price = RawMaterialPrice::create([ 'raw_material_id' => $rawMaterial->id, 'variant' => $validated['variant'], 'price' => (int) $validated['price'], 'stock' => 0.0, ]); } $quantity = (float) $validated['quantity']; $unitPrice = (int) $price->price; $subtotal = (int) round($quantity * $unitPrice); $existingItem = PurchaseItem::query() ->where('user_id', $user->id) ->where('raw_material_price_id', $price->id) ->whereNull('purchase_id') ->first(); if ($existingItem) { $newQty = $existingItem->quantity + $quantity; $existingItem->update([ 'quantity' => $newQty, 'unit_price' => $unitPrice, 'subtotal' => (int) round($newQty * $unitPrice), ]); $item = $existingItem; } else { $item = PurchaseItem::create([ 'user_id' => $user->id, 'raw_material_price_id' => $price->id, 'purchase_id' => null, 'quantity' => $quantity, 'unit_price' => $unitPrice, 'subtotal' => $subtotal, ]); } return [$price, $item]; }, 'Gagal membuat bahan baku baru dan menambahkan ke keranjang' ); [$price, $item] = $data; if ($price->media()->count() === 0 && (! empty($validated['photos']) || ! empty($validated['s3_keys']))) { $this->mediaService->syncCollection( $price, 'images', $validated['photos'] ?? null, null, 5, required: false, errorKey: 'photos', s3Keys: $validated['s3_keys'] ?? null, ); } $item->load([ 'rawMaterialPrice.rawMaterial:id,name,unit', 'rawMaterialPrice.media', ]); $result = $this->presentDraftItem($item); $this->breakItemCircularReference($item); return $result; } public function removeDraftItem(User $user, RawMaterialPrice $rawMaterialPrice): void { PurchaseItem::query() ->whereNull('purchase_id') ->where('user_id', $user->id) ->where('raw_material_price_id', $rawMaterialPrice->id) ->delete(); } public function create(array $validated, User $user): Purchase { $isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value); $purchase = $this->runInTransaction( function () use ($validated, $user, $isOwner): Purchase { $resolvedItems = $this->processRequestItems($validated['items'] ?? [], $isOwner); $subtotal = array_sum(array_column($resolvedItems, 'subtotal')); $discount = (int) ($validated['discount'] ?? 0); $shippingCost = (int) ($validated['shipping_cost'] ?? 0); $total = max($subtotal - $discount + $shippingCost, 0); $purchase = Purchase::create([ 'supplier_id' => $validated['supplier_id'], 'created_by_id' => $user->id, 'subtotal' => $subtotal, 'discount' => $discount, 'shipping_cost' => $shippingCost, 'total' => $total, 'notes' => $validated['notes'] ?? null, ]); foreach ($resolvedItems as $itemData) { $purchase->items()->create([ 'raw_material_price_id' => $itemData['raw_material_price_id'], 'quantity' => $itemData['quantity'], 'unit_price' => $itemData['unit_price'], 'subtotal' => $itemData['subtotal'], ]); } $this->syncPhotos($purchase, $validated); $purchase->load(['supplier', 'items.rawMaterialPrice.rawMaterial:id,name']); if ($isOwner) { foreach ($purchase->items as $item) { $this->incrementStock($item); } } else { OwnerVerificationRequest::create([ 'action' => OwnerVerificationAction::CREATE, 'status' => OwnerVerificationStatus::PENDING, 'subject_type' => Purchase::class, 'subject_id' => $purchase->id, 'submitted_by_id' => $user->id, 'payload' => [ 'old' => null, 'new' => $this->snapshotPurchase($purchase), ], ]); } return $purchase; }, 'Gagal membuat pembelian', ); if (! $isOwner) { $this->notifyForPendingRequest( $user, 'Tambah Belanja', "Pengajuan belanja dari supplier {$purchase->supplier->name} senilai {$purchase->total_formatted} oleh {$user->profile?->full_name} menunggu verifikasi owner.", route('admin.manage.purchases.index', ['search_id' => $purchase->id]), (string) $purchase->id, ); } $this->cacheForgetByPattern('manage:purchases:*'); return $purchase; } 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']); $this->runInTransaction( function () use ($purchase, $validated, $user, $isOwner): void { if ($isOwner) { $payload = $this->buildPayloadFromValidated($validated, $isOwner); $this->applyPayloadToPurchase($purchase, $payload); if (($validated['photos'] ?? null) !== null || ($validated['remove_media_ids'] ?? []) !== []) { $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, $isOwner), ], ]); if (($validated['photos'] ?? null) !== null || ($validated['remove_media_ids'] ?? []) !== []) { $this->syncRequestPhotos($verificationRequest, $validated); } } }, 'Gagal memperbarui belanja', ); if (! $isOwner) { $purchase->load('supplier'); $this->notifyForPendingRequest( $user, 'Ubah Belanja', "Pengajuan ubah belanja dari supplier {$purchase->supplier->name} oleh {$user->profile?->full_name} menunggu verifikasi owner.", route('admin.manage.purchases.index', ['search_id' => $purchase->id]), (string) $purchase->id, ); } $this->cacheForgetByPattern('manage:purchases:*'); } public function delete(Purchase $purchase, User $user): void { $isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value); if ($isOwner) { $this->executeDelete($purchase); $this->cacheForgetByPattern('manage:purchases:*'); return; } $purchase->load(['supplier', 'items.rawMaterialPrice.rawMaterial:id,name']); $this->runInTransaction( function () use ($purchase, $user): void { OwnerVerificationRequest::create([ 'action' => OwnerVerificationAction::DELETE, 'status' => OwnerVerificationStatus::PENDING, 'subject_type' => Purchase::class, 'subject_id' => $purchase->id, 'submitted_by_id' => $user->id, 'payload' => [ 'old' => $this->snapshotPurchase($purchase), 'new' => null, ], ]); }, 'Gagal mengajukan penghapusan belanja', ); $purchase->load('supplier'); $this->notifyForPendingRequest( $user, 'Hapus Belanja', "Pengajuan hapus belanja dari supplier {$purchase->supplier->name} oleh {$user->profile?->full_name} menunggu verifikasi owner.", route('admin.manage.purchases.index', ['search_id' => $purchase->id]), (string) $purchase->id, ); } public function applyVerificationRequest(OwnerVerificationRequest $verificationRequest): void { match ($verificationRequest->action) { OwnerVerificationAction::CREATE => $this->applyCreate($verificationRequest), OwnerVerificationAction::UPDATE => $this->applyUpdate($verificationRequest), OwnerVerificationAction::DELETE => $this->applyDelete($verificationRequest), default => throw ValidationException::withMessages([ 'action' => 'Aksi verifikasi belanja tidak didukung.', ]), }; $this->cacheForgetByPattern('manage:purchases:*'); } public function rejectVerificationRequest(OwnerVerificationRequest $verificationRequest): void { match ($verificationRequest->action) { OwnerVerificationAction::CREATE => $this->rejectCreate($verificationRequest), OwnerVerificationAction::UPDATE, OwnerVerificationAction::DELETE => null, default => throw ValidationException::withMessages([ 'action' => 'Aksi verifikasi belanja tidak didukung.', ]), }; $this->cacheForgetByPattern('manage:purchases:*'); } public function clearVerificationRequestMedia(OwnerVerificationRequest $verificationRequest): void { $verificationRequest->clearMediaCollection('photos'); } public function applyCreate(OwnerVerificationRequest $verificationRequest): void { $purchase = $verificationRequest->subject; if (! $purchase instanceof Purchase) { throw ValidationException::withMessages([ 'purchase' => 'Belanja tidak ditemukan.', ]); } $purchase->load('items'); foreach ($purchase->items as $item) { $this->incrementStock($item); } } public function applyUpdate(OwnerVerificationRequest $verificationRequest): void { $purchase = $verificationRequest->subject; if (! $purchase instanceof Purchase) { throw ValidationException::withMessages([ 'purchase' => 'Belanja tidak ditemukan.', ]); } $this->applyPayloadToPurchase($purchase, $this->payloadNew($verificationRequest), $verificationRequest); } public function applyDelete(OwnerVerificationRequest $verificationRequest): void { $purchase = $verificationRequest->subject; if (! $purchase instanceof Purchase) { throw ValidationException::withMessages([ 'purchase' => 'Belanja tidak ditemukan.', ]); } $this->executeDelete($purchase); } private function buildLineItems(array $items): array { return collect($items) ->map(function (array $itemData, int $index) { $price = RawMaterialPrice::query()->find($itemData['raw_material_price_id']); if ($price === null) { throw ValidationException::withMessages([ "items.{$index}.raw_material_price_id" => 'Bahan baku tidak ditemukan.', ]); } $quantity = (float) $itemData['quantity']; $unitPrice = (int) $price->price; $subtotal = (int) round($quantity * $unitPrice); return [ 'raw_material_price_id' => $price->id, 'quantity' => $quantity, 'unit_price' => $unitPrice, 'subtotal' => $subtotal, ]; }) ->all(); } private function syncPhotos(Purchase $purchase, array $validated): void { $this->mediaService->syncCollection( $purchase, 'photos', $validated['photos'] ?? null, $validated['remove_media_ids'] ?? null, self::MAX_PHOTOS, required: false, errorKey: 'photos', s3Keys: $validated['s3_keys'] ?? null, ); } private function draftItemsQuery(User $user): Builder { return PurchaseItem::query() ->whereNull('purchase_id') ->where('user_id', $user->id); } private function presentDraftItem(PurchaseItem $item): array { $price = $item->rawMaterialPrice; $rawMaterial = $price?->rawMaterial; return [ 'raw_material_price_id' => $item->raw_material_price_id, 'raw_material_name' => $rawMaterial?->name ?? '', 'variant' => $price?->variant ?? '', 'unit_abbreviation' => $rawMaterial?->unit?->abbreviation() ?? '', 'quantity' => $item->quantity_input, 'unit_price' => $item->unit_price, 'images' => $price ? MediaPresenter::collection($price, 'images') : [], ]; } private function incrementStock(PurchaseItem $item): void { RawMaterialPrice::query() ->whereKey($item->raw_material_price_id) ->increment('stock', $item->quantity); } private function decrementStock(PurchaseItem $item): void { RawMaterialPrice::query() ->whereKey($item->raw_material_price_id) ->decrement('stock', $item->quantity); } private function notifyForPendingRequest(User $user, string $typeLabel, string $body, string $submitterUrl, ?string $search = null): void { $ownerUrl = route('admin.manage.purchases.index'); if ($search !== null) { $ownerUrl = route('admin.manage.purchases.index', ['search_id' => $search]); } $this->pushNotificationService->sendToRoles( "📦 {$typeLabel} Menunggu Persetujuan Owner", $body, ['owner', 'developer', 'direktur'], $ownerUrl, ); $this->pushNotificationService->sendToUser( '📤 Pengajuan Terkirim', $body, $user->id, $submitterUrl, ); } private function rejectCreate(OwnerVerificationRequest $verificationRequest): void { $purchase = $verificationRequest->subject; if (! $purchase instanceof Purchase) { return; } $this->runInTransaction( function () use ($purchase): void { $purchase->clearMediaCollection('photos'); $purchase->items()->delete(); $purchase->delete(); }, 'Gagal menolak belanja', ); } private function executeDelete(Purchase $purchase): void { $this->runInTransaction( function () use ($purchase): void { $purchase->load('items'); foreach ($purchase->items as $item) { $this->decrementStock($item); } $purchase->clearMediaCollection('photos'); $purchase->items()->delete(); $purchase->delete(); }, 'Gagal menghapus belanja', ); } private function applyPayloadToPurchase( Purchase $purchase, array $payload, ?OwnerVerificationRequest $verificationRequest = null, ): void { $this->runInTransaction( function () use ($purchase, $payload, $verificationRequest): void { $purchase->load('items'); foreach ($purchase->items as $item) { $this->decrementStock($item); } $purchase->items()->delete(); foreach ($payload['items'] ?? [] as $itemData) { $purchaseItem = $purchase->items()->create([ 'raw_material_price_id' => $itemData['raw_material_price_id'], 'quantity' => $itemData['quantity'], 'unit_price' => $itemData['unit_price'], 'subtotal' => $itemData['subtotal'], ]); $this->incrementStock($purchaseItem); } $purchase->update([ 'supplier_id' => $payload['supplier_id'], 'subtotal' => $payload['subtotal'], 'discount' => $payload['discount'], 'shipping_cost' => $payload['shipping_cost'], 'total' => $payload['total'], 'notes' => $payload['notes'] ?? null, ]); if ($verificationRequest !== null) { $this->applyRequestPhotos($verificationRequest, $purchase, $payload); } }, 'Gagal memperbarui belanja', ); } private function snapshotPurchase(Purchase $purchase): array { $purchase->load([ 'supplier:id,name', 'items.rawMaterialPrice.rawMaterial:id,name', ]); return [ 'supplier_id' => $purchase->supplier_id, 'supplier_name' => $purchase->supplier?->name, 'subtotal' => $purchase->subtotal, 'discount' => $purchase->discount, 'shipping_cost' => $purchase->shipping_cost, 'total' => $purchase->total, 'notes' => $purchase->notes, 'items' => $purchase->items ->map(fn (PurchaseItem $item) => [ 'raw_material_price_id' => $item->raw_material_price_id, 'raw_material_name' => $item->rawMaterialPrice?->rawMaterial?->name, 'variant' => $item->rawMaterialPrice?->variant, 'quantity' => (float) $item->quantity, 'unit_price' => $item->unit_price, 'subtotal' => $item->subtotal, ]) ->all(), 'has_photos' => $purchase->hasMedia('photos'), ]; } private function buildPayloadFromValidated(array $validated, bool $isOwner): array { $lineItems = $this->enrichLineItems($this->processRequestItems($validated['items'], $isOwner)); $subtotal = array_sum(array_column($lineItems, 'subtotal')); $discount = (int) ($validated['discount'] ?? 0); $shippingCost = (int) ($validated['shipping_cost'] ?? 0); $total = max($subtotal - $discount + $shippingCost, 0); $supplier = Supplier::query()->find($validated['supplier_id']); return [ 'supplier_id' => (int) $validated['supplier_id'], 'supplier_name' => $supplier?->name, 'subtotal' => $subtotal, 'discount' => $discount, 'shipping_cost' => $shippingCost, 'total' => $total, 'notes' => $validated['notes'] ?? null, 'items' => $lineItems, 'remove_media_ids' => $validated['remove_media_ids'] ?? [], ]; } private function processRequestItems(array $items, bool $isOwner): array { $resolvedItems = []; foreach ($items as $index => $itemData) { // Resolve RawMaterial $rawMaterial = RawMaterial::withTrashed() ->where('name', $itemData['name']) ->where('unit', $itemData['unit']) ->first(); if ($rawMaterial) { if ($rawMaterial->trashed()) { $rawMaterial->restore(); } if ($isOwner && ! $rawMaterial->is_active) { $rawMaterial->update(['is_active' => true]); } } else { $rawMaterial = RawMaterial::create([ 'name' => $itemData['name'], 'unit' => $itemData['unit'], 'is_active' => $isOwner, ]); } // Resolve RawMaterialPrice (variant) $price = RawMaterialPrice::withTrashed() ->where('raw_material_id', $rawMaterial->id) ->where('variant', $itemData['variant']) ->first(); if ($price) { if ($price->trashed()) { $price->restore(); } $price->update([ 'price' => (int) $itemData['price'], ]); } else { $price = RawMaterialPrice::create([ 'raw_material_id' => $rawMaterial->id, 'variant' => $itemData['variant'], 'price' => (int) $itemData['price'], 'stock' => 0.0, ]); } // Sync variant photo if provided if (! empty($itemData['photos']) || ! empty($itemData['s3_keys']) || ! empty($itemData['remove_media_ids'])) { $this->mediaService->syncCollection( $price, 'images', $itemData['photos'] ?? null, $itemData['remove_media_ids'] ?? null, 5, required: false, errorKey: "items.{$index}.photos", s3Keys: $itemData['s3_keys'] ?? null, ); } $quantity = (float) $itemData['quantity']; $unitPrice = (int) $price->price; $lineSubtotal = (int) round($quantity * $unitPrice); $resolvedItems[] = [ 'raw_material_price_id' => $price->id, 'quantity' => $quantity, 'unit_price' => $unitPrice, 'subtotal' => $lineSubtotal, ]; } return $resolvedItems; } private function enrichLineItems(array $lineItems): array { $prices = RawMaterialPrice::query() ->with('rawMaterial:id,name') ->whereIn('id', array_column($lineItems, 'raw_material_price_id')) ->get() ->keyBy('id'); return collect($lineItems) ->map(function (array $item) use ($prices) { $price = $prices->get($item['raw_material_price_id']); return array_merge($item, [ 'raw_material_name' => $price?->rawMaterial?->name, 'variant' => $price?->variant, ]); }) ->all(); } private function syncRequestPhotos(OwnerVerificationRequest $verificationRequest, array $validated): void { $this->mediaService->syncCollection( $verificationRequest, 'photos', $validated['photos'] ?? null, $validated['remove_media_ids'] ?? null, self::MAX_PHOTOS, required: false, errorKey: 'photos', s3Keys: $validated['s3_keys'] ?? null, ); } private function applyRequestPhotos( OwnerVerificationRequest $verificationRequest, Purchase $purchase, array $payload, ): void { if ($verificationRequest->hasMedia('photos')) { $purchase->clearMediaCollection('photos'); foreach ($verificationRequest->getMedia('photos') as $media) { $media->copy($purchase, 'photos'); } return; } foreach ($payload['remove_media_ids'] ?? [] as $mediaId) { $purchase->deleteMedia((int) $mediaId); } } private function payloadNew(OwnerVerificationRequest $verificationRequest): array { $payload = $verificationRequest->payload ?? []; if (array_key_exists('new', $payload)) { return is_array($payload['new']) ? $payload['new'] : []; } return is_array($payload) ? $payload : []; } private function breakItemCircularReference(PurchaseItem $item): void { $price = $item->rawMaterialPrice; if ($price) { $rawMaterial = $price->rawMaterial; $item->setAttribute('variant', $price->variant); if ($rawMaterial) { $unitAbbreviation = $rawMaterial->unit->abbreviation(); $price->setAttribute('unit_abbreviation', $unitAbbreviation); $item->setAttribute('unit_abbreviation', $unitAbbreviation); $item->setAttribute('raw_material_id', $rawMaterial->id); $item->setAttribute('raw_material_name', $rawMaterial->name); $item->setAttribute('raw_material_unit_label', $rawMaterial->unit->label()); } $price->unsetRelation('rawMaterial'); } $item->unsetRelation('rawMaterialPrice'); } private function applySorting(Builder $query, string $sort, string $direction): void { if (in_array($sort, ['created_at', 'total', 'discount', 'subtotal'], true)) { $query->orderBy($sort, $direction); return; } $query->latest(); } }