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\ProductVariant;
|
||||
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 ProductService
|
||||
{
|
||||
use RunsInTransaction;
|
||||
|
||||
private const MAX_VARIANT_IMAGES = 5;
|
||||
|
||||
public function __construct(
|
||||
@ -119,8 +120,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 {
|
||||
$product = Product::create([
|
||||
'name' => $validated['name'],
|
||||
'description' => $validated['description'] ?? null,
|
||||
@ -161,18 +162,9 @@ public function create(array $validated, User $user): void
|
||||
],
|
||||
]);
|
||||
}
|
||||
});
|
||||
} catch (ValidationException $e) {
|
||||
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.',
|
||||
]);
|
||||
}
|
||||
},
|
||||
'Gagal membuat produk',
|
||||
);
|
||||
|
||||
if (! $isOwner) {
|
||||
$this->notifyForPendingRequest(
|
||||
@ -189,8 +181,8 @@ public function update(Product $product, array $validated, User $user): void
|
||||
{
|
||||
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($validated, $product, $user, $isOwner): void {
|
||||
$this->runInTransaction(
|
||||
function () use ($validated, $product, $user, $isOwner): void {
|
||||
if ($isOwner) {
|
||||
$payload = $this->enrichPayload($this->buildPayloadFromValidated($validated));
|
||||
$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);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (ValidationException $e) {
|
||||
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.',
|
||||
]);
|
||||
}
|
||||
},
|
||||
'Gagal memperbarui produk',
|
||||
);
|
||||
|
||||
if (! $isOwner) {
|
||||
$this->notifyForPendingRequest(
|
||||
@ -278,27 +261,22 @@ public function delete(Product $product, User $user): void
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
OwnerVerificationRequest::create([
|
||||
'action' => OwnerVerificationAction::DELETE,
|
||||
'status' => OwnerVerificationStatus::PENDING,
|
||||
'subject_type' => Product::class,
|
||||
'subject_id' => $product->id,
|
||||
'submitted_by_id' => $user->id,
|
||||
'payload' => [
|
||||
'old' => $this->snapshotProduct($product),
|
||||
'new' => null,
|
||||
],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gagal mengajukan penghapusan produk: '.$e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
||||
]);
|
||||
}
|
||||
$this->runInTransaction(
|
||||
function () use ($product, $user): void {
|
||||
OwnerVerificationRequest::create([
|
||||
'action' => OwnerVerificationAction::DELETE,
|
||||
'status' => OwnerVerificationStatus::PENDING,
|
||||
'subject_type' => Product::class,
|
||||
'subject_id' => $product->id,
|
||||
'submitted_by_id' => $user->id,
|
||||
'payload' => [
|
||||
'old' => $this->snapshotProduct($product),
|
||||
'new' => null,
|
||||
],
|
||||
]);
|
||||
},
|
||||
'Gagal mengajukan penghapusan produk',
|
||||
);
|
||||
|
||||
$this->notifyForPendingRequest(
|
||||
$user,
|
||||
@ -321,33 +299,28 @@ public function toggleStatus(Product $product, array $validated, User $user): vo
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
OwnerVerificationRequest::create([
|
||||
'action' => OwnerVerificationAction::TOGGLE_STATUS,
|
||||
'status' => OwnerVerificationStatus::PENDING,
|
||||
'subject_type' => Product::class,
|
||||
'subject_id' => $product->id,
|
||||
'submitted_by_id' => $user->id,
|
||||
'payload' => [
|
||||
'old' => [
|
||||
'name' => $product->name,
|
||||
'is_active' => $product->is_active,
|
||||
$this->runInTransaction(
|
||||
function () use ($product, $validated, $user): void {
|
||||
OwnerVerificationRequest::create([
|
||||
'action' => OwnerVerificationAction::TOGGLE_STATUS,
|
||||
'status' => OwnerVerificationStatus::PENDING,
|
||||
'subject_type' => Product::class,
|
||||
'subject_id' => $product->id,
|
||||
'submitted_by_id' => $user->id,
|
||||
'payload' => [
|
||||
'old' => [
|
||||
'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'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
} 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.',
|
||||
]);
|
||||
}
|
||||
]);
|
||||
},
|
||||
'Gagal mengajukan perubahan status produk',
|
||||
);
|
||||
|
||||
$statusLabel = $validated['is_active'] ? 'aktif' : 'nonaktif';
|
||||
|
||||
@ -517,14 +490,17 @@ public function applyDelete(OwnerVerificationRequest $verificationRequest): void
|
||||
|
||||
private function applyDeleteSubject(Product $product): void
|
||||
{
|
||||
DB::transaction(function () use ($product): void {
|
||||
$product->variants()->each(function (ProductVariant $variant): void {
|
||||
$variant->clearMediaCollection('images');
|
||||
});
|
||||
$product->variants()->delete();
|
||||
$product->categories()->detach();
|
||||
$product->delete();
|
||||
});
|
||||
$this->runInTransaction(
|
||||
function () use ($product): void {
|
||||
$product->variants()->each(function (ProductVariant $variant): void {
|
||||
$variant->clearMediaCollection('images');
|
||||
});
|
||||
$product->variants()->delete();
|
||||
$product->categories()->detach();
|
||||
$product->delete();
|
||||
},
|
||||
'Gagal menghapus produk',
|
||||
);
|
||||
}
|
||||
|
||||
public function applyToggleStatus(OwnerVerificationRequest $verificationRequest): void
|
||||
@ -574,14 +550,17 @@ private function rejectCreate(OwnerVerificationRequest $verificationRequest): vo
|
||||
return;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($product): void {
|
||||
$product->variants()->each(function (ProductVariant $variant): void {
|
||||
$variant->clearMediaCollection('images');
|
||||
});
|
||||
$product->variants()->delete();
|
||||
$product->categories()->detach();
|
||||
$product->delete();
|
||||
});
|
||||
$this->runInTransaction(
|
||||
function () use ($product): void {
|
||||
$product->variants()->each(function (ProductVariant $variant): void {
|
||||
$variant->clearMediaCollection('images');
|
||||
});
|
||||
$product->variants()->delete();
|
||||
$product->categories()->detach();
|
||||
$product->delete();
|
||||
},
|
||||
'Gagal menolak produk',
|
||||
);
|
||||
}
|
||||
|
||||
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(),
|
||||
'name' => fake()->randomElement(['S', 'M', 'L', 'XL', 'All Size']),
|
||||
'stock' => fake()->numberBetween(0, 100),
|
||||
'retail_stock' => fake()->numberBetween(0, 50),
|
||||
'reject_stock' => fake()->numberBetween(0, 20),
|
||||
];
|
||||
}
|
||||
|
||||
@ -95,6 +95,8 @@
|
||||
Route::prefix('products')->name('products.')
|
||||
->middleware('permission:'.Permission::PRODUCTS_VIEW->value)
|
||||
->group(function () {
|
||||
Route::get('/', [ProductController::class, 'index'])->name('index');
|
||||
|
||||
Route::get('create', [ProductController::class, 'create'])
|
||||
->middleware('permission:'.Permission::PRODUCTS_CREATE->value)
|
||||
->name('create');
|
||||
@ -117,13 +119,6 @@
|
||||
])
|
||||
->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'])
|
||||
->middleware([
|
||||
'permission:'.Permission::PRODUCTS_DELETE->value,
|
||||
@ -131,7 +126,12 @@
|
||||
])
|
||||
->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.')
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
@ -72,10 +73,42 @@ function createProductVerifierUser(): User
|
||||
|
||||
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 [
|
||||
'name' => $name,
|
||||
'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',
|
||||
'category_ids' => [$category->id],
|
||||
'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',
|
||||
'category_ids' => [$category->id],
|
||||
'variants' => [
|
||||
['id' => $variant->id, 'name' => 'New Variant', 'stock' => 20],
|
||||
variantUpdateData($variant->id, 'New Variant', 20),
|
||||
],
|
||||
])
|
||||
->assertRedirect(route('admin.master.products.index'));
|
||||
@ -523,7 +556,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
||||
'description' => 'Deskripsi baru',
|
||||
'category_ids' => [$category->id],
|
||||
'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), [
|
||||
'name' => 'Nama Baru',
|
||||
'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'));
|
||||
});
|
||||
|
||||
@ -554,7 +587,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
||||
->put(route('admin.master.products.update', $product), [
|
||||
'name' => 'Nama Baru',
|
||||
'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();
|
||||
});
|
||||
@ -571,7 +604,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
||||
'name' => '',
|
||||
'category_ids' => [$category->id],
|
||||
'variants' => [
|
||||
['id' => $variant->id, 'name' => $variant->name, 'stock' => $variant->stock],
|
||||
variantUpdateData($variant->id, $variant->name, $variant->stock),
|
||||
],
|
||||
])
|
||||
->assertSessionHasErrors('name');
|
||||
@ -588,7 +621,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
||||
'name' => 'Nama Produk',
|
||||
'category_ids' => [],
|
||||
'variants' => [
|
||||
['id' => $variant->id, 'name' => $variant->name, 'stock' => $variant->stock],
|
||||
variantUpdateData($variant->id, $variant->name, $variant->stock),
|
||||
],
|
||||
])
|
||||
->assertSessionHasErrors('category_ids');
|
||||
@ -607,7 +640,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
||||
'name' => 'Updated Product',
|
||||
'category_ids' => [$category->id],
|
||||
'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();
|
||||
});
|
||||
|
||||
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