feat: implement transaction management in ProductService for create, update, delete, and toggle status operations; refactor error handling to improve clarity and maintainability; update routes to ensure proper permission checks and enhance product management functionality
This commit is contained in:
parent
8404b8af41
commit
80e1542151
@ -10,17 +10,18 @@
|
|||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\ProductVariant;
|
use App\Models\ProductVariant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\Concerns\RunsInTransaction;
|
||||||
use App\Services\Media\MediaService;
|
use App\Services\Media\MediaService;
|
||||||
use App\Services\System\PushNotificationService;
|
use App\Services\System\PushNotificationService;
|
||||||
use App\Support\Media\MediaPresenter;
|
use App\Support\Media\MediaPresenter;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class ProductService
|
class ProductService
|
||||||
{
|
{
|
||||||
|
use RunsInTransaction;
|
||||||
|
|
||||||
private const MAX_VARIANT_IMAGES = 5;
|
private const MAX_VARIANT_IMAGES = 5;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@ -119,8 +120,8 @@ public function create(array $validated, User $user): void
|
|||||||
{
|
{
|
||||||
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
||||||
|
|
||||||
try {
|
$this->runInTransaction(
|
||||||
DB::transaction(function () use ($validated, $user, $isOwner): void {
|
function () use ($validated, $user, $isOwner): void {
|
||||||
$product = Product::create([
|
$product = Product::create([
|
||||||
'name' => $validated['name'],
|
'name' => $validated['name'],
|
||||||
'description' => $validated['description'] ?? null,
|
'description' => $validated['description'] ?? null,
|
||||||
@ -161,18 +162,9 @@ public function create(array $validated, User $user): void
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
} catch (ValidationException $e) {
|
'Gagal membuat produk',
|
||||||
throw $e;
|
);
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::error('Gagal membuat produk: '.$e->getMessage(), [
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $isOwner) {
|
if (! $isOwner) {
|
||||||
$this->notifyForPendingRequest(
|
$this->notifyForPendingRequest(
|
||||||
@ -189,8 +181,8 @@ public function update(Product $product, array $validated, User $user): void
|
|||||||
{
|
{
|
||||||
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
||||||
|
|
||||||
try {
|
$this->runInTransaction(
|
||||||
DB::transaction(function () use ($validated, $product, $user, $isOwner): void {
|
function () use ($validated, $product, $user, $isOwner): void {
|
||||||
if ($isOwner) {
|
if ($isOwner) {
|
||||||
$payload = $this->enrichPayload($this->buildPayloadFromValidated($validated));
|
$payload = $this->enrichPayload($this->buildPayloadFromValidated($validated));
|
||||||
$this->applyPayloadToProduct($product, $payload);
|
$this->applyPayloadToProduct($product, $payload);
|
||||||
@ -244,18 +236,9 @@ public function update(Product $product, array $validated, User $user): void
|
|||||||
$this->syncRequestVariantImages($verificationRequest, $variantData, $index, required: $isNewVariant);
|
$this->syncRequestVariantImages($verificationRequest, $variantData, $index, required: $isNewVariant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
} catch (ValidationException $e) {
|
'Gagal memperbarui produk',
|
||||||
throw $e;
|
);
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::error('Gagal memperbarui produk: '.$e->getMessage(), [
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $isOwner) {
|
if (! $isOwner) {
|
||||||
$this->notifyForPendingRequest(
|
$this->notifyForPendingRequest(
|
||||||
@ -278,27 +261,22 @@ public function delete(Product $product, User $user): void
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
$this->runInTransaction(
|
||||||
OwnerVerificationRequest::create([
|
function () use ($product, $user): void {
|
||||||
'action' => OwnerVerificationAction::DELETE,
|
OwnerVerificationRequest::create([
|
||||||
'status' => OwnerVerificationStatus::PENDING,
|
'action' => OwnerVerificationAction::DELETE,
|
||||||
'subject_type' => Product::class,
|
'status' => OwnerVerificationStatus::PENDING,
|
||||||
'subject_id' => $product->id,
|
'subject_type' => Product::class,
|
||||||
'submitted_by_id' => $user->id,
|
'subject_id' => $product->id,
|
||||||
'payload' => [
|
'submitted_by_id' => $user->id,
|
||||||
'old' => $this->snapshotProduct($product),
|
'payload' => [
|
||||||
'new' => null,
|
'old' => $this->snapshotProduct($product),
|
||||||
],
|
'new' => null,
|
||||||
]);
|
],
|
||||||
} catch (\Throwable $e) {
|
]);
|
||||||
Log::error('Gagal mengajukan penghapusan produk: '.$e->getMessage(), [
|
},
|
||||||
'trace' => $e->getTraceAsString(),
|
'Gagal mengajukan penghapusan produk',
|
||||||
]);
|
);
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->notifyForPendingRequest(
|
$this->notifyForPendingRequest(
|
||||||
$user,
|
$user,
|
||||||
@ -321,33 +299,28 @@ public function toggleStatus(Product $product, array $validated, User $user): vo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
$this->runInTransaction(
|
||||||
OwnerVerificationRequest::create([
|
function () use ($product, $validated, $user): void {
|
||||||
'action' => OwnerVerificationAction::TOGGLE_STATUS,
|
OwnerVerificationRequest::create([
|
||||||
'status' => OwnerVerificationStatus::PENDING,
|
'action' => OwnerVerificationAction::TOGGLE_STATUS,
|
||||||
'subject_type' => Product::class,
|
'status' => OwnerVerificationStatus::PENDING,
|
||||||
'subject_id' => $product->id,
|
'subject_type' => Product::class,
|
||||||
'submitted_by_id' => $user->id,
|
'subject_id' => $product->id,
|
||||||
'payload' => [
|
'submitted_by_id' => $user->id,
|
||||||
'old' => [
|
'payload' => [
|
||||||
'name' => $product->name,
|
'old' => [
|
||||||
'is_active' => $product->is_active,
|
'name' => $product->name,
|
||||||
|
'is_active' => $product->is_active,
|
||||||
|
],
|
||||||
|
'new' => [
|
||||||
|
'name' => $product->name,
|
||||||
|
'is_active' => (bool) $validated['is_active'],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'new' => [
|
]);
|
||||||
'name' => $product->name,
|
},
|
||||||
'is_active' => (bool) $validated['is_active'],
|
'Gagal mengajukan perubahan status produk',
|
||||||
],
|
);
|
||||||
],
|
|
||||||
]);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::error('Gagal mengajukan perubahan status produk: '.$e->getMessage(), [
|
|
||||||
'trace' => $e->getTraceAsString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$statusLabel = $validated['is_active'] ? 'aktif' : 'nonaktif';
|
$statusLabel = $validated['is_active'] ? 'aktif' : 'nonaktif';
|
||||||
|
|
||||||
@ -517,14 +490,17 @@ public function applyDelete(OwnerVerificationRequest $verificationRequest): void
|
|||||||
|
|
||||||
private function applyDeleteSubject(Product $product): void
|
private function applyDeleteSubject(Product $product): void
|
||||||
{
|
{
|
||||||
DB::transaction(function () use ($product): void {
|
$this->runInTransaction(
|
||||||
$product->variants()->each(function (ProductVariant $variant): void {
|
function () use ($product): void {
|
||||||
$variant->clearMediaCollection('images');
|
$product->variants()->each(function (ProductVariant $variant): void {
|
||||||
});
|
$variant->clearMediaCollection('images');
|
||||||
$product->variants()->delete();
|
});
|
||||||
$product->categories()->detach();
|
$product->variants()->delete();
|
||||||
$product->delete();
|
$product->categories()->detach();
|
||||||
});
|
$product->delete();
|
||||||
|
},
|
||||||
|
'Gagal menghapus produk',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyToggleStatus(OwnerVerificationRequest $verificationRequest): void
|
public function applyToggleStatus(OwnerVerificationRequest $verificationRequest): void
|
||||||
@ -574,14 +550,17 @@ private function rejectCreate(OwnerVerificationRequest $verificationRequest): vo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::transaction(function () use ($product): void {
|
$this->runInTransaction(
|
||||||
$product->variants()->each(function (ProductVariant $variant): void {
|
function () use ($product): void {
|
||||||
$variant->clearMediaCollection('images');
|
$product->variants()->each(function (ProductVariant $variant): void {
|
||||||
});
|
$variant->clearMediaCollection('images');
|
||||||
$product->variants()->delete();
|
});
|
||||||
$product->categories()->detach();
|
$product->variants()->delete();
|
||||||
$product->delete();
|
$product->categories()->detach();
|
||||||
});
|
$product->delete();
|
||||||
|
},
|
||||||
|
'Gagal menolak produk',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function rollbackUpdate(OwnerVerificationRequest $verificationRequest): void
|
private function rollbackUpdate(OwnerVerificationRequest $verificationRequest): void
|
||||||
|
|||||||
23
database/factories/ProductPriceFactory.php
Normal file
23
database/factories/ProductPriceFactory.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Enums\PriceType;
|
||||||
|
use App\Models\ProductPrice;
|
||||||
|
use App\Models\ProductVariant;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<ProductPrice>
|
||||||
|
*/
|
||||||
|
class ProductPriceFactory extends Factory
|
||||||
|
{
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'variant_id' => ProductVariant::factory(),
|
||||||
|
'type' => fake()->randomElement(PriceType::cases()),
|
||||||
|
'price' => fake()->numberBetween(50000, 500000),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@ public function definition(): array
|
|||||||
'product_id' => Product::factory(),
|
'product_id' => Product::factory(),
|
||||||
'name' => fake()->randomElement(['S', 'M', 'L', 'XL', 'All Size']),
|
'name' => fake()->randomElement(['S', 'M', 'L', 'XL', 'All Size']),
|
||||||
'stock' => fake()->numberBetween(0, 100),
|
'stock' => fake()->numberBetween(0, 100),
|
||||||
|
'retail_stock' => fake()->numberBetween(0, 50),
|
||||||
'reject_stock' => fake()->numberBetween(0, 20),
|
'reject_stock' => fake()->numberBetween(0, 20),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,6 +95,8 @@
|
|||||||
Route::prefix('products')->name('products.')
|
Route::prefix('products')->name('products.')
|
||||||
->middleware('permission:'.Permission::PRODUCTS_VIEW->value)
|
->middleware('permission:'.Permission::PRODUCTS_VIEW->value)
|
||||||
->group(function () {
|
->group(function () {
|
||||||
|
Route::get('/', [ProductController::class, 'index'])->name('index');
|
||||||
|
|
||||||
Route::get('create', [ProductController::class, 'create'])
|
Route::get('create', [ProductController::class, 'create'])
|
||||||
->middleware('permission:'.Permission::PRODUCTS_CREATE->value)
|
->middleware('permission:'.Permission::PRODUCTS_CREATE->value)
|
||||||
->name('create');
|
->name('create');
|
||||||
@ -117,13 +119,6 @@
|
|||||||
])
|
])
|
||||||
->name('update');
|
->name('update');
|
||||||
|
|
||||||
Route::patch('{product}/toggle-status', [ProductController::class, 'toggleStatus'])
|
|
||||||
->middleware([
|
|
||||||
'permission:'.Permission::PRODUCTS_TOGGLE_STATUS->value,
|
|
||||||
'no_pending_owner_verification:product',
|
|
||||||
])
|
|
||||||
->name('toggle_status');
|
|
||||||
|
|
||||||
Route::delete('{product}', [ProductController::class, 'destroy'])
|
Route::delete('{product}', [ProductController::class, 'destroy'])
|
||||||
->middleware([
|
->middleware([
|
||||||
'permission:'.Permission::PRODUCTS_DELETE->value,
|
'permission:'.Permission::PRODUCTS_DELETE->value,
|
||||||
@ -131,7 +126,12 @@
|
|||||||
])
|
])
|
||||||
->name('destroy');
|
->name('destroy');
|
||||||
|
|
||||||
Route::get('/', [ProductController::class, 'index'])->name('index');
|
Route::patch('{product}/toggle-status', [ProductController::class, 'toggleStatus'])
|
||||||
|
->middleware([
|
||||||
|
'permission:'.Permission::PRODUCTS_TOGGLE_STATUS->value,
|
||||||
|
'no_pending_owner_verification:product',
|
||||||
|
])
|
||||||
|
->name('toggle_status');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::prefix('raw-materials')->name('raw_materials.')
|
Route::prefix('raw-materials')->name('raw_materials.')
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
@ -72,10 +73,42 @@ function createProductVerifierUser(): User
|
|||||||
|
|
||||||
function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
||||||
{
|
{
|
||||||
|
$disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public');
|
||||||
|
$key = 'products/'.Str::uuid().".jpg";
|
||||||
|
$imageContent = UploadedFile::fake()->image("{$name}.jpg", 100, 100)->get();
|
||||||
|
Storage::disk($disk)->put($key, $imageContent);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'stock' => $stock,
|
'stock' => $stock,
|
||||||
'images' => [UploadedFile::fake()->image("{$name}.jpg", 100, 100)],
|
'retail_stock' => 0,
|
||||||
|
's3_keys' => [$key],
|
||||||
|
'prices' => defaultPrices(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultPrices(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'distributor' => 100000,
|
||||||
|
'agent' => 110000,
|
||||||
|
'sub_agent' => 120000,
|
||||||
|
'grosir' => 130000,
|
||||||
|
'retail' => 150000,
|
||||||
|
'tiktok' => 160000,
|
||||||
|
'shopee' => 165000,
|
||||||
|
'harga_modal' => 80000,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock = 20): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $id,
|
||||||
|
'name' => $name,
|
||||||
|
'stock' => $stock,
|
||||||
|
'retail_stock' => 0,
|
||||||
|
'prices' => defaultPrices(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -468,7 +501,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
|||||||
'name' => 'Nama Diubah',
|
'name' => 'Nama Diubah',
|
||||||
'category_ids' => [$category->id],
|
'category_ids' => [$category->id],
|
||||||
'variants' => [
|
'variants' => [
|
||||||
['id' => $variant->id, 'name' => $variant->name, 'stock' => $variant->stock],
|
variantUpdateData($variant->id, $variant->name, $variant->stock),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -494,7 +527,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
|||||||
'description' => 'Deskripsi baru',
|
'description' => 'Deskripsi baru',
|
||||||
'category_ids' => [$category->id],
|
'category_ids' => [$category->id],
|
||||||
'variants' => [
|
'variants' => [
|
||||||
['id' => $variant->id, 'name' => 'New Variant', 'stock' => 20],
|
variantUpdateData($variant->id, 'New Variant', 20),
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
->assertRedirect(route('admin.master.products.index'));
|
->assertRedirect(route('admin.master.products.index'));
|
||||||
@ -523,7 +556,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
|||||||
'description' => 'Deskripsi baru',
|
'description' => 'Deskripsi baru',
|
||||||
'category_ids' => [$category->id],
|
'category_ids' => [$category->id],
|
||||||
'variants' => [
|
'variants' => [
|
||||||
['id' => $variant->id, 'name' => 'New Variant', 'stock' => 20],
|
variantUpdateData($variant->id, 'New Variant', 20),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -540,7 +573,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
|||||||
$this->put(route('admin.master.products.update', $product), [
|
$this->put(route('admin.master.products.update', $product), [
|
||||||
'name' => 'Nama Baru',
|
'name' => 'Nama Baru',
|
||||||
'category_ids' => [$product->categories->first()->id],
|
'category_ids' => [$product->categories->first()->id],
|
||||||
'variants' => [['id' => $variant->id, 'name' => $variant->name, 'stock' => $variant->stock]],
|
'variants' => [variantUpdateData($variant->id, $variant->name, $variant->stock)],
|
||||||
])->assertRedirect(route('login'));
|
])->assertRedirect(route('login'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -554,7 +587,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
|||||||
->put(route('admin.master.products.update', $product), [
|
->put(route('admin.master.products.update', $product), [
|
||||||
'name' => 'Nama Baru',
|
'name' => 'Nama Baru',
|
||||||
'category_ids' => [$product->categories->first()->id],
|
'category_ids' => [$product->categories->first()->id],
|
||||||
'variants' => [['id' => $variant->id, 'name' => $variant->name, 'stock' => $variant->stock]],
|
'variants' => [variantUpdateData($variant->id, $variant->name, $variant->stock)],
|
||||||
])
|
])
|
||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
});
|
});
|
||||||
@ -571,7 +604,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
|||||||
'name' => '',
|
'name' => '',
|
||||||
'category_ids' => [$category->id],
|
'category_ids' => [$category->id],
|
||||||
'variants' => [
|
'variants' => [
|
||||||
['id' => $variant->id, 'name' => $variant->name, 'stock' => $variant->stock],
|
variantUpdateData($variant->id, $variant->name, $variant->stock),
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
->assertSessionHasErrors('name');
|
->assertSessionHasErrors('name');
|
||||||
@ -588,7 +621,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
|||||||
'name' => 'Nama Produk',
|
'name' => 'Nama Produk',
|
||||||
'category_ids' => [],
|
'category_ids' => [],
|
||||||
'variants' => [
|
'variants' => [
|
||||||
['id' => $variant->id, 'name' => $variant->name, 'stock' => $variant->stock],
|
variantUpdateData($variant->id, $variant->name, $variant->stock),
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
->assertSessionHasErrors('category_ids');
|
->assertSessionHasErrors('category_ids');
|
||||||
@ -607,7 +640,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
|||||||
'name' => 'Updated Product',
|
'name' => 'Updated Product',
|
||||||
'category_ids' => [$category->id],
|
'category_ids' => [$category->id],
|
||||||
'variants' => [
|
'variants' => [
|
||||||
['id' => $variantToKeep->id, 'name' => 'Kept Variant', 'stock' => 5],
|
variantUpdateData($variantToKeep->id, 'Kept Variant', 5),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -832,4 +865,234 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
|||||||
|
|
||||||
expect($product->fresh()->is_active)->toBeFalse();
|
expect($product->fresh()->is_active)->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('product has total_stock_formatted accessor', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 10]);
|
||||||
|
ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 20]);
|
||||||
|
|
||||||
|
expect($product->fresh()->total_stock_formatted)->toBe('30');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('product has slug auto-generated', function () {
|
||||||
|
$product = Product::create(['name' => 'Test Product Name']);
|
||||||
|
|
||||||
|
expect($product->slug)->toBe('test-product-name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── ProductVariant Model ──────────────────────────────────
|
||||||
|
|
||||||
|
describe('ProductVariant Model', function () {
|
||||||
|
test('variant uses soft deletes', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
||||||
|
|
||||||
|
$variant->delete();
|
||||||
|
|
||||||
|
expect($variant->trashed())->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('variant has stock cast to integer', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 50]);
|
||||||
|
|
||||||
|
expect($variant->stock)->toBeInt();
|
||||||
|
expect($variant->stock)->toBe(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('variant has formatted stock accessor', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 1500]);
|
||||||
|
|
||||||
|
expect($variant->stock_formatted)->toBe('1.500');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('variant belongs to product', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
||||||
|
|
||||||
|
expect($variant->product)->not->toBeNull();
|
||||||
|
expect($variant->product->id)->toBe($product->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('variant can have prices', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
||||||
|
|
||||||
|
\App\Models\ProductPrice::factory()->count(3)->create(['variant_id' => $variant->id]);
|
||||||
|
|
||||||
|
expect($variant->fresh()->prices)->toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('variant has minStock constant', function () {
|
||||||
|
expect(ProductVariant::minStock())->toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── ProductPrice Model ────────────────────────────────────
|
||||||
|
|
||||||
|
describe('ProductPrice Model', function () {
|
||||||
|
test('price has price cast to integer', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
||||||
|
$price = \App\Models\ProductPrice::factory()->create(['variant_id' => $variant->id, 'price' => 150000]);
|
||||||
|
|
||||||
|
expect($price->price)->toBeInt();
|
||||||
|
expect($price->price)->toBe(150000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('price has formatted price accessor', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
||||||
|
$price = \App\Models\ProductPrice::factory()->create(['variant_id' => $variant->id, 'price' => 1500000]);
|
||||||
|
|
||||||
|
expect($price->price_formatted)->toBe('Rp 1.500.000');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('price has price_input accessor', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
||||||
|
$price = \App\Models\ProductPrice::factory()->create(['variant_id' => $variant->id, 'price' => 200000]);
|
||||||
|
|
||||||
|
expect($price->price_input)->toBe('200000');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('price belongs to variant', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
||||||
|
$price = \App\Models\ProductPrice::factory()->create(['variant_id' => $variant->id]);
|
||||||
|
|
||||||
|
expect($price->variant)->not->toBeNull();
|
||||||
|
expect($price->variant->id)->toBe($variant->id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Owner Direct Actions ──────────────────────────────────
|
||||||
|
|
||||||
|
describe('Owner Direct Actions', function () {
|
||||||
|
test('owner can create product directly without verification', function () {
|
||||||
|
$owner = createProductUserWithPermission(
|
||||||
|
PermissionEnum::PRODUCTS_VIEW,
|
||||||
|
PermissionEnum::PRODUCTS_CREATE,
|
||||||
|
PermissionEnum::OWNER_VERIFICATIONS_VERIFY,
|
||||||
|
);
|
||||||
|
|
||||||
|
$category = Category::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($owner)
|
||||||
|
->post(route('admin.master.products.store'), [
|
||||||
|
'name' => 'Produk Owner',
|
||||||
|
'description' => 'Deskripsi',
|
||||||
|
'category_ids' => [$category->id],
|
||||||
|
'variants' => [
|
||||||
|
variantWithImage('All Size', 10),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertRedirect(route('admin.master.products.index'));
|
||||||
|
|
||||||
|
$product = Product::where('name', 'Produk Owner')->first();
|
||||||
|
expect($product)->not->toBeNull();
|
||||||
|
expect($product->is_active)->toBeTrue();
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('owner_verification_requests', [
|
||||||
|
'subject_id' => $product->id,
|
||||||
|
'subject_type' => Product::class,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('owner can update product directly without verification', function () {
|
||||||
|
$owner = createProductUserWithPermission(
|
||||||
|
PermissionEnum::PRODUCTS_VIEW,
|
||||||
|
PermissionEnum::PRODUCTS_UPDATE,
|
||||||
|
PermissionEnum::OWNER_VERIFICATIONS_VERIFY,
|
||||||
|
);
|
||||||
|
|
||||||
|
$product = createProductWithVariants();
|
||||||
|
$category = $product->categories->first();
|
||||||
|
$variant = $product->variants->first();
|
||||||
|
|
||||||
|
$this->actingAs($owner)
|
||||||
|
->put(route('admin.master.products.update', $product), [
|
||||||
|
'name' => 'Produk Updated',
|
||||||
|
'description' => 'Deskripsi baru',
|
||||||
|
'category_ids' => [$category->id],
|
||||||
|
'variants' => [
|
||||||
|
variantUpdateData($variant->id, 'Updated Variant', 25),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertRedirect(route('admin.master.products.index'));
|
||||||
|
|
||||||
|
expect($product->fresh()->name)->toBe('Produk Updated');
|
||||||
|
expect($variant->fresh()->name)->toBe('Updated Variant');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('owner can delete product directly without verification', function () {
|
||||||
|
$owner = createProductUserWithPermission(
|
||||||
|
PermissionEnum::PRODUCTS_VIEW,
|
||||||
|
PermissionEnum::PRODUCTS_DELETE,
|
||||||
|
PermissionEnum::OWNER_VERIFICATIONS_VERIFY,
|
||||||
|
);
|
||||||
|
|
||||||
|
$product = createProductWithVariants();
|
||||||
|
|
||||||
|
$this->actingAs($owner)
|
||||||
|
->delete(route('admin.master.products.destroy', $product))
|
||||||
|
->assertRedirect(route('admin.master.products.index'));
|
||||||
|
|
||||||
|
$this->assertSoftDeleted('products', ['id' => $product->id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('owner can toggle product status directly without verification', function () {
|
||||||
|
$owner = createProductUserWithPermission(
|
||||||
|
PermissionEnum::PRODUCTS_VIEW,
|
||||||
|
PermissionEnum::PRODUCTS_TOGGLE_STATUS,
|
||||||
|
PermissionEnum::OWNER_VERIFICATIONS_VERIFY,
|
||||||
|
);
|
||||||
|
|
||||||
|
$product = createProductWithVariants();
|
||||||
|
|
||||||
|
$this->actingAs($owner)
|
||||||
|
->patch(route('admin.master.products.toggle_status', $product), [
|
||||||
|
'is_active' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($product->fresh()->is_active)->toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Reject Update Request ─────────────────────────────────
|
||||||
|
|
||||||
|
describe('Reject Update Request', function () {
|
||||||
|
test('rejecting update request rolls back to old values', function () {
|
||||||
|
$user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_UPDATE);
|
||||||
|
$verifier = createProductVerifierUser();
|
||||||
|
|
||||||
|
$product = createProductWithVariants();
|
||||||
|
$category = $product->categories->first();
|
||||||
|
$variant = $product->variants->first();
|
||||||
|
$originalName = $product->name;
|
||||||
|
$originalVariantName = $variant->name;
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->put(route('admin.master.products.update', $product), [
|
||||||
|
'name' => 'Nama Diubah',
|
||||||
|
'description' => 'Deskripsi baru',
|
||||||
|
'category_ids' => [$category->id],
|
||||||
|
'variants' => [
|
||||||
|
variantUpdateData($variant->id, 'New Variant', 20),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$verificationRequest = OwnerVerificationRequest::query()->pending()->latest()->firstOrFail();
|
||||||
|
|
||||||
|
$this->actingAs($verifier)
|
||||||
|
->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest), [
|
||||||
|
'reason' => 'Tidak sesuai standar',
|
||||||
|
])
|
||||||
|
->assertRedirect();
|
||||||
|
|
||||||
|
expect($product->fresh()->name)->toBe($originalName);
|
||||||
|
expect($variant->fresh()->name)->toBe($originalVariantName);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user