feat: implement transaction management in RawMaterialService for create, update, delete, and toggle status operations; refactor error handling for improved clarity; enhance tests for raw material and price models to ensure proper functionality and validation
This commit is contained in:
parent
80e1542151
commit
9d0075d7b2
@ -10,17 +10,18 @@
|
||||
use App\Models\RawMaterial;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Models\User;
|
||||
use App\Services\Concerns\RunsInTransaction;
|
||||
use App\Services\Media\MediaService;
|
||||
use App\Services\System\PushNotificationService;
|
||||
use App\Support\Media\MediaPresenter;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class RawMaterialService
|
||||
{
|
||||
use RunsInTransaction;
|
||||
|
||||
private const MAX_VARIANT_IMAGES = 5;
|
||||
|
||||
public function __construct(
|
||||
@ -114,8 +115,8 @@ public function create(array $validated, User $user): void
|
||||
{
|
||||
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($validated, $user, $isOwner): void {
|
||||
$this->runInTransaction(
|
||||
function () use ($validated, $user, $isOwner): void {
|
||||
$rawMaterial = RawMaterial::create([
|
||||
'name' => $validated['name'],
|
||||
'unit' => $validated['unit'],
|
||||
@ -139,18 +140,9 @@ public function create(array $validated, User $user): void
|
||||
],
|
||||
]);
|
||||
}
|
||||
});
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gagal membuat bahan baku: '.$e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
||||
]);
|
||||
}
|
||||
},
|
||||
'Gagal membuat bahan baku',
|
||||
);
|
||||
|
||||
if (! $isOwner) {
|
||||
$this->notifyForPendingRequest(
|
||||
@ -167,8 +159,8 @@ public function update(RawMaterial $rawMaterial, array $validated, User $user):
|
||||
{
|
||||
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($validated, $rawMaterial, $user, $isOwner): void {
|
||||
$this->runInTransaction(
|
||||
function () use ($validated, $rawMaterial, $user, $isOwner): void {
|
||||
if ($isOwner) {
|
||||
$payload = $this->enrichPayload($this->buildPayloadFromValidated($validated));
|
||||
$this->applyPayloadToRawMaterial($rawMaterial, $payload);
|
||||
@ -201,18 +193,9 @@ public function update(RawMaterial $rawMaterial, array $validated, User $user):
|
||||
$this->syncRequestPriceImages($verificationRequest, $priceData, $index, required: $isNewPrice);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gagal memperbarui bahan baku: '.$e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
||||
]);
|
||||
}
|
||||
},
|
||||
'Gagal memperbarui bahan baku',
|
||||
);
|
||||
|
||||
if (! $isOwner) {
|
||||
$this->notifyForPendingRequest(
|
||||
@ -235,27 +218,22 @@ public function delete(RawMaterial $rawMaterial, User $user): void
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
OwnerVerificationRequest::create([
|
||||
'action' => OwnerVerificationAction::DELETE,
|
||||
'status' => OwnerVerificationStatus::PENDING,
|
||||
'subject_type' => RawMaterial::class,
|
||||
'subject_id' => $rawMaterial->id,
|
||||
'submitted_by_id' => $user->id,
|
||||
'payload' => [
|
||||
'old' => $this->snapshotRawMaterial($rawMaterial),
|
||||
'new' => null,
|
||||
],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gagal mengajukan penghapusan bahan baku: '.$e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
||||
]);
|
||||
}
|
||||
$this->runInTransaction(
|
||||
function () use ($rawMaterial, $user): void {
|
||||
OwnerVerificationRequest::create([
|
||||
'action' => OwnerVerificationAction::DELETE,
|
||||
'status' => OwnerVerificationStatus::PENDING,
|
||||
'subject_type' => RawMaterial::class,
|
||||
'subject_id' => $rawMaterial->id,
|
||||
'submitted_by_id' => $user->id,
|
||||
'payload' => [
|
||||
'old' => $this->snapshotRawMaterial($rawMaterial),
|
||||
'new' => null,
|
||||
],
|
||||
]);
|
||||
},
|
||||
'Gagal mengajukan penghapusan bahan baku',
|
||||
);
|
||||
|
||||
$this->notifyForPendingRequest(
|
||||
$user,
|
||||
@ -278,33 +256,28 @@ public function toggleStatus(RawMaterial $rawMaterial, array $validated, User $u
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
OwnerVerificationRequest::create([
|
||||
'action' => OwnerVerificationAction::TOGGLE_STATUS,
|
||||
'status' => OwnerVerificationStatus::PENDING,
|
||||
'subject_type' => RawMaterial::class,
|
||||
'subject_id' => $rawMaterial->id,
|
||||
'submitted_by_id' => $user->id,
|
||||
'payload' => [
|
||||
'old' => [
|
||||
'name' => $rawMaterial->name,
|
||||
'is_active' => $rawMaterial->is_active,
|
||||
$this->runInTransaction(
|
||||
function () use ($rawMaterial, $validated, $user): void {
|
||||
OwnerVerificationRequest::create([
|
||||
'action' => OwnerVerificationAction::TOGGLE_STATUS,
|
||||
'status' => OwnerVerificationStatus::PENDING,
|
||||
'subject_type' => RawMaterial::class,
|
||||
'subject_id' => $rawMaterial->id,
|
||||
'submitted_by_id' => $user->id,
|
||||
'payload' => [
|
||||
'old' => [
|
||||
'name' => $rawMaterial->name,
|
||||
'is_active' => $rawMaterial->is_active,
|
||||
],
|
||||
'new' => [
|
||||
'name' => $rawMaterial->name,
|
||||
'is_active' => (bool) $validated['is_active'],
|
||||
],
|
||||
],
|
||||
'new' => [
|
||||
'name' => $rawMaterial->name,
|
||||
'is_active' => (bool) $validated['is_active'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gagal mengajukan perubahan status bahan baku: '.$e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
||||
]);
|
||||
}
|
||||
]);
|
||||
},
|
||||
'Gagal mengajukan perubahan status bahan baku',
|
||||
);
|
||||
|
||||
$statusLabel = $validated['is_active'] ? 'aktif' : 'nonaktif';
|
||||
|
||||
@ -418,13 +391,16 @@ public function applyDelete(OwnerVerificationRequest $verificationRequest): void
|
||||
|
||||
private function applyDeleteSubject(RawMaterial $rawMaterial): void
|
||||
{
|
||||
DB::transaction(function () use ($rawMaterial): void {
|
||||
$rawMaterial->prices()->each(function (RawMaterialPrice $price): void {
|
||||
$price->clearMediaCollection('images');
|
||||
});
|
||||
$rawMaterial->prices()->delete();
|
||||
$rawMaterial->delete();
|
||||
});
|
||||
$this->runInTransaction(
|
||||
function () use ($rawMaterial): void {
|
||||
$rawMaterial->prices()->each(function (RawMaterialPrice $price): void {
|
||||
$price->clearMediaCollection('images');
|
||||
});
|
||||
$rawMaterial->prices()->delete();
|
||||
$rawMaterial->delete();
|
||||
},
|
||||
'Gagal menghapus bahan baku',
|
||||
);
|
||||
}
|
||||
|
||||
public function applyToggleStatus(OwnerVerificationRequest $verificationRequest): void
|
||||
@ -510,13 +486,16 @@ private function rejectCreate(OwnerVerificationRequest $verificationRequest): vo
|
||||
return;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($rawMaterial): void {
|
||||
$rawMaterial->prices()->each(function (RawMaterialPrice $price): void {
|
||||
$price->clearMediaCollection('images');
|
||||
});
|
||||
$rawMaterial->prices()->delete();
|
||||
$rawMaterial->delete();
|
||||
});
|
||||
$this->runInTransaction(
|
||||
function () use ($rawMaterial): void {
|
||||
$rawMaterial->prices()->each(function (RawMaterialPrice $price): void {
|
||||
$price->clearMediaCollection('images');
|
||||
});
|
||||
$rawMaterial->prices()->delete();
|
||||
$rawMaterial->delete();
|
||||
},
|
||||
'Gagal menolak bahan baku',
|
||||
);
|
||||
}
|
||||
|
||||
private function rollbackUpdate(OwnerVerificationRequest $verificationRequest): void
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
use App\Enums\OwnerVerificationStatus;
|
||||
use App\Enums\Permission as PermissionEnum;
|
||||
use App\Enums\RawMaterialUnit;
|
||||
use App\Models\OwnerVerificationRequest;
|
||||
use App\Models\RawMaterial;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Models\User;
|
||||
@ -11,6 +12,7 @@
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
@ -53,11 +55,16 @@ function createRawMaterialWithPrices(): RawMaterial
|
||||
|
||||
function priceWithImage(string $variant = 'Default', int $price = 50000, float $stock = 10): array
|
||||
{
|
||||
$disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public');
|
||||
$key = 'raw-materials/'.Str::uuid().".jpg";
|
||||
$imageContent = UploadedFile::fake()->image("{$variant}.jpg", 100, 100)->get();
|
||||
Storage::disk($disk)->put($key, $imageContent);
|
||||
|
||||
return [
|
||||
'variant' => $variant,
|
||||
'price' => $price,
|
||||
'stock' => $stock,
|
||||
'images' => [UploadedFile::fake()->image("{$variant}.jpg", 100, 100)],
|
||||
's3_keys' => [$key],
|
||||
];
|
||||
}
|
||||
|
||||
@ -725,4 +732,178 @@ function createRawMaterialVerifierUser(): User
|
||||
|
||||
expect($rawMaterial->unit_abbreviation)->toBe('kg');
|
||||
});
|
||||
|
||||
test('raw material has total_stock_formatted accessor', function () {
|
||||
$rawMaterial = RawMaterial::factory()->create(['unit' => RawMaterialUnit::METER->value]);
|
||||
RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'stock' => 10.5]);
|
||||
RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'stock' => 5.25]);
|
||||
|
||||
expect($rawMaterial->fresh()->total_stock_formatted)->toContain('15,75');
|
||||
expect($rawMaterial->fresh()->total_stock_formatted)->toContain('m');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── RawMaterialPrice Model ────────────────────────────────
|
||||
|
||||
describe('RawMaterialPrice Model', function () {
|
||||
test('price has price cast to integer', function () {
|
||||
$rawMaterial = RawMaterial::factory()->create();
|
||||
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'price' => 75000]);
|
||||
|
||||
expect($price->price)->toBeInt();
|
||||
expect($price->price)->toBe(75000);
|
||||
});
|
||||
|
||||
test('price has formatted price accessor', function () {
|
||||
$rawMaterial = RawMaterial::factory()->create();
|
||||
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'price' => 1500000]);
|
||||
|
||||
expect($price->price_formatted)->toBe('Rp 1.500.000');
|
||||
});
|
||||
|
||||
test('price has price_input accessor', function () {
|
||||
$rawMaterial = RawMaterial::factory()->create();
|
||||
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'price' => 200000]);
|
||||
|
||||
expect($price->price_input)->toBe('200000');
|
||||
});
|
||||
|
||||
test('price belongs to raw material', function () {
|
||||
$rawMaterial = RawMaterial::factory()->create();
|
||||
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
||||
|
||||
expect($price->rawMaterial)->not->toBeNull();
|
||||
expect($price->rawMaterial->id)->toBe($rawMaterial->id);
|
||||
});
|
||||
|
||||
test('price uses soft deletes', function () {
|
||||
$rawMaterial = RawMaterial::factory()->create();
|
||||
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
||||
|
||||
$price->delete();
|
||||
|
||||
expect($price->trashed())->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Owner Direct Actions ──────────────────────────────────
|
||||
|
||||
describe('Owner Direct Actions', function () {
|
||||
test('owner can create raw material directly without verification', function () {
|
||||
$owner = createRawMaterialUserWithPermission(
|
||||
PermissionEnum::RAW_MATERIALS_VIEW,
|
||||
PermissionEnum::RAW_MATERIALS_CREATE,
|
||||
PermissionEnum::OWNER_VERIFICATIONS_VERIFY,
|
||||
);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('admin.master.raw_materials.store'), [
|
||||
'name' => 'Kain Owner',
|
||||
'unit' => RawMaterialUnit::METER->value,
|
||||
'prices' => [
|
||||
priceWithImage('Merah', 75000, 50),
|
||||
],
|
||||
])
|
||||
->assertRedirect(route('admin.master.raw_materials.index'));
|
||||
|
||||
$rawMaterial = RawMaterial::where('name', 'Kain Owner')->first();
|
||||
expect($rawMaterial)->not->toBeNull();
|
||||
expect($rawMaterial->is_active)->toBeTrue();
|
||||
|
||||
$this->assertDatabaseMissing('owner_verification_requests', [
|
||||
'subject_id' => $rawMaterial->id,
|
||||
'subject_type' => RawMaterial::class,
|
||||
]);
|
||||
});
|
||||
|
||||
test('owner can update raw material directly without verification', function () {
|
||||
$owner = createRawMaterialUserWithPermission(
|
||||
PermissionEnum::RAW_MATERIALS_VIEW,
|
||||
PermissionEnum::RAW_MATERIALS_UPDATE,
|
||||
PermissionEnum::OWNER_VERIFICATIONS_VERIFY,
|
||||
);
|
||||
|
||||
$rawMaterial = createRawMaterialWithPrices();
|
||||
$price = $rawMaterial->prices->first();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->put(route('admin.master.raw_materials.update', $rawMaterial), [
|
||||
'name' => 'Kain Updated',
|
||||
'unit' => RawMaterialUnit::KILOGRAM->value,
|
||||
'prices' => [
|
||||
['id' => $price->id, 'variant' => 'Putih', 'price' => 90000, 'stock' => 25],
|
||||
],
|
||||
])
|
||||
->assertRedirect(route('admin.master.raw_materials.index'));
|
||||
|
||||
expect($rawMaterial->fresh()->name)->toBe('Kain Updated');
|
||||
expect($price->fresh()->variant)->toBe('Putih');
|
||||
});
|
||||
|
||||
test('owner can delete raw material directly without verification', function () {
|
||||
$owner = createRawMaterialUserWithPermission(
|
||||
PermissionEnum::RAW_MATERIALS_VIEW,
|
||||
PermissionEnum::RAW_MATERIALS_DELETE,
|
||||
PermissionEnum::OWNER_VERIFICATIONS_VERIFY,
|
||||
);
|
||||
|
||||
$rawMaterial = createRawMaterialWithPrices();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->delete(route('admin.master.raw_materials.destroy', $rawMaterial))
|
||||
->assertRedirect(route('admin.master.raw_materials.index'));
|
||||
|
||||
$this->assertSoftDeleted('raw_materials', ['id' => $rawMaterial->id]);
|
||||
});
|
||||
|
||||
test('owner can toggle raw material status directly without verification', function () {
|
||||
$owner = createRawMaterialUserWithPermission(
|
||||
PermissionEnum::RAW_MATERIALS_VIEW,
|
||||
PermissionEnum::RAW_MATERIALS_TOGGLE_STATUS,
|
||||
PermissionEnum::OWNER_VERIFICATIONS_VERIFY,
|
||||
);
|
||||
|
||||
$rawMaterial = RawMaterial::factory()->create();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [
|
||||
'is_active' => false,
|
||||
]);
|
||||
|
||||
expect($rawMaterial->fresh()->is_active)->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Reject Update Request ─────────────────────────────────
|
||||
|
||||
describe('Reject Update Request', function () {
|
||||
test('rejecting update request rolls back to old values', function () {
|
||||
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE);
|
||||
$verifier = createRawMaterialVerifierUser();
|
||||
|
||||
$rawMaterial = createRawMaterialWithPrices();
|
||||
$price = $rawMaterial->prices->first();
|
||||
$originalName = $rawMaterial->name;
|
||||
$originalVariant = $price->variant;
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.master.raw_materials.update', $rawMaterial), [
|
||||
'name' => 'Nama Diubah',
|
||||
'unit' => RawMaterialUnit::KILOGRAM->value,
|
||||
'prices' => [
|
||||
['id' => $price->id, 'variant' => 'Putih', 'price' => 90000, 'stock' => 25],
|
||||
],
|
||||
]);
|
||||
|
||||
$verificationRequest = OwnerVerificationRequest::query()->pending()->latest()->firstOrFail();
|
||||
|
||||
$this->actingAs($verifier)
|
||||
->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest), [
|
||||
'reason' => 'Tidak sesuai standar',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
expect($rawMaterial->fresh()->name)->toBe($originalName);
|
||||
expect($price->fresh()->variant)->toBe($originalVariant);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user