feat: implement transaction management in PurchaseService for create, update, and delete operations; refactor error handling for improved clarity; enhance tests for purchase index and creation functionalities to ensure proper permission checks and validation
This commit is contained in:
parent
9d0075d7b2
commit
6158a91060
@ -12,6 +12,7 @@
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\User;
|
||||
use App\Services\Concerns\RunsInTransaction;
|
||||
use App\Services\Media\MediaService;
|
||||
use App\Services\System\PushNotificationService;
|
||||
use App\Support\Media\MediaPresenter;
|
||||
@ -19,11 +20,12 @@
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class PurchaseService
|
||||
{
|
||||
use RunsInTransaction;
|
||||
|
||||
private const MAX_PHOTOS = 1;
|
||||
|
||||
public function __construct(
|
||||
@ -252,8 +254,8 @@ public function create(array $validated, User $user): Purchase
|
||||
{
|
||||
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
||||
|
||||
try {
|
||||
$purchase = DB::transaction(function () use ($validated, $user, $isOwner): Purchase {
|
||||
$purchase = $this->runInTransaction(
|
||||
function () use ($validated, $user, $isOwner): Purchase {
|
||||
|
||||
$draftItems = $this->draftItemsQuery($user)
|
||||
->lockForUpdate()
|
||||
@ -309,18 +311,9 @@ public function create(array $validated, User $user): Purchase
|
||||
}
|
||||
|
||||
return $purchase;
|
||||
});
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gagal membuat pembelian: '.$e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
||||
]);
|
||||
}
|
||||
},
|
||||
'Gagal membuat pembelian',
|
||||
);
|
||||
|
||||
if (! $isOwner) {
|
||||
$this->notifyForPendingRequest(
|
||||
@ -340,8 +333,8 @@ 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']);
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($purchase, $validated, $user, $isOwner): void {
|
||||
$this->runInTransaction(
|
||||
function () use ($purchase, $validated, $user, $isOwner): void {
|
||||
if ($isOwner) {
|
||||
$payload = $this->buildPayloadFromValidated($validated);
|
||||
$this->applyPayloadToPurchase($purchase, $payload);
|
||||
@ -366,18 +359,9 @@ public function update(Purchase $purchase, array $validated, User $user): void
|
||||
$this->syncRequestPhotos($verificationRequest, $validated);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gagal memperbarui pembelian: '.$e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
||||
]);
|
||||
}
|
||||
},
|
||||
'Gagal memperbarui pembelian',
|
||||
);
|
||||
|
||||
if (! $isOwner) {
|
||||
$purchase->load('supplier');
|
||||
@ -404,27 +388,22 @@ public function delete(Purchase $purchase, User $user): void
|
||||
|
||||
$purchase->load(['supplier', 'items.rawMaterialPrice.rawMaterial:id,name']);
|
||||
|
||||
try {
|
||||
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,
|
||||
],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gagal mengajukan penghapusan belanja: '.$e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
||||
]);
|
||||
}
|
||||
$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');
|
||||
|
||||
@ -615,26 +594,32 @@ private function rejectCreate(OwnerVerificationRequest $verificationRequest): vo
|
||||
return;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($purchase): void {
|
||||
$purchase->clearMediaCollection('photos');
|
||||
$purchase->items()->delete();
|
||||
$purchase->delete();
|
||||
});
|
||||
$this->runInTransaction(
|
||||
function () use ($purchase): void {
|
||||
$purchase->clearMediaCollection('photos');
|
||||
$purchase->items()->delete();
|
||||
$purchase->delete();
|
||||
},
|
||||
'Gagal menolak belanja',
|
||||
);
|
||||
}
|
||||
|
||||
private function executeDelete(Purchase $purchase): void
|
||||
{
|
||||
DB::transaction(function () use ($purchase): void {
|
||||
$purchase->load('items');
|
||||
$this->runInTransaction(
|
||||
function () use ($purchase): void {
|
||||
$purchase->load('items');
|
||||
|
||||
foreach ($purchase->items as $item) {
|
||||
$this->decrementStock($item);
|
||||
}
|
||||
foreach ($purchase->items as $item) {
|
||||
$this->decrementStock($item);
|
||||
}
|
||||
|
||||
$purchase->clearMediaCollection('photos');
|
||||
$purchase->items()->delete();
|
||||
$purchase->delete();
|
||||
});
|
||||
$purchase->clearMediaCollection('photos');
|
||||
$purchase->items()->delete();
|
||||
$purchase->delete();
|
||||
},
|
||||
'Gagal menghapus belanja',
|
||||
);
|
||||
}
|
||||
|
||||
private function applyPayloadToPurchase(
|
||||
@ -642,38 +627,41 @@ private function applyPayloadToPurchase(
|
||||
array $payload,
|
||||
?OwnerVerificationRequest $verificationRequest = null,
|
||||
): void {
|
||||
DB::transaction(function () use ($purchase, $payload, $verificationRequest): void {
|
||||
$purchase->load('items');
|
||||
$this->runInTransaction(
|
||||
function () use ($purchase, $payload, $verificationRequest): void {
|
||||
$purchase->load('items');
|
||||
|
||||
foreach ($purchase->items as $item) {
|
||||
$this->decrementStock($item);
|
||||
}
|
||||
foreach ($purchase->items as $item) {
|
||||
$this->decrementStock($item);
|
||||
}
|
||||
|
||||
$purchase->items()->delete();
|
||||
$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'],
|
||||
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,
|
||||
]);
|
||||
$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);
|
||||
}
|
||||
});
|
||||
if ($verificationRequest !== null) {
|
||||
$this->applyRequestPhotos($verificationRequest, $purchase, $payload);
|
||||
}
|
||||
},
|
||||
'Gagal memperbarui belanja',
|
||||
);
|
||||
}
|
||||
|
||||
private function snapshotPurchase(Purchase $purchase): array
|
||||
|
||||
@ -339,3 +339,418 @@ function approveLatestPurchaseVerificationRequest(User $verifier): OwnerVerifica
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Index ────────────────────────────────────────────────
|
||||
|
||||
describe('Purchase Index', function () {
|
||||
test('authenticated user with permission can view purchase index', function () {
|
||||
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.purchases.index'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('guest is redirected to login', function () {
|
||||
$this->get(route('admin.manage.purchases.index'))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without permission is forbidden', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.purchases.index'))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('index can search purchases by supplier name', function () {
|
||||
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW);
|
||||
|
||||
$supplier = Supplier::factory()->create(['name' => 'Supplier Kain']);
|
||||
$purchase = Purchase::factory()->create(['supplier_id' => $supplier->id, 'created_by_id' => $user->id]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.purchases.index', ['search' => 'Kain']))
|
||||
->assertOk();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Create ───────────────────────────────────────────────
|
||||
|
||||
describe('Purchase Create', function () {
|
||||
test('authenticated user with permission can view create form', function () {
|
||||
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.purchases.create'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('guest is redirected to login', function () {
|
||||
$this->get(route('admin.manage.purchases.create'))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without create permission is forbidden', function () {
|
||||
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.purchases.create'))
|
||||
->assertForbidden();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Store Validation ─────────────────────────────────────
|
||||
|
||||
describe('Purchase Store Validation', function () {
|
||||
test('supplier_id is required', function () {
|
||||
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE);
|
||||
|
||||
$price = RawMaterialPrice::factory()->create();
|
||||
|
||||
$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' => '',
|
||||
'discount' => 0,
|
||||
'shipping_cost' => 0,
|
||||
])
|
||||
->assertSessionHasErrors('supplier_id');
|
||||
});
|
||||
|
||||
test('supplier must exist', function () {
|
||||
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE);
|
||||
|
||||
$price = RawMaterialPrice::factory()->create();
|
||||
|
||||
$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' => 99999,
|
||||
'discount' => 0,
|
||||
'shipping_cost' => 0,
|
||||
])
|
||||
->assertSessionHasErrors('supplier_id');
|
||||
});
|
||||
|
||||
test('store fails without draft items', function () {
|
||||
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE);
|
||||
$supplier = Supplier::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.purchases.store'), [
|
||||
'supplier_id' => $supplier->id,
|
||||
'discount' => 0,
|
||||
'shipping_cost' => 0,
|
||||
])
|
||||
->assertSessionHasErrors('items');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Edit ─────────────────────────────────────────────────
|
||||
|
||||
describe('Purchase Edit', function () {
|
||||
test('authenticated user with permission can view edit form', function () {
|
||||
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, PermissionEnum::PURCHASES_UPDATE);
|
||||
$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,
|
||||
]);
|
||||
|
||||
approveLatestPurchaseVerificationRequest($verifier);
|
||||
|
||||
$purchase = Purchase::query()->latest()->firstOrFail();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.purchases.edit', $purchase))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('guest is redirected to login', function () {
|
||||
$purchase = Purchase::factory()->create();
|
||||
|
||||
$this->get(route('admin.manage.purchases.edit', $purchase))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without update permission is forbidden', function () {
|
||||
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW);
|
||||
$purchase = Purchase::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.purchases.edit', $purchase))
|
||||
->assertForbidden();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Update Validation ────────────────────────────────────
|
||||
|
||||
describe('Purchase Update Validation', function () {
|
||||
test('items is required on update', function () {
|
||||
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, PermissionEnum::PURCHASES_UPDATE);
|
||||
$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,
|
||||
]);
|
||||
|
||||
approveLatestPurchaseVerificationRequest($verifier);
|
||||
|
||||
$purchase = Purchase::query()->latest()->firstOrFail();
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.manage.purchases.update', $purchase), [
|
||||
'supplier_id' => $supplier->id,
|
||||
'discount' => 0,
|
||||
'shipping_cost' => 0,
|
||||
'items' => [],
|
||||
])
|
||||
->assertSessionHasErrors('items');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Destroy ──────────────────────────────────────────────
|
||||
|
||||
describe('Purchase Destroy', function () {
|
||||
test('guest cannot delete a purchase', function () {
|
||||
$purchase = Purchase::factory()->create();
|
||||
|
||||
$this->delete(route('admin.manage.purchases.destroy', $purchase))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without delete permission is forbidden', function () {
|
||||
$user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW);
|
||||
$purchase = Purchase::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->delete(route('admin.manage.purchases.destroy', $purchase))
|
||||
->assertForbidden();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Owner Direct Actions ──────────────────────────────────
|
||||
|
||||
describe('Owner Direct Actions', function () {
|
||||
test('owner can create purchase directly and increments stock', function () {
|
||||
$owner = createPurchaseUserWithPermission(
|
||||
PermissionEnum::PURCHASES_VIEW,
|
||||
PermissionEnum::PURCHASES_CREATE,
|
||||
PermissionEnum::OWNER_VERIFICATIONS_VERIFY,
|
||||
);
|
||||
|
||||
$supplier = Supplier::factory()->create();
|
||||
$price = RawMaterialPrice::factory()->create(['stock' => 10]);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->postJson(route('admin.manage.purchases.draft_items.store'), [
|
||||
'raw_material_price_id' => $price->id,
|
||||
'quantity' => 3,
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('admin.manage.purchases.store'), [
|
||||
'supplier_id' => $supplier->id,
|
||||
'discount' => 0,
|
||||
'shipping_cost' => 0,
|
||||
])
|
||||
->assertRedirect(route('admin.manage.purchases.index'));
|
||||
|
||||
$purchase = Purchase::query()->latest()->first();
|
||||
|
||||
expect($purchase)->not->toBeNull();
|
||||
|
||||
$this->assertDatabaseMissing('owner_verification_requests', [
|
||||
'subject_id' => $purchase->id,
|
||||
'subject_type' => Purchase::class,
|
||||
]);
|
||||
|
||||
expect((float) $price->fresh()->stock)->toBe(13.0);
|
||||
});
|
||||
|
||||
test('owner can delete purchase directly and decrements stock', function () {
|
||||
$owner = createPurchaseUserWithPermission(
|
||||
PermissionEnum::PURCHASES_VIEW,
|
||||
PermissionEnum::PURCHASES_CREATE,
|
||||
PermissionEnum::PURCHASES_DELETE,
|
||||
PermissionEnum::OWNER_VERIFICATIONS_VERIFY,
|
||||
);
|
||||
|
||||
$supplier = Supplier::factory()->create();
|
||||
$price = RawMaterialPrice::factory()->create(['stock' => 10]);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->postJson(route('admin.manage.purchases.draft_items.store'), [
|
||||
'raw_material_price_id' => $price->id,
|
||||
'quantity' => 3,
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('admin.manage.purchases.store'), [
|
||||
'supplier_id' => $supplier->id,
|
||||
'discount' => 0,
|
||||
'shipping_cost' => 0,
|
||||
]);
|
||||
|
||||
expect((float) $price->fresh()->stock)->toBe(13.0);
|
||||
|
||||
$purchase = Purchase::query()->latest()->firstOrFail();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->delete(route('admin.manage.purchases.destroy', $purchase))
|
||||
->assertRedirect(route('admin.manage.purchases.index'));
|
||||
|
||||
expect(Purchase::query()->whereKey($purchase->id)->exists())->toBeFalse();
|
||||
expect((float) $price->fresh()->stock)->toBe(10.0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Purchase Model ────────────────────────────────────────
|
||||
|
||||
describe('Purchase Model', function () {
|
||||
test('purchase uses soft deletes', function () {
|
||||
$purchase = Purchase::factory()->create();
|
||||
|
||||
$purchase->delete();
|
||||
|
||||
expect($purchase->trashed())->toBeTrue();
|
||||
});
|
||||
|
||||
test('purchase has amounts cast to integer', function () {
|
||||
$purchase = Purchase::factory()->create([
|
||||
'subtotal' => 100000,
|
||||
'discount' => 5000,
|
||||
'shipping_cost' => 10000,
|
||||
'total' => 105000,
|
||||
]);
|
||||
|
||||
expect($purchase->subtotal)->toBeInt();
|
||||
expect($purchase->discount)->toBeInt();
|
||||
expect($purchase->shipping_cost)->toBeInt();
|
||||
expect($purchase->total)->toBeInt();
|
||||
});
|
||||
|
||||
test('purchase has formatted amount accessors', function () {
|
||||
$purchase = Purchase::factory()->create([
|
||||
'subtotal' => 100000,
|
||||
'discount' => 5000,
|
||||
'shipping_cost' => 10000,
|
||||
'total' => 105000,
|
||||
]);
|
||||
|
||||
expect($purchase->subtotal_formatted)->toBe('Rp 100.000');
|
||||
expect($purchase->discount_formatted)->toBe('Rp 5.000');
|
||||
expect($purchase->shipping_cost_formatted)->toBe('Rp 10.000');
|
||||
expect($purchase->total_formatted)->toBe('Rp 105.000');
|
||||
});
|
||||
|
||||
test('purchase belongs to supplier', function () {
|
||||
$supplier = Supplier::factory()->create();
|
||||
$purchase = Purchase::factory()->create(['supplier_id' => $supplier->id]);
|
||||
|
||||
expect($purchase->supplier)->not->toBeNull();
|
||||
expect($purchase->supplier->id)->toBe($supplier->id);
|
||||
});
|
||||
|
||||
test('purchase belongs to created by user', function () {
|
||||
$user = User::factory()->create();
|
||||
$purchase = Purchase::factory()->create(['created_by_id' => $user->id]);
|
||||
|
||||
expect($purchase->createdBy)->not->toBeNull();
|
||||
expect($purchase->createdBy->id)->toBe($user->id);
|
||||
});
|
||||
|
||||
test('purchase can have items', function () {
|
||||
$purchase = Purchase::factory()->create();
|
||||
PurchaseItem::factory()->count(3)->create(['purchase_id' => $purchase->id]);
|
||||
|
||||
expect($purchase->fresh()->items)->toHaveCount(3);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── PurchaseItem Model ────────────────────────────────────
|
||||
|
||||
describe('PurchaseItem Model', function () {
|
||||
test('purchase item has amounts cast to integer', function () {
|
||||
$purchase = Purchase::factory()->create();
|
||||
$item = PurchaseItem::factory()->create([
|
||||
'purchase_id' => $purchase->id,
|
||||
'unit_price' => 50000,
|
||||
'subtotal' => 100000,
|
||||
]);
|
||||
|
||||
expect($item->unit_price)->toBeInt();
|
||||
expect($item->subtotal)->toBeInt();
|
||||
});
|
||||
|
||||
test('purchase item has formatted amount accessors', function () {
|
||||
$purchase = Purchase::factory()->create();
|
||||
$item = PurchaseItem::factory()->create([
|
||||
'purchase_id' => $purchase->id,
|
||||
'unit_price' => 50000,
|
||||
'subtotal' => 100000,
|
||||
]);
|
||||
|
||||
expect($item->unit_price_formatted)->toBe('Rp 50.000');
|
||||
expect($item->subtotal_formatted)->toBe('Rp 100.000');
|
||||
});
|
||||
|
||||
test('purchase item belongs to purchase', function () {
|
||||
$purchase = Purchase::factory()->create();
|
||||
$item = PurchaseItem::factory()->create(['purchase_id' => $purchase->id]);
|
||||
|
||||
expect($item->purchase)->not->toBeNull();
|
||||
expect($item->purchase->id)->toBe($purchase->id);
|
||||
});
|
||||
|
||||
test('purchase item uses soft deletes', function () {
|
||||
$purchase = Purchase::factory()->create();
|
||||
$item = PurchaseItem::factory()->create(['purchase_id' => $purchase->id]);
|
||||
|
||||
$item->delete();
|
||||
|
||||
expect($item->trashed())->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user