1125 lines
44 KiB
PHP
1125 lines
44 KiB
PHP
<?php
|
|
|
|
use App\Enums\CuttingStatus;
|
|
use App\Enums\Permission as PermissionEnum;
|
|
use App\Models\Category;
|
|
use App\Models\Cutting;
|
|
use App\Models\CuttingMaterial;
|
|
use App\Models\CuttingMaterialCombination;
|
|
use App\Models\CuttingResult;
|
|
use App\Models\Product;
|
|
use App\Models\ProductVariant;
|
|
use App\Models\RawMaterial;
|
|
use App\Models\RawMaterialPrice;
|
|
use App\Models\User;
|
|
use Database\Seeders\RolePermissionSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
$this->seed(RolePermissionSeeder::class);
|
|
Storage::fake('public');
|
|
});
|
|
|
|
// ─── Helper ───────────────────────────────────────────────
|
|
|
|
function createCuttingUserWithPermission(PermissionEnum ...$permissions): User
|
|
{
|
|
$user = User::factory()->create();
|
|
|
|
$user->givePermissionTo(
|
|
array_merge(
|
|
[PermissionEnum::DASHBOARD_VIEW->value],
|
|
array_map(fn (PermissionEnum $p) => $p->value, $permissions)
|
|
)
|
|
);
|
|
|
|
$user->forgetCachedPermissions();
|
|
|
|
return $user;
|
|
}
|
|
|
|
function createCuttingWithMaterialsAndResults(?User $user = null): Cutting
|
|
{
|
|
$user ??= User::factory()->create();
|
|
|
|
$cutting = Cutting::factory()->create(['created_by_id' => $user->id]);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
$product = Product::factory()->create();
|
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
|
|
|
CuttingMaterial::factory()->create([
|
|
'cutting_id' => $cutting->id,
|
|
'raw_material_price_id' => $price->id,
|
|
]);
|
|
|
|
CuttingResult::factory()->create([
|
|
'cutting_id' => $cutting->id,
|
|
'product_variant_id' => $variant->id,
|
|
]);
|
|
|
|
return $cutting;
|
|
}
|
|
|
|
function setupDraftItems(User $user): array
|
|
{
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
$product = Product::factory()->create();
|
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
|
|
|
return ['price' => $price, 'variant' => $variant];
|
|
}
|
|
|
|
// ─── Index ────────────────────────────────────────────────
|
|
|
|
describe('Cutting Index', function () {
|
|
test('authenticated user with permission can view cutting index', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.manage.cuttings.index'))
|
|
->assertOk();
|
|
});
|
|
|
|
test('guest is redirected to login', function () {
|
|
$this->get(route('admin.manage.cuttings.index'))
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without permission is forbidden', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.manage.cuttings.index'))
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('index displays cuttings', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW);
|
|
|
|
createCuttingWithMaterialsAndResults($user);
|
|
createCuttingWithMaterialsAndResults($user);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.manage.cuttings.index'))
|
|
->assertOk();
|
|
});
|
|
|
|
test('index can search cuttings by description', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW);
|
|
|
|
Cutting::factory()->create([
|
|
'created_by_id' => $user->id,
|
|
'description' => 'Cutting kain batik',
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.manage.cuttings.index', ['search' => 'batik']))
|
|
->assertOk();
|
|
});
|
|
});
|
|
|
|
// ─── Create ───────────────────────────────────────────────
|
|
|
|
describe('Cutting Create', function () {
|
|
test('authenticated user with permission can view create form', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.manage.cuttings.create'))
|
|
->assertOk();
|
|
});
|
|
|
|
test('guest is redirected to login', function () {
|
|
$this->get(route('admin.manage.cuttings.create'))
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without create permission can still view create form', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.manage.cuttings.create'))
|
|
->assertOk();
|
|
});
|
|
});
|
|
|
|
// ─── Store ────────────────────────────────────────────────
|
|
|
|
describe('Cutting Store', function () {
|
|
test('authenticated user with permission can create a cutting', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$draft = setupDraftItems($user);
|
|
|
|
// Create draft material
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.cuttings.draft_materials.store'), [
|
|
'raw_material_price_id' => $draft['price']->id,
|
|
'material_usage' => 5,
|
|
])
|
|
->assertOk();
|
|
|
|
// Create draft result
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.cuttings.draft_results.store'), [
|
|
'product_variant_id' => $draft['variant']->id,
|
|
'cutting_result' => 10,
|
|
'sample' => 8,
|
|
'original_outside_sample' => 2,
|
|
])
|
|
->assertOk();
|
|
|
|
// Store cutting
|
|
$this->actingAs($user)
|
|
->post(route('admin.manage.cuttings.store'), [
|
|
'description' => 'Cutting test',
|
|
'sewing_cost' => 50000,
|
|
'other_cost' => 10000,
|
|
])
|
|
->assertRedirect(route('admin.manage.cuttings.index'));
|
|
|
|
$cutting = Cutting::where('description', 'Cutting test')->first();
|
|
expect($cutting)->not->toBeNull();
|
|
expect($cutting->status)->toBe(CuttingStatus::IN_PROGRESS);
|
|
expect($cutting->materials)->toHaveCount(1);
|
|
expect($cutting->results)->toHaveCount(1);
|
|
});
|
|
|
|
test('draft material can have material_result', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$draft = setupDraftItems($user);
|
|
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.cuttings.draft_materials.store'), [
|
|
'raw_material_price_id' => $draft['price']->id,
|
|
'material_usage' => 5,
|
|
'material_result' => 10,
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('item.material_result', 10);
|
|
|
|
$this->assertDatabaseHas('cutting_materials', [
|
|
'raw_material_price_id' => $draft['price']->id,
|
|
'material_result' => 10,
|
|
]);
|
|
});
|
|
|
|
test('material_result is preserved when creating cutting from drafts', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$draft = setupDraftItems($user);
|
|
|
|
// Create draft material with material_result
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.cuttings.draft_materials.store'), [
|
|
'raw_material_price_id' => $draft['price']->id,
|
|
'material_usage' => 5,
|
|
'material_result' => 10,
|
|
])
|
|
->assertOk();
|
|
|
|
// Create draft result
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.cuttings.draft_results.store'), [
|
|
'product_variant_id' => $draft['variant']->id,
|
|
'cutting_result' => 10,
|
|
'sample' => 8,
|
|
'original_outside_sample' => 2,
|
|
])
|
|
->assertOk();
|
|
|
|
// Store cutting
|
|
$this->actingAs($user)
|
|
->post(route('admin.manage.cuttings.store'), [
|
|
'description' => 'Cutting with result',
|
|
])
|
|
->assertRedirect(route('admin.manage.cuttings.index'));
|
|
|
|
$cutting = Cutting::where('description', 'Cutting with result')->first();
|
|
expect($cutting->materials->first()->material_result)->toBe(10);
|
|
});
|
|
|
|
test('guest cannot create a cutting', function () {
|
|
$this->post(route('admin.manage.cuttings.store'), [
|
|
'description' => 'Test',
|
|
])->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without create permission is forbidden', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.manage.cuttings.store'), [
|
|
'description' => 'Test',
|
|
])
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('store fails without draft materials', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$draft = setupDraftItems($user);
|
|
|
|
// Only create draft result, no material
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.cuttings.draft_results.store'), [
|
|
'product_variant_id' => $draft['variant']->id,
|
|
'cutting_result' => 10,
|
|
'sample' => 8,
|
|
'original_outside_sample' => 2,
|
|
])
|
|
->assertOk();
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.manage.cuttings.store'), [
|
|
'description' => 'Test',
|
|
])
|
|
->assertSessionHasErrors('materials');
|
|
});
|
|
|
|
test('store fails without draft results', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$draft = setupDraftItems($user);
|
|
|
|
// Only create draft material, no result
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.cuttings.draft_materials.store'), [
|
|
'raw_material_price_id' => $draft['price']->id,
|
|
'material_usage' => 5,
|
|
])
|
|
->assertOk();
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.manage.cuttings.store'), [
|
|
'description' => 'Test',
|
|
])
|
|
->assertSessionHasErrors('results');
|
|
});
|
|
});
|
|
|
|
// ─── Edit ─────────────────────────────────────────────────
|
|
|
|
describe('Cutting Edit', function () {
|
|
test('authenticated user with permission can view edit form', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE);
|
|
|
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.manage.cuttings.edit', $cutting))
|
|
->assertOk();
|
|
});
|
|
|
|
test('guest is redirected to login', function () {
|
|
$cutting = createCuttingWithMaterialsAndResults();
|
|
|
|
$this->get(route('admin.manage.cuttings.edit', $cutting))
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without update permission can still view edit form', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW);
|
|
|
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.manage.cuttings.edit', $cutting))
|
|
->assertOk();
|
|
});
|
|
|
|
test('completed cutting cannot be edited', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE);
|
|
|
|
$cutting = Cutting::factory()->completed()->create(['created_by_id' => $user->id]);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.manage.cuttings.edit', $cutting))
|
|
->assertRedirect(route('admin.manage.cuttings.index'));
|
|
});
|
|
});
|
|
|
|
// ─── Update ───────────────────────────────────────────────
|
|
|
|
describe('Cutting Update', function () {
|
|
test('authenticated user with permission can update a cutting', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE);
|
|
|
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
$product = Product::factory()->create();
|
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.manage.cuttings.update', $cutting), [
|
|
'description' => 'Updated description',
|
|
'sewing_cost' => 75000,
|
|
'other_cost' => 15000,
|
|
'materials' => [
|
|
['raw_material_price_id' => $price->id, 'material_usage' => 10],
|
|
],
|
|
'results' => [
|
|
[
|
|
'product_variant_id' => $variant->id,
|
|
'cutting_result' => 20,
|
|
'sample' => 15,
|
|
'original_outside_sample' => 5,
|
|
],
|
|
],
|
|
])
|
|
->assertRedirect(route('admin.manage.cuttings.index'));
|
|
|
|
expect($cutting->fresh()->description)->toBe('Updated description');
|
|
});
|
|
|
|
test('update can save material_result for single materials', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE);
|
|
|
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
$product = Product::factory()->create();
|
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.manage.cuttings.update', $cutting), [
|
|
'description' => 'With material result',
|
|
'sewing_cost' => 0,
|
|
'other_cost' => 0,
|
|
'materials' => [
|
|
['raw_material_price_id' => $price->id, 'material_usage' => 10, 'material_result' => 25],
|
|
],
|
|
'results' => [
|
|
[
|
|
'product_variant_id' => $variant->id,
|
|
'cutting_result' => 20,
|
|
'sample' => 15,
|
|
'original_outside_sample' => 5,
|
|
],
|
|
],
|
|
])
|
|
->assertRedirect(route('admin.manage.cuttings.index'));
|
|
|
|
$cutting->refresh();
|
|
expect($cutting->materials->first()->material_result)->toBe(25);
|
|
});
|
|
|
|
test('guest cannot update a cutting', function () {
|
|
$cutting = createCuttingWithMaterialsAndResults();
|
|
|
|
$this->put(route('admin.manage.cuttings.update', $cutting), [
|
|
'description' => 'Test',
|
|
'materials' => [['raw_material_price_id' => 1, 'material_usage' => 1]],
|
|
'results' => [['product_variant_id' => 1, 'cutting_result' => 1, 'sample' => 1, 'original_outside_sample' => 0]],
|
|
])->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without update permission is forbidden', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW);
|
|
|
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.manage.cuttings.update', $cutting), [
|
|
'description' => 'Test',
|
|
'materials' => [['raw_material_price_id' => 1, 'material_usage' => 1]],
|
|
'results' => [['product_variant_id' => 1, 'cutting_result' => 1, 'sample' => 1, 'original_outside_sample' => 0]],
|
|
])
|
|
->assertForbidden();
|
|
});
|
|
});
|
|
|
|
// ─── Destroy ──────────────────────────────────────────────
|
|
|
|
describe('Cutting Destroy', function () {
|
|
test('authenticated user with permission can delete a cutting', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_DELETE);
|
|
|
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
|
|
|
$this->actingAs($user)
|
|
->delete(route('admin.manage.cuttings.destroy', $cutting))
|
|
->assertRedirect(route('admin.manage.cuttings.index'));
|
|
|
|
$this->assertSoftDeleted('cuttings', ['id' => $cutting->id]);
|
|
});
|
|
|
|
test('guest cannot delete a cutting', function () {
|
|
$cutting = createCuttingWithMaterialsAndResults();
|
|
|
|
$this->delete(route('admin.manage.cuttings.destroy', $cutting))
|
|
->assertRedirect(route('login'));
|
|
|
|
$this->assertNotSoftDeleted('cuttings', ['id' => $cutting->id]);
|
|
});
|
|
|
|
test('user without delete permission can still delete cutting', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW);
|
|
|
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
|
|
|
$this->actingAs($user)
|
|
->delete(route('admin.manage.cuttings.destroy', $cutting))
|
|
->assertRedirect(route('admin.manage.cuttings.index'));
|
|
|
|
$this->assertSoftDeleted('cuttings', ['id' => $cutting->id]);
|
|
});
|
|
|
|
test('completed cutting cannot be deleted', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_DELETE);
|
|
|
|
$cutting = Cutting::factory()->completed()->create(['created_by_id' => $user->id]);
|
|
|
|
$this->actingAs($user)
|
|
->delete(route('admin.manage.cuttings.destroy', $cutting))
|
|
->assertSessionHasErrors('status');
|
|
|
|
$this->assertNotSoftDeleted('cuttings', ['id' => $cutting->id]);
|
|
});
|
|
});
|
|
|
|
// ─── Status Transition ────────────────────────────────────
|
|
|
|
describe('Cutting Status Transition', function () {
|
|
test('authenticated user can complete a cutting', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_COMPLETE);
|
|
|
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.manage.cuttings.transition_status', $cutting), [
|
|
'status' => CuttingStatus::COMPLETED->value,
|
|
])
|
|
->assertRedirect(route('admin.manage.cuttings.index'));
|
|
|
|
expect($cutting->fresh()->status)->toBe(CuttingStatus::COMPLETED);
|
|
});
|
|
|
|
test('guest cannot transition status', function () {
|
|
$cutting = createCuttingWithMaterialsAndResults();
|
|
|
|
$this->post(route('admin.manage.cuttings.transition_status', $cutting), [
|
|
'status' => CuttingStatus::COMPLETED->value,
|
|
])->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without permission cannot complete cutting', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW);
|
|
|
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.manage.cuttings.transition_status', $cutting), [
|
|
'status' => CuttingStatus::COMPLETED->value,
|
|
])
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('invalid status transition is rejected', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_COMPLETE, PermissionEnum::CUTTINGS_VERIFY);
|
|
|
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
|
|
|
// IN_PROGRESS cannot go directly to VERIFIED
|
|
$this->actingAs($user)
|
|
->post(route('admin.manage.cuttings.transition_status', $cutting), [
|
|
'status' => CuttingStatus::VERIFIED->value,
|
|
])
|
|
->assertSessionHasErrors('status');
|
|
});
|
|
|
|
test('completed cutting can be verified by owner', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_COMPLETE, PermissionEnum::CUTTINGS_VERIFY);
|
|
$user->assignRole('owner');
|
|
$user->forgetCachedPermissions();
|
|
|
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
|
|
|
// First complete
|
|
$this->actingAs($user)
|
|
->post(route('admin.manage.cuttings.transition_status', $cutting), [
|
|
'status' => CuttingStatus::COMPLETED->value,
|
|
]);
|
|
|
|
expect($cutting->fresh()->status)->toBe(CuttingStatus::COMPLETED);
|
|
|
|
// Then verify with result_prices
|
|
$result = $cutting->results->first();
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.manage.cuttings.transition_status', $cutting), [
|
|
'status' => CuttingStatus::VERIFIED->value,
|
|
'results' => [
|
|
[
|
|
'product_variant_id' => $result->product_variant_id,
|
|
'sample' => $result->sample ?? 0,
|
|
'original_outside_sample' => $result->original_outside_sample ?? 0,
|
|
],
|
|
],
|
|
'result_prices' => [
|
|
[
|
|
'product_variant_id' => $result->product_variant_id,
|
|
'prices' => [
|
|
['type' => 'retail', 'price' => 150000],
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
expect($cutting->fresh()->status)->toBe(CuttingStatus::VERIFIED);
|
|
});
|
|
});
|
|
|
|
// ─── Draft Combination ──────────────────────────────────────
|
|
|
|
describe('Draft Combination', function () {
|
|
test('authenticated user with permission can create draft combination', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.cuttings.draft_combinations.store'), [
|
|
'materials' => [
|
|
['raw_material_price_id' => $price1->id, 'material_usage' => 3],
|
|
['raw_material_price_id' => $price2->id, 'material_usage' => 2],
|
|
],
|
|
])
|
|
->assertOk()
|
|
->assertJsonStructure(['items' => [['raw_material_price_id', 'combination_id']]]);
|
|
|
|
$this->assertDatabaseCount('cutting_material_combinations', 1);
|
|
$this->assertDatabaseCount('cutting_materials', 2);
|
|
});
|
|
|
|
test('draft combination can have material_result', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.cuttings.draft_combinations.store'), [
|
|
'materials' => [
|
|
['raw_material_price_id' => $price1->id, 'material_usage' => 3],
|
|
['raw_material_price_id' => $price2->id, 'material_usage' => 2],
|
|
],
|
|
'material_result' => 15,
|
|
])
|
|
->assertOk();
|
|
|
|
$combination = CuttingMaterialCombination::first();
|
|
expect($combination->material_result)->toBe(15);
|
|
});
|
|
|
|
test('draft combination requires at least 2 materials', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.cuttings.draft_combinations.store'), [
|
|
'materials' => [
|
|
['raw_material_price_id' => $price->id, 'material_usage' => 3],
|
|
],
|
|
])
|
|
->assertUnprocessable();
|
|
});
|
|
|
|
test('authenticated user can delete draft combination', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.cuttings.draft_combinations.store'), [
|
|
'materials' => [
|
|
['raw_material_price_id' => $price1->id, 'material_usage' => 3],
|
|
['raw_material_price_id' => $price2->id, 'material_usage' => 2],
|
|
],
|
|
])
|
|
->assertOk();
|
|
|
|
$combination = CuttingMaterialCombination::first();
|
|
|
|
$this->actingAs($user)
|
|
->deleteJson(route('admin.manage.cuttings.draft_combinations.destroy', $combination->id))
|
|
->assertOk();
|
|
|
|
$this->assertDatabaseCount('cutting_material_combinations', 0);
|
|
$this->assertDatabaseCount('cutting_materials', 0);
|
|
});
|
|
|
|
test('user without permission cannot create draft combination', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.cuttings.draft_combinations.store'), [
|
|
'materials' => [
|
|
['raw_material_price_id' => $price1->id, 'material_usage' => 3],
|
|
['raw_material_price_id' => $price2->id, 'material_usage' => 2],
|
|
],
|
|
])
|
|
->assertForbidden();
|
|
});
|
|
});
|
|
|
|
// ─── Quick Create Product ─────────────────────────────────
|
|
|
|
describe('Quick Create Product', function () {
|
|
test('authenticated user with permission can quick create product without prices', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->postJson('/admin/manage/cuttings/quick-create-product', [
|
|
'name' => 'Test Product',
|
|
'description' => 'Test Description',
|
|
'variants' => [
|
|
[
|
|
'name' => 'Varian 1',
|
|
'stock' => 10,
|
|
'retail_stock' => 5,
|
|
],
|
|
],
|
|
])
|
|
->assertOk()
|
|
->assertJsonStructure(['product' => ['id', 'name']]);
|
|
|
|
$this->assertDatabaseHas('products', ['name' => 'Test Product']);
|
|
$this->assertDatabaseHas('product_variants', ['name' => 'Varian 1', 'stock' => 10]);
|
|
});
|
|
|
|
test('quick create product without images is allowed', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->postJson('/admin/manage/cuttings/quick-create-product', [
|
|
'name' => 'Product No Image',
|
|
'variants' => [
|
|
[
|
|
'name' => 'Varian 1',
|
|
'stock' => 5,
|
|
'retail_stock' => 2,
|
|
],
|
|
],
|
|
])
|
|
->assertOk();
|
|
|
|
$this->assertDatabaseHas('products', ['name' => 'Product No Image']);
|
|
});
|
|
|
|
test('quick create product requires name', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->postJson('/admin/manage/cuttings/quick-create-product', [
|
|
'variants' => [
|
|
[
|
|
'name' => 'Varian 1',
|
|
'stock' => 5,
|
|
'retail_stock' => 2,
|
|
],
|
|
],
|
|
])
|
|
->assertUnprocessable();
|
|
});
|
|
|
|
test('quick create product requires at least one variant', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->postJson('/admin/manage/cuttings/quick-create-product', [
|
|
'name' => 'Test Product',
|
|
'variants' => [],
|
|
])
|
|
->assertUnprocessable();
|
|
});
|
|
|
|
test('quick create product with categories', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user)
|
|
->postJson('/admin/manage/cuttings/quick-create-product', [
|
|
'name' => 'Product With Category',
|
|
'category_ids' => [$category->id],
|
|
'variants' => [
|
|
[
|
|
'name' => 'Varian 1',
|
|
'stock' => 5,
|
|
'retail_stock' => 2,
|
|
],
|
|
],
|
|
])
|
|
->assertOk();
|
|
|
|
$product = Product::where('name', 'Product With Category')->first();
|
|
expect($product)->not->toBeNull();
|
|
expect($product->categories)->toHaveCount(1);
|
|
});
|
|
|
|
test('user without permission cannot quick create product', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW);
|
|
|
|
$this->actingAs($user)
|
|
->postJson('/admin/manage/cuttings/quick-create-product', [
|
|
'name' => 'Test Product',
|
|
'variants' => [
|
|
[
|
|
'name' => 'Varian 1',
|
|
'stock' => 5,
|
|
'retail_stock' => 2,
|
|
],
|
|
],
|
|
])
|
|
->assertForbidden();
|
|
});
|
|
});
|
|
|
|
// ─── Store with Combination ─────────────────────────────────
|
|
|
|
describe('Cutting Store with Combination', function () {
|
|
test('cutting can be created with combination materials', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
$product = Product::factory()->create();
|
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
|
|
|
// Create draft combination
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.cuttings.draft_combinations.store'), [
|
|
'materials' => [
|
|
['raw_material_price_id' => $price1->id, 'material_usage' => 3],
|
|
['raw_material_price_id' => $price2->id, 'material_usage' => 2],
|
|
],
|
|
])
|
|
->assertOk();
|
|
|
|
// Create draft result
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.cuttings.draft_results.store'), [
|
|
'product_variant_id' => $variant->id,
|
|
'cutting_result' => 10,
|
|
'sample' => 8,
|
|
'original_outside_sample' => 2,
|
|
])
|
|
->assertOk();
|
|
|
|
// Store cutting
|
|
$this->actingAs($user)
|
|
->post(route('admin.manage.cuttings.store'), [
|
|
'description' => 'Cutting with combination',
|
|
])
|
|
->assertRedirect(route('admin.manage.cuttings.index'));
|
|
|
|
$cutting = Cutting::where('description', 'Cutting with combination')->first();
|
|
expect($cutting)->not->toBeNull();
|
|
expect($cutting->materials)->toHaveCount(2);
|
|
expect($cutting->materials->first()->combination_id)->not->toBeNull();
|
|
expect($cutting->materials->first()->combination_id)->toBe($cutting->materials->last()->combination_id);
|
|
});
|
|
});
|
|
|
|
// ─── Update with Combination ────────────────────────────────
|
|
|
|
describe('Cutting Update with Combination', function () {
|
|
test('cutting can be updated with combination materials', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE);
|
|
|
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
$product = Product::factory()->create();
|
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.manage.cuttings.update', $cutting), [
|
|
'description' => 'Updated with combination',
|
|
'sewing_cost' => 0,
|
|
'other_cost' => 0,
|
|
'materials' => [
|
|
['raw_material_price_id' => $price1->id, 'material_usage' => 3, 'combination_id' => 999],
|
|
['raw_material_price_id' => $price2->id, 'material_usage' => 2, 'combination_id' => 999],
|
|
],
|
|
'results' => [
|
|
[
|
|
'product_variant_id' => $variant->id,
|
|
'cutting_result' => 10,
|
|
'sample' => 8,
|
|
'original_outside_sample' => 2,
|
|
],
|
|
],
|
|
])
|
|
->assertRedirect(route('admin.manage.cuttings.index'));
|
|
|
|
$cutting->refresh();
|
|
expect($cutting->description)->toBe('Updated with combination');
|
|
expect($cutting->materials)->toHaveCount(2);
|
|
// Both materials should have the same combination_id
|
|
expect($cutting->materials->first()->combination_id)->not->toBeNull();
|
|
expect($cutting->materials->first()->combination_id)->toBe($cutting->materials->last()->combination_id);
|
|
});
|
|
|
|
test('update can save combination_material_result', function () {
|
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE);
|
|
|
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
$product = Product::factory()->create();
|
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.manage.cuttings.update', $cutting), [
|
|
'description' => 'Combination with result',
|
|
'sewing_cost' => 0,
|
|
'other_cost' => 0,
|
|
'materials' => [
|
|
['raw_material_price_id' => $price1->id, 'material_usage' => 3, 'combination_id' => 999, 'combination_material_result' => 20],
|
|
['raw_material_price_id' => $price2->id, 'material_usage' => 2, 'combination_id' => 999, 'combination_material_result' => 20],
|
|
],
|
|
'results' => [
|
|
[
|
|
'product_variant_id' => $variant->id,
|
|
'cutting_result' => 10,
|
|
'sample' => 8,
|
|
'original_outside_sample' => 2,
|
|
],
|
|
],
|
|
])
|
|
->assertRedirect(route('admin.manage.cuttings.index'));
|
|
|
|
$cutting->refresh();
|
|
$combination = $cutting->combinations()->first();
|
|
expect($combination)->not->toBeNull();
|
|
expect($combination->material_result)->toBe(20);
|
|
});
|
|
});
|
|
|
|
// ─── Cutting Model ────────────────────────────────────────
|
|
|
|
describe('Cutting Model', function () {
|
|
test('cutting uses soft deletes', function () {
|
|
$cutting = Cutting::factory()->create();
|
|
|
|
$cutting->delete();
|
|
|
|
expect($cutting->trashed())->toBeTrue();
|
|
});
|
|
|
|
test('cutting has status cast to enum', function () {
|
|
$cutting = Cutting::factory()->create(['status' => CuttingStatus::IN_PROGRESS->value]);
|
|
|
|
expect($cutting->status)->toBe(CuttingStatus::IN_PROGRESS);
|
|
});
|
|
|
|
test('cutting has status label accessor', function () {
|
|
$cutting = Cutting::factory()->create(['status' => CuttingStatus::IN_PROGRESS->value]);
|
|
|
|
expect($cutting->status_label)->toBe('Proses');
|
|
});
|
|
|
|
test('cutting can have materials', function () {
|
|
$cutting = Cutting::factory()->create();
|
|
CuttingMaterial::factory()->count(3)->create(['cutting_id' => $cutting->id]);
|
|
|
|
expect($cutting->fresh()->materials)->toHaveCount(3);
|
|
});
|
|
|
|
test('cutting can have results', function () {
|
|
$cutting = Cutting::factory()->create();
|
|
CuttingResult::factory()->count(2)->create(['cutting_id' => $cutting->id]);
|
|
|
|
expect($cutting->fresh()->results)->toHaveCount(2);
|
|
});
|
|
|
|
test('cutting belongs to created by user', function () {
|
|
$user = User::factory()->create();
|
|
$cutting = Cutting::factory()->create(['created_by_id' => $user->id]);
|
|
|
|
expect($cutting->createdBy)->not->toBeNull();
|
|
expect($cutting->createdBy->id)->toBe($user->id);
|
|
});
|
|
|
|
test('cutting status is editable for in_progress and rejected', function () {
|
|
$inProgress = Cutting::factory()->create(['status' => CuttingStatus::IN_PROGRESS->value]);
|
|
$rejected = Cutting::factory()->create(['status' => CuttingStatus::REJECTED->value]);
|
|
|
|
expect($inProgress->status->isEditable())->toBeTrue();
|
|
expect($rejected->status->isEditable())->toBeTrue();
|
|
});
|
|
});
|
|
|
|
// ─── CuttingMaterial Model ─────────────────────────────────
|
|
|
|
describe('CuttingMaterial Model', function () {
|
|
test('cutting material has material_usage cast to decimal', function () {
|
|
$cutting = Cutting::factory()->create();
|
|
$material = CuttingMaterial::factory()->create([
|
|
'cutting_id' => $cutting->id,
|
|
'material_usage' => 10.5,
|
|
]);
|
|
|
|
expect((float) $material->material_usage)->toBe(10.5);
|
|
});
|
|
|
|
test('cutting material belongs to cutting', function () {
|
|
$cutting = Cutting::factory()->create();
|
|
$material = CuttingMaterial::factory()->create(['cutting_id' => $cutting->id]);
|
|
|
|
expect($material->cutting)->not->toBeNull();
|
|
expect($material->cutting->id)->toBe($cutting->id);
|
|
});
|
|
|
|
test('cutting material uses soft deletes', function () {
|
|
$cutting = Cutting::factory()->create();
|
|
$material = CuttingMaterial::factory()->create(['cutting_id' => $cutting->id]);
|
|
|
|
$material->delete();
|
|
|
|
expect($material->trashed())->toBeTrue();
|
|
});
|
|
|
|
test('cutting material has material_result cast to integer', function () {
|
|
$cutting = Cutting::factory()->create();
|
|
$material = CuttingMaterial::factory()->create([
|
|
'cutting_id' => $cutting->id,
|
|
'material_result' => 10,
|
|
]);
|
|
|
|
expect($material->material_result)->toBeInt();
|
|
expect($material->material_result)->toBe(10);
|
|
});
|
|
});
|
|
|
|
// ─── CuttingResult Model ───────────────────────────────────
|
|
|
|
describe('CuttingResult Model', function () {
|
|
test('cutting result has integer casts', function () {
|
|
$cutting = Cutting::factory()->create();
|
|
$result = CuttingResult::factory()->create([
|
|
'cutting_id' => $cutting->id,
|
|
'cutting_result' => 50,
|
|
'sample' => 40,
|
|
'original_outside_sample' => 10,
|
|
]);
|
|
|
|
expect($result->cutting_result)->toBeInt();
|
|
expect($result->sample)->toBeInt();
|
|
expect($result->original_outside_sample)->toBeInt();
|
|
});
|
|
|
|
test('cutting result belongs to cutting', function () {
|
|
$cutting = Cutting::factory()->create();
|
|
$result = CuttingResult::factory()->create(['cutting_id' => $cutting->id]);
|
|
|
|
expect($result->cutting)->not->toBeNull();
|
|
expect($result->cutting->id)->toBe($cutting->id);
|
|
});
|
|
|
|
test('cutting result uses soft deletes', function () {
|
|
$cutting = Cutting::factory()->create();
|
|
$result = CuttingResult::factory()->create(['cutting_id' => $cutting->id]);
|
|
|
|
$result->delete();
|
|
|
|
expect($result->trashed())->toBeTrue();
|
|
});
|
|
});
|
|
|
|
// ─── CuttingMaterialCombination Model ────────────────────────
|
|
|
|
describe('CuttingMaterialCombination Model', function () {
|
|
test('combination uses soft deletes', function () {
|
|
$combination = CuttingMaterialCombination::factory()->create();
|
|
|
|
$combination->delete();
|
|
|
|
expect($combination->trashed())->toBeTrue();
|
|
});
|
|
|
|
test('combination belongs to cutting', function () {
|
|
$cutting = Cutting::factory()->create();
|
|
$combination = CuttingMaterialCombination::factory()->create([
|
|
'cutting_id' => $cutting->id,
|
|
]);
|
|
|
|
expect($combination->cutting)->not->toBeNull();
|
|
expect($combination->cutting->id)->toBe($cutting->id);
|
|
});
|
|
|
|
test('combination has many materials', function () {
|
|
$combination = CuttingMaterialCombination::factory()->create();
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
CuttingMaterial::factory()->create([
|
|
'combination_id' => $combination->id,
|
|
'raw_material_price_id' => $price1->id,
|
|
]);
|
|
CuttingMaterial::factory()->create([
|
|
'combination_id' => $combination->id,
|
|
'raw_material_price_id' => $price2->id,
|
|
]);
|
|
|
|
expect($combination->materials)->toHaveCount(2);
|
|
});
|
|
|
|
test('combination can be draft (cutting_id null)', function () {
|
|
$user = User::factory()->create();
|
|
$combination = CuttingMaterialCombination::factory()->create([
|
|
'user_id' => $user->id,
|
|
'cutting_id' => null,
|
|
]);
|
|
|
|
expect($combination->cutting_id)->toBeNull();
|
|
expect($combination->user_id)->toBe($user->id);
|
|
});
|
|
|
|
test('combination has material_result cast to integer', function () {
|
|
$combination = CuttingMaterialCombination::factory()->create([
|
|
'material_result' => 15,
|
|
]);
|
|
|
|
expect($combination->material_result)->toBeInt();
|
|
expect($combination->material_result)->toBe(15);
|
|
});
|
|
});
|