296 lines
9.5 KiB
PHP
296 lines
9.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Manage;
|
|
|
|
use App\Models\Purchase;
|
|
use App\Models\PurchaseItem;
|
|
use App\Models\RawMaterial;
|
|
use App\Models\RawMaterialPrice;
|
|
use App\Models\Supplier;
|
|
use App\Models\User;
|
|
use App\Services\Media\MediaService;
|
|
use App\Support\Media\MediaPresenter;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class PurchaseService
|
|
{
|
|
private const MAX_PHOTOS_FILES = 1;
|
|
|
|
public function __construct(
|
|
private readonly MediaService $mediaService,
|
|
) {}
|
|
|
|
/**
|
|
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
|
*/
|
|
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
|
|
{
|
|
$query = Purchase::query()
|
|
->with([
|
|
'supplier:id,name',
|
|
'createdBy.profile',
|
|
'items.rawMaterialPrice.rawMaterial:id,name,unit',
|
|
'media',
|
|
])
|
|
->when($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(10)
|
|
->withQueryString()
|
|
->through(function (Purchase $purchase) {
|
|
$purchase->setAttribute(
|
|
'photos',
|
|
MediaPresenter::first($purchase, 'photos'),
|
|
);
|
|
|
|
return $purchase;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @return list<array{value: int, label: string}>
|
|
*/
|
|
public function supplierOptions(): array
|
|
{
|
|
return Supplier::query()
|
|
->orderBy('name')
|
|
->get(['id', 'name'])
|
|
->map(fn (Supplier $supplier) => [
|
|
'value' => $supplier->id,
|
|
'label' => $supplier->name,
|
|
])
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, RawMaterial>
|
|
*/
|
|
public function catalogItems(?Purchase $purchase = null): Collection
|
|
{
|
|
$purchasePriceIds = $purchase
|
|
? $purchase->items()->pluck('raw_material_price_id')->all()
|
|
: [];
|
|
|
|
return RawMaterial::query()
|
|
->with([
|
|
'prices' => fn ($query) => $query
|
|
->orderBy('created_at')
|
|
->with(['rawMaterial:id,unit', '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): void {
|
|
$price->setAttribute(
|
|
'images',
|
|
MediaPresenter::collection($price, 'images'),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
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'),
|
|
);
|
|
}
|
|
});
|
|
|
|
return $purchase;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $validated
|
|
*/
|
|
public function create(array $validated, User $user): Purchase
|
|
{
|
|
return DB::transaction(function () use ($validated, $user): Purchase {
|
|
$lineItems = $this->buildLineItems($validated['items']);
|
|
$subtotal = array_sum(array_column($lineItems, 'subtotal'));
|
|
$discount = (int) ($validated['discount'] ?? 0);
|
|
$total = max($subtotal - $discount, 0);
|
|
|
|
$purchase = Purchase::create([
|
|
'supplier_id' => $validated['supplier_id'],
|
|
'created_by_id' => $user->id,
|
|
'subtotal' => $subtotal,
|
|
'discount' => $discount,
|
|
'total' => $total,
|
|
'notes' => $validated['notes'] ?? null,
|
|
]);
|
|
|
|
foreach ($lineItems as $itemData) {
|
|
$purchaseItem = $purchase->items()->create($itemData);
|
|
$this->incrementStock($purchaseItem);
|
|
}
|
|
|
|
$this->syncPhotos($purchase, $validated);
|
|
|
|
return $purchase;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $validated
|
|
*/
|
|
public function update(Purchase $purchase, array $validated): void
|
|
{
|
|
DB::transaction(function () use ($purchase, $validated): void {
|
|
$purchase->load('items');
|
|
|
|
foreach ($purchase->items as $item) {
|
|
$this->decrementStock($item);
|
|
}
|
|
|
|
$purchase->items()->delete();
|
|
|
|
$lineItems = $this->buildLineItems($validated['items']);
|
|
$subtotal = array_sum(array_column($lineItems, 'subtotal'));
|
|
$discount = (int) ($validated['discount'] ?? 0);
|
|
$total = max($subtotal - $discount, 0);
|
|
|
|
$purchase->supplier_id = $validated['supplier_id'];
|
|
$purchase->subtotal = $subtotal;
|
|
$purchase->discount = $discount;
|
|
$purchase->total = $total;
|
|
$purchase->notes = $validated['notes'] ?? null;
|
|
$purchase->save();
|
|
|
|
foreach ($lineItems as $itemData) {
|
|
$purchaseItem = $purchase->items()->create($itemData);
|
|
$this->incrementStock($purchaseItem);
|
|
}
|
|
|
|
$this->syncPhotos($purchase, $validated);
|
|
});
|
|
}
|
|
|
|
public function delete(Purchase $purchase): void
|
|
{
|
|
DB::transaction(function () use ($purchase): void {
|
|
$purchase->load('items');
|
|
|
|
foreach ($purchase->items as $item) {
|
|
$this->decrementStock($item);
|
|
}
|
|
|
|
$purchase->clearMediaCollection('photos');
|
|
$purchase->items()->delete();
|
|
$purchase->delete();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param list<array{raw_material_price_id: int, quantity: float|int|string}> $items
|
|
* @return list<array{raw_material_price_id: int, quantity: float, unit_price: int, subtotal: int}>
|
|
*/
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $validated
|
|
*/
|
|
private function syncPhotos(Purchase $purchase, array $validated): void
|
|
{
|
|
$this->mediaService->syncCollection(
|
|
$purchase,
|
|
'photos',
|
|
$validated['photos'] ?? null,
|
|
$validated['remove_media_ids'] ?? null,
|
|
self::MAX_PHOTOS_FILES,
|
|
required: false,
|
|
errorKey: 'photos',
|
|
);
|
|
}
|
|
|
|
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 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();
|
|
}
|
|
}
|