feat: enhance purchase management by adding photo and S3 key support in requests and services
This commit is contained in:
parent
57040f1875
commit
78f422b1cf
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Manage\Purchase;
|
namespace App\Http\Controllers\Admin\Manage\Purchase;
|
||||||
|
|
||||||
use App\Enums\Permission;
|
|
||||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
@ -50,11 +49,7 @@ public function store(PurchaseRequest $request): RedirectResponse
|
|||||||
{
|
{
|
||||||
$this->purchaseService->create($request->validated(), $request->user());
|
$this->purchaseService->create($request->validated(), $request->user());
|
||||||
|
|
||||||
if ($request->user()->can(Permission::OWNER_VERIFICATIONS_VERIFY->value)) {
|
$this->flashCreated('Belanja');
|
||||||
$this->flashCreated('Belanja');
|
|
||||||
} else {
|
|
||||||
$this->flashSuccess('Belanja berhasil diajukan dan menunggu verifikasi owner.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->route('admin.manage.purchases.index');
|
return redirect()->route('admin.manage.purchases.index');
|
||||||
}
|
}
|
||||||
@ -72,11 +67,7 @@ public function update(PurchaseRequest $request, Purchase $purchase): RedirectRe
|
|||||||
{
|
{
|
||||||
$this->purchaseService->update($purchase, $request->validated(), $request->user());
|
$this->purchaseService->update($purchase, $request->validated(), $request->user());
|
||||||
|
|
||||||
if ($request->user()->can(Permission::OWNER_VERIFICATIONS_VERIFY->value)) {
|
$this->flashUpdated('Belanja');
|
||||||
$this->flashUpdated('Belanja');
|
|
||||||
} else {
|
|
||||||
$this->flashSuccess('Perubahan belanja berhasil diajukan dan menunggu verifikasi owner.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->route('admin.manage.purchases.index');
|
return redirect()->route('admin.manage.purchases.index');
|
||||||
}
|
}
|
||||||
@ -85,11 +76,7 @@ public function destroy(Request $request, Purchase $purchase): RedirectResponse
|
|||||||
{
|
{
|
||||||
$this->purchaseService->delete($purchase, $request->user());
|
$this->purchaseService->delete($purchase, $request->user());
|
||||||
|
|
||||||
if ($request->user()->can(Permission::OWNER_VERIFICATIONS_VERIFY->value)) {
|
$this->flashDeleted('Belanja');
|
||||||
$this->flashDeleted('Belanja');
|
|
||||||
} else {
|
|
||||||
$this->flashSuccess('Penghapusan belanja berhasil diajukan dan menunggu verifikasi owner.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->route('admin.manage.purchases.index');
|
return redirect()->route('admin.manage.purchases.index');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,10 @@ public function rules(): array
|
|||||||
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
||||||
],
|
],
|
||||||
'quantity' => ['required', 'numeric', 'decimal:0,4', 'gt:0'],
|
'quantity' => ['required', 'numeric', 'decimal:0,4', 'gt:0'],
|
||||||
|
'photos' => ['nullable', 'array', 'max:5'],
|
||||||
|
'photos.*' => ['image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
||||||
|
's3_keys' => ['nullable', 'array', 'max:5'],
|
||||||
|
's3_keys.*' => ['nullable', 'string'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,10 +28,10 @@ public function rules(): array
|
|||||||
'variant' => ['required', 'string', 'max:200'],
|
'variant' => ['required', 'string', 'max:200'],
|
||||||
'price' => ['required', 'integer', 'gt:0'],
|
'price' => ['required', 'integer', 'gt:0'],
|
||||||
'quantity' => ['required', 'numeric', 'decimal:0,4', 'gt:0'],
|
'quantity' => ['required', 'numeric', 'decimal:0,4', 'gt:0'],
|
||||||
'photos' => ['required_without:s3_keys', 'array', 'min:1', 'max:5'],
|
'photos' => ['nullable', 'array', 'max:5'],
|
||||||
'photos.*' => ['image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
'photos.*' => ['image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
||||||
's3_keys' => ['required_without:photos', 'array', 'min:1', 'max:5'],
|
's3_keys' => ['nullable', 'array', 'max:5'],
|
||||||
's3_keys.*' => ['required', 'string'],
|
's3_keys.*' => ['nullable', 'string'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,6 @@
|
|||||||
namespace App\Services\Manage;
|
namespace App\Services\Manage;
|
||||||
|
|
||||||
use App\Enums\OwnerVerificationAction;
|
use App\Enums\OwnerVerificationAction;
|
||||||
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;
|
||||||
@ -214,6 +212,19 @@ public function syncDraftItem(array $validated, User $user): array
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$item->load([
|
$item->load([
|
||||||
'rawMaterialPrice.rawMaterial:id,name,unit',
|
'rawMaterialPrice.rawMaterial:id,name,unit',
|
||||||
'rawMaterialPrice.media',
|
'rawMaterialPrice.media',
|
||||||
@ -275,48 +286,18 @@ public function createRawMaterialAndDraft(array $validated, User $user): array
|
|||||||
{
|
{
|
||||||
$data = $this->runInTransaction(
|
$data = $this->runInTransaction(
|
||||||
function () use ($validated, $user): array {
|
function () use ($validated, $user): array {
|
||||||
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
$rawMaterial = RawMaterial::create([
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'unit' => $validated['unit'],
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
$rawMaterial = RawMaterial::withTrashed()
|
$price = RawMaterialPrice::create([
|
||||||
->where('name', $validated['name'])
|
'raw_material_id' => $rawMaterial->id,
|
||||||
->where('unit', $validated['unit'])
|
'variant' => $validated['variant'],
|
||||||
->first();
|
'price' => (int) $validated['price'],
|
||||||
|
'stock' => 0.0,
|
||||||
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'];
|
$quantity = (float) $validated['quantity'];
|
||||||
$unitPrice = (int) $price->price;
|
$unitPrice = (int) $price->price;
|
||||||
@ -389,11 +370,9 @@ 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);
|
|
||||||
|
|
||||||
$purchase = $this->runInTransaction(
|
$purchase = $this->runInTransaction(
|
||||||
function () use ($validated, $user, $isOwner): Purchase {
|
function () use ($validated, $user): Purchase {
|
||||||
$resolvedItems = $this->processRequestItems($validated['items'] ?? [], $isOwner);
|
$resolvedItems = $this->processRequestItems($validated['items'] ?? []);
|
||||||
|
|
||||||
$subtotal = array_sum(array_column($resolvedItems, 'subtotal'));
|
$subtotal = array_sum(array_column($resolvedItems, 'subtotal'));
|
||||||
$discount = (int) ($validated['discount'] ?? 0);
|
$discount = (int) ($validated['discount'] ?? 0);
|
||||||
@ -423,22 +402,8 @@ function () use ($validated, $user, $isOwner): Purchase {
|
|||||||
|
|
||||||
$purchase->load(['supplier', 'items.rawMaterialPrice.rawMaterial:id,name']);
|
$purchase->load(['supplier', 'items.rawMaterialPrice.rawMaterial:id,name']);
|
||||||
|
|
||||||
if ($isOwner) {
|
foreach ($purchase->items as $item) {
|
||||||
foreach ($purchase->items as $item) {
|
$this->incrementStock($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;
|
return $purchase;
|
||||||
@ -446,109 +411,55 @@ function () use ($validated, $user, $isOwner): Purchase {
|
|||||||
'Gagal membuat pembelian',
|
'Gagal membuat pembelian',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! $isOwner) {
|
$this->draftItemsQuery($user)->delete();
|
||||||
$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:*');
|
$this->cacheForgetByPattern('manage:purchases:*');
|
||||||
|
|
||||||
|
$this->notifyPurchase(
|
||||||
|
'Belanja Baru',
|
||||||
|
"{$user->profile?->full_name} membuat belanja dari supplier {$purchase->supplier->name} senilai {$purchase->total_formatted}.",
|
||||||
|
route('admin.manage.purchases.index', ['search_id' => $purchase->id]),
|
||||||
|
);
|
||||||
|
|
||||||
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']);
|
||||||
|
|
||||||
$this->runInTransaction(
|
$this->runInTransaction(
|
||||||
function () use ($purchase, $validated, $user, $isOwner): void {
|
function () use ($purchase, $validated): void {
|
||||||
if ($isOwner) {
|
$payload = $this->buildPayloadFromValidated($validated);
|
||||||
$payload = $this->buildPayloadFromValidated($validated, $isOwner);
|
$this->applyPayloadToPurchase($purchase, $payload);
|
||||||
$this->applyPayloadToPurchase($purchase, $payload);
|
|
||||||
|
|
||||||
if (($validated['photos'] ?? null) !== null || ($validated['remove_media_ids'] ?? []) !== []) {
|
if (($validated['photos'] ?? null) !== null || ($validated['remove_media_ids'] ?? []) !== []) {
|
||||||
$this->syncPhotos($purchase, $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, $isOwner),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (($validated['photos'] ?? null) !== null || ($validated['remove_media_ids'] ?? []) !== []) {
|
|
||||||
$this->syncRequestPhotos($verificationRequest, $validated);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Gagal memperbarui belanja',
|
'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:*');
|
$this->cacheForgetByPattern('manage:purchases:*');
|
||||||
|
|
||||||
|
$this->notifyPurchase(
|
||||||
|
'Belanja Diubah',
|
||||||
|
"{$user->profile?->full_name} mengubah belanja dari supplier {$purchase->supplier->name}.",
|
||||||
|
route('admin.manage.purchases.index', ['search_id' => $purchase->id]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
$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');
|
$purchase->load('supplier');
|
||||||
|
|
||||||
$this->notifyForPendingRequest(
|
$this->executeDelete($purchase);
|
||||||
$user,
|
$this->cacheForgetByPattern('manage:purchases:*');
|
||||||
'Hapus Belanja',
|
|
||||||
"Pengajuan hapus belanja dari supplier {$purchase->supplier->name} oleh {$user->profile?->full_name} menunggu verifikasi owner.",
|
$this->notifyPurchase(
|
||||||
route('admin.manage.purchases.index', ['search_id' => $purchase->id]),
|
'Belanja Dihapus',
|
||||||
(string) $purchase->id,
|
"{$user->profile?->full_name} menghapus belanja dari supplier {$purchase->supplier->name}.",
|
||||||
|
route('admin.manage.purchases.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,12 +504,6 @@ public function applyCreate(OwnerVerificationRequest $verificationRequest): void
|
|||||||
'purchase' => 'Belanja tidak ditemukan.',
|
'purchase' => 'Belanja tidak ditemukan.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$purchase->load('items');
|
|
||||||
|
|
||||||
foreach ($purchase->items as $item) {
|
|
||||||
$this->incrementStock($item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyUpdate(OwnerVerificationRequest $verificationRequest): void
|
public function applyUpdate(OwnerVerificationRequest $verificationRequest): void
|
||||||
@ -704,26 +609,23 @@ private function decrementStock(PurchaseItem $item): void
|
|||||||
->decrement('stock', $item->quantity);
|
->decrement('stock', $item->quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function notifyForPendingRequest(User $user, string $typeLabel, string $body, string $submitterUrl, ?string $search = null): void
|
private function notifyPurchase(string $typeLabel, string $body, string $url, ?string $userId = null): void
|
||||||
{
|
{
|
||||||
$ownerUrl = route('admin.manage.purchases.index');
|
|
||||||
if ($search !== null) {
|
|
||||||
$ownerUrl = route('admin.manage.purchases.index', ['search_id' => $search]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
"📦 {$typeLabel} Menunggu Persetujuan Owner",
|
"📦 {$typeLabel}",
|
||||||
$body,
|
$body,
|
||||||
['owner', 'developer'],
|
['owner', 'developer'],
|
||||||
$ownerUrl,
|
$url,
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->pushNotificationService->sendToUser(
|
if ($userId !== null) {
|
||||||
'📤 Pengajuan Terkirim',
|
$this->pushNotificationService->sendToUser(
|
||||||
$body,
|
'📦 Belanja',
|
||||||
$user->id,
|
$body,
|
||||||
$submitterUrl,
|
$userId,
|
||||||
);
|
$url,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function rejectCreate(OwnerVerificationRequest $verificationRequest): void
|
private function rejectCreate(OwnerVerificationRequest $verificationRequest): void
|
||||||
@ -831,9 +733,9 @@ private function snapshotPurchase(Purchase $purchase): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildPayloadFromValidated(array $validated, bool $isOwner): array
|
private function buildPayloadFromValidated(array $validated): array
|
||||||
{
|
{
|
||||||
$lineItems = $this->enrichLineItems($this->processRequestItems($validated['items'], $isOwner));
|
$lineItems = $this->enrichLineItems($this->processRequestItems($validated['items']));
|
||||||
$subtotal = array_sum(array_column($lineItems, 'subtotal'));
|
$subtotal = array_sum(array_column($lineItems, 'subtotal'));
|
||||||
$discount = (int) ($validated['discount'] ?? 0);
|
$discount = (int) ($validated['discount'] ?? 0);
|
||||||
$shippingCost = (int) ($validated['shipping_cost'] ?? 0);
|
$shippingCost = (int) ($validated['shipping_cost'] ?? 0);
|
||||||
@ -853,7 +755,7 @@ private function buildPayloadFromValidated(array $validated, bool $isOwner): arr
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function processRequestItems(array $items, bool $isOwner): array
|
private function processRequestItems(array $items): array
|
||||||
{
|
{
|
||||||
$resolvedItems = [];
|
$resolvedItems = [];
|
||||||
|
|
||||||
@ -868,14 +770,14 @@ private function processRequestItems(array $items, bool $isOwner): array
|
|||||||
if ($rawMaterial->trashed()) {
|
if ($rawMaterial->trashed()) {
|
||||||
$rawMaterial->restore();
|
$rawMaterial->restore();
|
||||||
}
|
}
|
||||||
if ($isOwner && ! $rawMaterial->is_active) {
|
if (! $rawMaterial->is_active) {
|
||||||
$rawMaterial->update(['is_active' => true]);
|
$rawMaterial->update(['is_active' => true]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$rawMaterial = RawMaterial::create([
|
$rawMaterial = RawMaterial::create([
|
||||||
'name' => $itemData['name'],
|
'name' => $itemData['name'],
|
||||||
'unit' => $itemData['unit'],
|
'unit' => $itemData['unit'],
|
||||||
'is_active' => $isOwner,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -160,6 +160,8 @@ function applyPriceToAllVariants(sourceClientId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addPrice() {
|
function addPrice() {
|
||||||
|
prices.value.forEach((price) => savePriceToDb(price));
|
||||||
|
|
||||||
const newPrice: RawMaterialPriceFormItem = {
|
const newPrice: RawMaterialPriceFormItem = {
|
||||||
client_id: createClientId(),
|
client_id: createClientId(),
|
||||||
variant: '',
|
variant: '',
|
||||||
@ -315,6 +317,12 @@ async function savePriceToDb(price: typeof prices.value[0]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (price.media.newFileS3Keys.length > 0) {
|
||||||
|
price.media.newFileS3Keys.forEach((key) => {
|
||||||
|
formData.append('s3_keys[]', key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const response = await apiFetch<{ item: any }>(
|
const response = await apiFetch<{ item: any }>(
|
||||||
'/admin/manage/purchases/draft-items/new-raw-material',
|
'/admin/manage/purchases/draft-items/new-raw-material',
|
||||||
{
|
{
|
||||||
@ -324,19 +332,54 @@ async function savePriceToDb(price: typeof prices.value[0]) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
price.id = response.item.raw_material_price_id;
|
price.id = response.item.raw_material_price_id;
|
||||||
toast.success(`Varian "${price.variant}" berhasil disimpan.`);
|
|
||||||
|
if (response.item.images?.length > 0) {
|
||||||
|
price.media.existing = response.item.images;
|
||||||
|
price.media.newFiles = [];
|
||||||
|
price.media.newFileS3Keys = [];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await apiFetch(
|
const hasNewPhotos = price.media.newFiles.length > 0 || price.media.newFileS3Keys.length > 0;
|
||||||
'/admin/manage/purchases/draft-items',
|
|
||||||
{
|
if (hasNewPhotos) {
|
||||||
method: 'POST',
|
const formData = new FormData();
|
||||||
headers: { 'Content-Type': 'application/json' },
|
formData.append('raw_material_price_id', String(price.id));
|
||||||
body: JSON.stringify({
|
formData.append('quantity', String(Number(price.stock) || 0));
|
||||||
raw_material_price_id: price.id,
|
|
||||||
quantity: Number(price.stock) || 0,
|
price.media.newFiles.forEach((file) => {
|
||||||
}),
|
formData.append('photos[]', file);
|
||||||
|
});
|
||||||
|
|
||||||
|
price.media.newFileS3Keys.forEach((key) => {
|
||||||
|
formData.append('s3_keys[]', key);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await apiFetch<{ item: any }>(
|
||||||
|
'/admin/manage/purchases/draft-items',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.item.images?.length > 0) {
|
||||||
|
price.media.existing = response.item.images;
|
||||||
|
price.media.newFiles = [];
|
||||||
|
price.media.newFileS3Keys = [];
|
||||||
}
|
}
|
||||||
);
|
} else {
|
||||||
|
await apiFetch(
|
||||||
|
'/admin/manage/purchases/draft-items',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
raw_material_price_id: price.id,
|
||||||
|
quantity: Number(price.stock) || 0,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -349,6 +392,23 @@ const debouncedSave = debounce(async (price: typeof prices.value[0]) => {
|
|||||||
await savePriceToDb(price);
|
await savePriceToDb(price);
|
||||||
}, 600);
|
}, 600);
|
||||||
|
|
||||||
|
const prevUploadCount = ref(0);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => prices.value.reduce((sum, p) => sum + (p.media.pendingUploads || 0), 0),
|
||||||
|
(current, old) => {
|
||||||
|
prevUploadCount.value = old ?? 0;
|
||||||
|
|
||||||
|
if (old > 0 && current === 0) {
|
||||||
|
prices.value.forEach((price) => {
|
||||||
|
if (price.media.newFiles.length > 0 || price.media.newFileS3Keys.length > 0) {
|
||||||
|
debouncedSave(price);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Variant master form event interceptors
|
// Variant master form event interceptors
|
||||||
function handleUpdateVariant(clientId: string, value: string) {
|
function handleUpdateVariant(clientId: string, value: string) {
|
||||||
setPriceField(clientId, 'variant', value);
|
setPriceField(clientId, 'variant', value);
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Enums\OwnerVerificationAction;
|
|
||||||
use App\Enums\OwnerVerificationStatus;
|
|
||||||
use App\Enums\Permission as PermissionEnum;
|
use App\Enums\Permission as PermissionEnum;
|
||||||
use App\Models\OwnerVerificationRequest;
|
|
||||||
use App\Models\Purchase;
|
use App\Models\Purchase;
|
||||||
use App\Models\PurchaseItem;
|
use App\Models\PurchaseItem;
|
||||||
use App\Models\RawMaterial;
|
use App\Models\RawMaterial;
|
||||||
@ -38,33 +35,6 @@ function createPurchaseUserWithPermission(PermissionEnum ...$permissions): User
|
|||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPurchaseVerifierUser(): User
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$user->givePermissionTo([
|
|
||||||
PermissionEnum::DASHBOARD_VIEW->value,
|
|
||||||
PermissionEnum::OWNER_VERIFICATIONS_VIEW->value,
|
|
||||||
PermissionEnum::OWNER_VERIFICATIONS_VERIFY->value,
|
|
||||||
PermissionEnum::OWNER_VERIFICATIONS_REJECT->value,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$user->forgetCachedPermissions();
|
|
||||||
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
function approveLatestPurchaseVerificationRequest(User $verifier): OwnerVerificationRequest
|
|
||||||
{
|
|
||||||
$verificationRequest = OwnerVerificationRequest::query()->pending()->latest()->firstOrFail();
|
|
||||||
|
|
||||||
test()->actingAs($verifier)
|
|
||||||
->post(route('admin.manage.owner_verifications.approve_request', $verificationRequest))
|
|
||||||
->assertRedirect();
|
|
||||||
|
|
||||||
return $verificationRequest->fresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPurchaseItemsPayload(RawMaterialPrice $price, float $quantity = 2): array
|
function getPurchaseItemsPayload(RawMaterialPrice $price, float $quantity = 2): array
|
||||||
{
|
{
|
||||||
$unit = $price->rawMaterial->unit;
|
$unit = $price->rawMaterial->unit;
|
||||||
@ -82,290 +52,6 @@ function getPurchaseItemsPayload(RawMaterialPrice $price, float $quantity = 2):
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Purchase Owner Verification', function () {
|
|
||||||
test('create purchase submits verification without changing stock', function () {
|
|
||||||
$user = createPurchaseUserWithPermission(
|
|
||||||
PermissionEnum::PURCHASES_VIEW,
|
|
||||||
PermissionEnum::PURCHASES_CREATE,
|
|
||||||
);
|
|
||||||
|
|
||||||
$supplier = Supplier::factory()->create();
|
|
||||||
$price = RawMaterialPrice::factory()->create(['stock' => 10]);
|
|
||||||
$initialStock = (float) $price->stock;
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->postJson(route('admin.manage.purchases.draft_items.store'), [
|
|
||||||
'raw_material_price_id' => $price->id,
|
|
||||||
'quantity' => 2,
|
|
||||||
])
|
|
||||||
->assertOk();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->post(route('admin.manage.purchases.store'), [
|
|
||||||
'supplier_id' => $supplier->id,
|
|
||||||
'discount' => 0,
|
|
||||||
'shipping_cost' => 0,
|
|
||||||
'notes' => 'Belanja test',
|
|
||||||
'items' => getPurchaseItemsPayload($price, 2),
|
|
||||||
])
|
|
||||||
->assertRedirect(route('admin.manage.purchases.index'));
|
|
||||||
|
|
||||||
$purchase = Purchase::query()->latest()->first();
|
|
||||||
|
|
||||||
expect($purchase)->not->toBeNull();
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('owner_verification_requests', [
|
|
||||||
'subject_type' => Purchase::class,
|
|
||||||
'subject_id' => $purchase->id,
|
|
||||||
'action' => OwnerVerificationAction::CREATE->value,
|
|
||||||
'status' => OwnerVerificationStatus::PENDING->value,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect((float) $price->fresh()->stock)->toBe($initialStock);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('approving create purchase increments stock', function () {
|
|
||||||
$user = createPurchaseUserWithPermission(
|
|
||||||
PermissionEnum::PURCHASES_VIEW,
|
|
||||||
PermissionEnum::PURCHASES_CREATE,
|
|
||||||
);
|
|
||||||
$verifier = createPurchaseVerifierUser();
|
|
||||||
|
|
||||||
$supplier = Supplier::factory()->create();
|
|
||||||
$price = RawMaterialPrice::factory()->create(['stock' => 10]);
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->postJson(route('admin.manage.purchases.draft_items.store'), [
|
|
||||||
'raw_material_price_id' => $price->id,
|
|
||||||
'quantity' => 2,
|
|
||||||
])
|
|
||||||
->assertOk();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->post(route('admin.manage.purchases.store'), [
|
|
||||||
'supplier_id' => $supplier->id,
|
|
||||||
'discount' => 0,
|
|
||||||
'shipping_cost' => 0,
|
|
||||||
'items' => getPurchaseItemsPayload($price, 2),
|
|
||||||
])
|
|
||||||
->assertRedirect(route('admin.manage.purchases.index'));
|
|
||||||
|
|
||||||
approveLatestPurchaseVerificationRequest($verifier);
|
|
||||||
|
|
||||||
expect((float) $price->fresh()->stock)->toBe(12.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('update purchase submits verification request only', function () {
|
|
||||||
$user = createPurchaseUserWithPermission(
|
|
||||||
PermissionEnum::PURCHASES_VIEW,
|
|
||||||
PermissionEnum::PURCHASES_CREATE,
|
|
||||||
PermissionEnum::PURCHASES_UPDATE,
|
|
||||||
);
|
|
||||||
$verifier = createPurchaseVerifierUser();
|
|
||||||
|
|
||||||
$supplier = Supplier::factory()->create();
|
|
||||||
$price = RawMaterialPrice::factory()->create(['stock' => 10, 'price' => 50_000]);
|
|
||||||
$otherPrice = RawMaterialPrice::factory()->create(['stock' => 5, 'price' => 30_000]);
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->postJson(route('admin.manage.purchases.draft_items.store'), [
|
|
||||||
'raw_material_price_id' => $price->id,
|
|
||||||
'quantity' => 2,
|
|
||||||
])
|
|
||||||
->assertOk();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->post(route('admin.manage.purchases.store'), [
|
|
||||||
'supplier_id' => $supplier->id,
|
|
||||||
'discount' => 0,
|
|
||||||
'shipping_cost' => 0,
|
|
||||||
'items' => getPurchaseItemsPayload($price, 2),
|
|
||||||
])
|
|
||||||
->assertRedirect(route('admin.manage.purchases.index'));
|
|
||||||
|
|
||||||
approveLatestPurchaseVerificationRequest($verifier);
|
|
||||||
|
|
||||||
$purchase = Purchase::query()->latest()->firstOrFail();
|
|
||||||
$originalTotal = $purchase->total;
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->put(route('admin.manage.purchases.update', $purchase), [
|
|
||||||
'supplier_id' => $supplier->id,
|
|
||||||
'discount' => 1000,
|
|
||||||
'shipping_cost' => 0,
|
|
||||||
'notes' => 'Ubah belanja',
|
|
||||||
'items' => getPurchaseItemsPayload($otherPrice, 1),
|
|
||||||
])
|
|
||||||
->assertRedirect(route('admin.manage.purchases.index'));
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('owner_verification_requests', [
|
|
||||||
'subject_type' => Purchase::class,
|
|
||||||
'subject_id' => $purchase->id,
|
|
||||||
'action' => OwnerVerificationAction::UPDATE->value,
|
|
||||||
'status' => OwnerVerificationStatus::PENDING->value,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect($purchase->fresh()->total)->toBe($originalTotal);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('delete purchase submits verification request only', function () {
|
|
||||||
$user = createPurchaseUserWithPermission(
|
|
||||||
PermissionEnum::PURCHASES_VIEW,
|
|
||||||
PermissionEnum::PURCHASES_CREATE,
|
|
||||||
PermissionEnum::PURCHASES_DELETE,
|
|
||||||
);
|
|
||||||
$verifier = createPurchaseVerifierUser();
|
|
||||||
|
|
||||||
$supplier = Supplier::factory()->create();
|
|
||||||
$price = RawMaterialPrice::factory()->create(['stock' => 10]);
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->postJson(route('admin.manage.purchases.draft_items.store'), [
|
|
||||||
'raw_material_price_id' => $price->id,
|
|
||||||
'quantity' => 2,
|
|
||||||
])
|
|
||||||
->assertOk();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->post(route('admin.manage.purchases.store'), [
|
|
||||||
'supplier_id' => $supplier->id,
|
|
||||||
'discount' => 0,
|
|
||||||
'shipping_cost' => 0,
|
|
||||||
'items' => getPurchaseItemsPayload($price, 2),
|
|
||||||
])
|
|
||||||
->assertRedirect(route('admin.manage.purchases.index'));
|
|
||||||
|
|
||||||
approveLatestPurchaseVerificationRequest($verifier);
|
|
||||||
|
|
||||||
$purchase = Purchase::query()->latest()->firstOrFail();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->delete(route('admin.manage.purchases.destroy', $purchase))
|
|
||||||
->assertRedirect(route('admin.manage.purchases.index'));
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('owner_verification_requests', [
|
|
||||||
'subject_type' => Purchase::class,
|
|
||||||
'subject_id' => $purchase->id,
|
|
||||||
'action' => OwnerVerificationAction::DELETE->value,
|
|
||||||
'status' => OwnerVerificationStatus::PENDING->value,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(Purchase::query()->whereKey($purchase->id)->exists())->toBeTrue();
|
|
||||||
expect((float) $price->fresh()->stock)->toBe(12.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rejecting create purchase deletes purchase without changing stock', function () {
|
|
||||||
$user = createPurchaseUserWithPermission(
|
|
||||||
PermissionEnum::PURCHASES_VIEW,
|
|
||||||
PermissionEnum::PURCHASES_CREATE,
|
|
||||||
);
|
|
||||||
$verifier = createPurchaseVerifierUser();
|
|
||||||
|
|
||||||
$supplier = Supplier::factory()->create();
|
|
||||||
$price = RawMaterialPrice::factory()->create(['stock' => 10]);
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->postJson(route('admin.manage.purchases.draft_items.store'), [
|
|
||||||
'raw_material_price_id' => $price->id,
|
|
||||||
'quantity' => 2,
|
|
||||||
])
|
|
||||||
->assertOk();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->post(route('admin.manage.purchases.store'), [
|
|
||||||
'supplier_id' => $supplier->id,
|
|
||||||
'discount' => 0,
|
|
||||||
'shipping_cost' => 0,
|
|
||||||
'items' => getPurchaseItemsPayload($price, 2),
|
|
||||||
])
|
|
||||||
->assertRedirect(route('admin.manage.purchases.index'));
|
|
||||||
|
|
||||||
$purchase = Purchase::query()->latest()->firstOrFail();
|
|
||||||
$verificationRequest = OwnerVerificationRequest::query()->pending()->latest()->firstOrFail();
|
|
||||||
|
|
||||||
$this->actingAs($verifier)
|
|
||||||
->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest), [
|
|
||||||
'reason' => 'Tidak sesuai',
|
|
||||||
])
|
|
||||||
->assertRedirect();
|
|
||||||
|
|
||||||
expect(Purchase::query()->whereKey($purchase->id)->exists())->toBeFalse();
|
|
||||||
expect(PurchaseItem::query()->where('purchase_id', $purchase->id)->exists())->toBeFalse();
|
|
||||||
expect((float) $price->fresh()->stock)->toBe(10.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('pending purchase blocks edit route', function () {
|
|
||||||
$user = createPurchaseUserWithPermission(
|
|
||||||
PermissionEnum::PURCHASES_VIEW,
|
|
||||||
PermissionEnum::PURCHASES_CREATE,
|
|
||||||
PermissionEnum::PURCHASES_UPDATE,
|
|
||||||
);
|
|
||||||
|
|
||||||
$supplier = Supplier::factory()->create();
|
|
||||||
$price = RawMaterialPrice::factory()->create(['stock' => 10]);
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->postJson(route('admin.manage.purchases.draft_items.store'), [
|
|
||||||
'raw_material_price_id' => $price->id,
|
|
||||||
'quantity' => 2,
|
|
||||||
])
|
|
||||||
->assertOk();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->post(route('admin.manage.purchases.store'), [
|
|
||||||
'supplier_id' => $supplier->id,
|
|
||||||
'discount' => 0,
|
|
||||||
'shipping_cost' => 0,
|
|
||||||
'items' => getPurchaseItemsPayload($price, 2),
|
|
||||||
])
|
|
||||||
->assertRedirect(route('admin.manage.purchases.index'));
|
|
||||||
|
|
||||||
$purchase = Purchase::query()->latest()->firstOrFail();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->get(route('admin.manage.purchases.edit', $purchase))
|
|
||||||
->assertRedirect(route('admin.manage.purchases.index'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('purchase index exposes pending verification state', function () {
|
|
||||||
$user = createPurchaseUserWithPermission(
|
|
||||||
PermissionEnum::PURCHASES_VIEW,
|
|
||||||
PermissionEnum::PURCHASES_CREATE,
|
|
||||||
);
|
|
||||||
|
|
||||||
$supplier = Supplier::factory()->create();
|
|
||||||
$price = RawMaterialPrice::factory()->create(['stock' => 10]);
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->postJson(route('admin.manage.purchases.draft_items.store'), [
|
|
||||||
'raw_material_price_id' => $price->id,
|
|
||||||
'quantity' => 2,
|
|
||||||
])
|
|
||||||
->assertOk();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->post(route('admin.manage.purchases.store'), [
|
|
||||||
'supplier_id' => $supplier->id,
|
|
||||||
'discount' => 0,
|
|
||||||
'shipping_cost' => 0,
|
|
||||||
'items' => getPurchaseItemsPayload($price, 2),
|
|
||||||
])
|
|
||||||
->assertRedirect(route('admin.manage.purchases.index'));
|
|
||||||
|
|
||||||
$purchase = Purchase::query()->latest()->firstOrFail();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
|
||||||
->get(route('admin.manage.purchases.index'));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
$response->assertInertia(fn ($page) => $page
|
|
||||||
->where('purchases.data.0.id', $purchase->id)
|
|
||||||
->where('purchases.data.0.has_pending_request', true)
|
|
||||||
->where('purchases.data.0.pending_request_action', OwnerVerificationAction::CREATE->value)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Index ────────────────────────────────────────────────
|
// ─── Index ────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('Purchase Index', function () {
|
describe('Purchase Index', function () {
|
||||||
@ -510,7 +196,6 @@ function getPurchaseItemsPayload(RawMaterialPrice $price, float $quantity = 2):
|
|||||||
describe('Purchase Edit', function () {
|
describe('Purchase Edit', function () {
|
||||||
test('authenticated user with permission can view edit form', function () {
|
test('authenticated user with permission can view edit form', function () {
|
||||||
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, PermissionEnum::PURCHASES_UPDATE);
|
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, PermissionEnum::PURCHASES_UPDATE);
|
||||||
$verifier = createPurchaseVerifierUser();
|
|
||||||
|
|
||||||
$supplier = Supplier::factory()->create();
|
$supplier = Supplier::factory()->create();
|
||||||
$price = RawMaterialPrice::factory()->create(['stock' => 10]);
|
$price = RawMaterialPrice::factory()->create(['stock' => 10]);
|
||||||
@ -530,8 +215,6 @@ function getPurchaseItemsPayload(RawMaterialPrice $price, float $quantity = 2):
|
|||||||
'items' => getPurchaseItemsPayload($price, 2),
|
'items' => getPurchaseItemsPayload($price, 2),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
approveLatestPurchaseVerificationRequest($verifier);
|
|
||||||
|
|
||||||
$purchase = Purchase::query()->latest()->firstOrFail();
|
$purchase = Purchase::query()->latest()->firstOrFail();
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
@ -561,7 +244,6 @@ function getPurchaseItemsPayload(RawMaterialPrice $price, float $quantity = 2):
|
|||||||
describe('Purchase Update Validation', function () {
|
describe('Purchase Update Validation', function () {
|
||||||
test('items is required on update', function () {
|
test('items is required on update', function () {
|
||||||
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, PermissionEnum::PURCHASES_UPDATE);
|
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, PermissionEnum::PURCHASES_UPDATE);
|
||||||
$verifier = createPurchaseVerifierUser();
|
|
||||||
|
|
||||||
$supplier = Supplier::factory()->create();
|
$supplier = Supplier::factory()->create();
|
||||||
$price = RawMaterialPrice::factory()->create(['stock' => 10]);
|
$price = RawMaterialPrice::factory()->create(['stock' => 10]);
|
||||||
@ -581,8 +263,6 @@ function getPurchaseItemsPayload(RawMaterialPrice $price, float $quantity = 2):
|
|||||||
'items' => getPurchaseItemsPayload($price, 2),
|
'items' => getPurchaseItemsPayload($price, 2),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
approveLatestPurchaseVerificationRequest($verifier);
|
|
||||||
|
|
||||||
$purchase = Purchase::query()->latest()->firstOrFail();
|
$purchase = Purchase::query()->latest()->firstOrFail();
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
@ -842,7 +522,7 @@ function getPurchaseItemsPayload(RawMaterialPrice $price, float $quantity = 2):
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('store new raw material fails if photo/s3_keys is missing', function () {
|
test('store new raw material succeeds without photo/s3_keys', function () {
|
||||||
$user = createPurchaseUserWithPermission(
|
$user = createPurchaseUserWithPermission(
|
||||||
PermissionEnum::PURCHASES_VIEW,
|
PermissionEnum::PURCHASES_VIEW,
|
||||||
PermissionEnum::PURCHASES_CREATE,
|
PermissionEnum::PURCHASES_CREATE,
|
||||||
@ -856,11 +536,34 @@ function getPurchaseItemsPayload(RawMaterialPrice $price, float $quantity = 2):
|
|||||||
'price' => 25000,
|
'price' => 25000,
|
||||||
'quantity' => 10,
|
'quantity' => 10,
|
||||||
])
|
])
|
||||||
->assertStatus(422)
|
->assertOk()
|
||||||
->assertJsonValidationErrors(['photos', 's3_keys']);
|
->assertJsonPath('item.raw_material_name', 'Kain Toyobo Baru')
|
||||||
|
->assertJsonPath('item.variant', 'Standard')
|
||||||
|
->assertJsonPath('item.unit_abbreviation', 'yard');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('raw_materials', [
|
||||||
|
'name' => 'Kain Toyobo Baru',
|
||||||
|
'unit' => 'yard',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('raw_material_prices', [
|
||||||
|
'variant' => 'Standard',
|
||||||
|
'price' => 25000,
|
||||||
|
'stock' => 0.0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$rawMaterialPrice = RawMaterialPrice::where('variant', 'Standard')
|
||||||
|
->whereHas('rawMaterial', fn ($q) => $q->where('name', 'Kain Toyobo Baru'))
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('purchase_items', [
|
||||||
|
'raw_material_price_id' => $rawMaterialPrice->id,
|
||||||
|
'quantity' => 10,
|
||||||
|
'purchase_id' => null,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('store new raw material reuses existing raw material if name and unit match', function () {
|
test('store new raw material creates new records even if same name and unit exist', function () {
|
||||||
$user = createPurchaseUserWithPermission(
|
$user = createPurchaseUserWithPermission(
|
||||||
PermissionEnum::PURCHASES_VIEW,
|
PermissionEnum::PURCHASES_VIEW,
|
||||||
PermissionEnum::PURCHASES_CREATE,
|
PermissionEnum::PURCHASES_CREATE,
|
||||||
@ -897,34 +600,32 @@ function getPurchaseItemsPayload(RawMaterialPrice $price, float $quantity = 2):
|
|||||||
])
|
])
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
// Check that raw_materials table only has 1 record for this name
|
// Duplicates are fine — each creates its own raw_material
|
||||||
$rawMaterialsCount = RawMaterial::where('name', 'Kain Toyobo Unik')->count();
|
$rawMaterialsCount = RawMaterial::where('name', 'Kain Toyobo Unik')->count();
|
||||||
expect($rawMaterialsCount)->toBe(1);
|
expect($rawMaterialsCount)->toBe(2);
|
||||||
|
|
||||||
// Check that raw_material_prices has both variants for the same raw_material_id
|
|
||||||
$rawMaterial = RawMaterial::where('name', 'Kain Toyobo Unik')->first();
|
|
||||||
$this->assertDatabaseHas('raw_material_prices', [
|
$this->assertDatabaseHas('raw_material_prices', [
|
||||||
'raw_material_id' => $rawMaterial->id,
|
|
||||||
'variant' => 'Standard',
|
'variant' => 'Standard',
|
||||||
'price' => 25000,
|
'price' => 25000,
|
||||||
]);
|
]);
|
||||||
$this->assertDatabaseHas('raw_material_prices', [
|
$this->assertDatabaseHas('raw_material_prices', [
|
||||||
'raw_material_id' => $rawMaterial->id,
|
|
||||||
'variant' => 'Premium',
|
'variant' => 'Premium',
|
||||||
'price' => 30000,
|
'price' => 30000,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('store new raw material increments draft item quantity if same variant is added again', function () {
|
test('store new raw material creates separate draft items for same variant name', function () {
|
||||||
$user = createPurchaseUserWithPermission(
|
$user = createPurchaseUserWithPermission(
|
||||||
PermissionEnum::PURCHASES_VIEW,
|
PermissionEnum::PURCHASES_VIEW,
|
||||||
PermissionEnum::PURCHASES_CREATE,
|
PermissionEnum::PURCHASES_CREATE,
|
||||||
);
|
);
|
||||||
|
|
||||||
$disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public');
|
$disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public');
|
||||||
$key = 'raw-materials/'.Str::uuid().'.jpg';
|
$key1 = 'raw-materials/'.Str::uuid().'.jpg';
|
||||||
|
$key2 = 'raw-materials/'.Str::uuid().'.jpg';
|
||||||
$imageContent = UploadedFile::fake()->image('Standard.jpg', 100, 100)->get();
|
$imageContent = UploadedFile::fake()->image('Standard.jpg', 100, 100)->get();
|
||||||
Storage::disk($disk)->put($key, $imageContent);
|
Storage::disk($disk)->put($key1, $imageContent);
|
||||||
|
Storage::disk($disk)->put($key2, $imageContent);
|
||||||
|
|
||||||
// Add standard variant first time (qty = 10)
|
// Add standard variant first time (qty = 10)
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
@ -934,12 +635,12 @@ function getPurchaseItemsPayload(RawMaterialPrice $price, float $quantity = 2):
|
|||||||
'variant' => 'Standard',
|
'variant' => 'Standard',
|
||||||
'price' => 25000,
|
'price' => 25000,
|
||||||
'quantity' => 10,
|
'quantity' => 10,
|
||||||
's3_keys' => [$key],
|
's3_keys' => [$key1],
|
||||||
])
|
])
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('item.quantity', '10');
|
->assertJsonPath('item.quantity', '10');
|
||||||
|
|
||||||
// Add same variant second time (qty = 5)
|
// Add same variant second time — creates separate draft item (qty = 5)
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->postJson(route('admin.manage.purchases.draft_items.store_new_raw_material'), [
|
->postJson(route('admin.manage.purchases.draft_items.store_new_raw_material'), [
|
||||||
'name' => 'Kain Toyobo Sama',
|
'name' => 'Kain Toyobo Sama',
|
||||||
@ -947,9 +648,9 @@ function getPurchaseItemsPayload(RawMaterialPrice $price, float $quantity = 2):
|
|||||||
'variant' => 'Standard',
|
'variant' => 'Standard',
|
||||||
'price' => 25000,
|
'price' => 25000,
|
||||||
'quantity' => 5,
|
'quantity' => 5,
|
||||||
's3_keys' => [$key],
|
's3_keys' => [$key2],
|
||||||
])
|
])
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('item.quantity', '15'); // 10 + 5
|
->assertJsonPath('item.quantity', '5');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user