feat: implement transaction management in OrderService for create, update, delete, and status transition operations; refactor error handling for improved clarity; add comprehensive tests for order management functionalities to ensure proper permission checks and validation
This commit is contained in:
parent
8e306c8f73
commit
b52b9ce08c
@ -13,6 +13,7 @@
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\User;
|
||||
use App\Services\Concerns\RunsInTransaction;
|
||||
use App\Services\Finance\CashService;
|
||||
use App\Services\Media\MediaService;
|
||||
use App\Services\System\PushNotificationService;
|
||||
@ -23,11 +24,12 @@
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class OrderService
|
||||
{
|
||||
use RunsInTransaction;
|
||||
|
||||
private const MAX_PHOTOS = 1;
|
||||
|
||||
public function __construct(
|
||||
@ -388,16 +390,16 @@ public function resyncDraftPrices(User $user, string $priceTypeValue): array
|
||||
|
||||
public function create(array $validated, User $user): Order
|
||||
{
|
||||
try {
|
||||
// Force cashier settings
|
||||
if ($user->hasRole('cashier')) {
|
||||
$validated['channel'] = 'store';
|
||||
$validated['price_type'] = 'retail';
|
||||
$validated['payment_type'] = 'cash';
|
||||
unset($validated['customer_id']);
|
||||
}
|
||||
// Force cashier settings
|
||||
if ($user->hasRole('cashier')) {
|
||||
$validated['channel'] = 'store';
|
||||
$validated['price_type'] = 'retail';
|
||||
$validated['payment_type'] = 'cash';
|
||||
unset($validated['customer_id']);
|
||||
}
|
||||
|
||||
$order = DB::transaction(function () use ($validated, $user): Order {
|
||||
$order = $this->runInTransaction(
|
||||
function () use ($validated, $user): Order {
|
||||
$priceType = $this->resolvePriceType($validated['channel'], $validated['price_type']);
|
||||
|
||||
$draftItems = $this->draftItemsQuery($user)
|
||||
@ -482,18 +484,9 @@ public function create(array $validated, User $user): Order
|
||||
}
|
||||
|
||||
return $order;
|
||||
});
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gagal membuat pesanan: '.$e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
||||
]);
|
||||
}
|
||||
},
|
||||
'Gagal membuat pesanan',
|
||||
);
|
||||
|
||||
$this->pushNotificationService->sendToRoles(
|
||||
'📦 Pesanan Baru',
|
||||
@ -509,8 +502,8 @@ public function update(Order $order, array $validated): void
|
||||
{
|
||||
$order->ensureEditable();
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($order, $validated): void {
|
||||
$this->runInTransaction(
|
||||
function () use ($order, $validated): void {
|
||||
$order->load('items');
|
||||
|
||||
foreach ($order->items as $item) {
|
||||
@ -580,18 +573,9 @@ public function update(Order $order, array $validated): void
|
||||
$orderItem = $order->items()->create($itemData);
|
||||
$this->decrementStock($orderItem);
|
||||
}
|
||||
});
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gagal memperbarui pesanan: '.$e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
||||
]);
|
||||
}
|
||||
},
|
||||
'Gagal memperbarui pesanan',
|
||||
);
|
||||
|
||||
$this->pushNotificationService->sendToRoles(
|
||||
'✏️ Pesanan Diperbarui',
|
||||
@ -606,8 +590,8 @@ public function delete(Order $order): void
|
||||
$orderNumber = $order->order_number;
|
||||
$totalAmount = $order->total_amount;
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($order): void {
|
||||
$this->runInTransaction(
|
||||
function () use ($order): void {
|
||||
$order->load('items');
|
||||
|
||||
if ($order->status->isEditable()) {
|
||||
@ -622,18 +606,9 @@ public function delete(Order $order): void
|
||||
|
||||
$order->items()->delete();
|
||||
$order->delete();
|
||||
});
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gagal menghapus pesanan: '.$e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
||||
]);
|
||||
}
|
||||
},
|
||||
'Gagal menghapus pesanan',
|
||||
);
|
||||
|
||||
$this->pushNotificationService->sendToRoles(
|
||||
'🗑️ Pesanan Dihapus',
|
||||
@ -651,8 +626,8 @@ public function transitionStatus(Order $order, OrderStatus $status): void
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($order, $status): void {
|
||||
$this->runInTransaction(
|
||||
function () use ($order, $status): void {
|
||||
if ($status === OrderStatus::CANCELLED) {
|
||||
$order->load('items');
|
||||
|
||||
@ -668,18 +643,9 @@ public function transitionStatus(Order $order, OrderStatus $status): void
|
||||
|
||||
$order->status = $status;
|
||||
$order->save();
|
||||
});
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gagal mengubah status pesanan: '.$e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
|
||||
]);
|
||||
}
|
||||
},
|
||||
'Gagal mengubah status pesanan',
|
||||
);
|
||||
|
||||
$this->pushNotificationService->sendToRoles(
|
||||
'📦 Status Pesanan Diubah',
|
||||
|
||||
585
tests/Feature/Admin/Manage/OrderTest.php
Normal file
585
tests/Feature/Admin/Manage/OrderTest.php
Normal file
@ -0,0 +1,585 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\OrderChannel;
|
||||
use App\Enums\OrderStatus;
|
||||
use App\Enums\PaymentType;
|
||||
use App\Enums\Permission as PermissionEnum;
|
||||
use App\Enums\PriceType;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductPrice;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\CashAccountSeeder;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
User::factory()->create();
|
||||
$this->seed(CashAccountSeeder::class);
|
||||
Storage::fake('public');
|
||||
});
|
||||
|
||||
// ─── Helper ───────────────────────────────────────────────
|
||||
|
||||
function createOrderUserWithPermission(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 createOrderWithItems(?User $user = null): Order
|
||||
{
|
||||
$user ??= User::factory()->create();
|
||||
|
||||
$order = Order::factory()->create([
|
||||
'created_by_id' => $user->id,
|
||||
'channel' => OrderChannel::STORE->value,
|
||||
'price_type' => PriceType::RETAIL->value,
|
||||
'payment_type' => PaymentType::CASH->value,
|
||||
]);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->create([
|
||||
'product_id' => $product->id,
|
||||
'stock' => 100,
|
||||
]);
|
||||
|
||||
OrderItem::factory()->create([
|
||||
'order_id' => $order->id,
|
||||
'product_variant_id' => $variant->id,
|
||||
'quantity' => 5,
|
||||
'unit_price' => 50000,
|
||||
'subtotal' => 250000,
|
||||
]);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
function setupOrderDraftItems(User $user): ProductVariant
|
||||
{
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->create([
|
||||
'product_id' => $product->id,
|
||||
'stock' => 100,
|
||||
]);
|
||||
|
||||
// Create product price so CuttingResultPriceResolver can resolve it
|
||||
ProductPrice::factory()->create([
|
||||
'variant_id' => $variant->id,
|
||||
'type' => PriceType::RETAIL,
|
||||
'price' => 50000,
|
||||
]);
|
||||
|
||||
// Create draft item via API
|
||||
test()->actingAs($user)
|
||||
->postJson(route('admin.manage.orders.draft_items.store'), [
|
||||
'product_variant_id' => $variant->id,
|
||||
'price_type' => PriceType::RETAIL->value,
|
||||
'stock_quality' => 'good',
|
||||
'quantity' => 5,
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
return $variant;
|
||||
}
|
||||
|
||||
// ─── Index ────────────────────────────────────────────────
|
||||
|
||||
describe('Order Index', function () {
|
||||
test('authenticated user with permission can view order index', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.orders.index'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('guest is redirected to login', function () {
|
||||
$this->get(route('admin.manage.orders.index'))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without permission is forbidden', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.orders.index'))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('index displays orders', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW);
|
||||
|
||||
createOrderWithItems($user);
|
||||
createOrderWithItems($user);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.orders.index'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('index can search orders by order number', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW);
|
||||
|
||||
$order = Order::factory()->create([
|
||||
'created_by_id' => $user->id,
|
||||
'order_number' => 'ORD-20260704-TEST',
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.orders.index', ['search' => 'TEST']))
|
||||
->assertOk();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Create ───────────────────────────────────────────────
|
||||
|
||||
describe('Order Create', function () {
|
||||
test('authenticated user with permission can view create form', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CREATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.orders.create'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('guest is redirected to login', function () {
|
||||
$this->get(route('admin.manage.orders.create'))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without create permission is forbidden', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.orders.create'))
|
||||
->assertForbidden();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Store ────────────────────────────────────────────────
|
||||
|
||||
describe('Order Store', function () {
|
||||
test('authenticated user with permission can create an order', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CREATE);
|
||||
|
||||
$customer = Customer::factory()->create();
|
||||
setupOrderDraftItems($user);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.orders.store'), [
|
||||
'customer_id' => $customer->id,
|
||||
'channel' => OrderChannel::STORE->value,
|
||||
'price_type' => PriceType::RETAIL->value,
|
||||
'payment_type' => PaymentType::CASH->value,
|
||||
'discount' => 0,
|
||||
])
|
||||
->assertRedirect(route('admin.manage.orders.index'));
|
||||
|
||||
$order = Order::query()->latest()->first();
|
||||
expect($order)->not->toBeNull();
|
||||
expect($order->status)->toBe(OrderStatus::PENDING);
|
||||
expect($order->items)->toHaveCount(1);
|
||||
});
|
||||
|
||||
test('guest cannot create an order', function () {
|
||||
$this->post(route('admin.manage.orders.store'), [
|
||||
'channel' => OrderChannel::STORE->value,
|
||||
'price_type' => PriceType::RETAIL->value,
|
||||
'payment_type' => PaymentType::CASH->value,
|
||||
])->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without create permission is forbidden', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.orders.store'), [
|
||||
'channel' => OrderChannel::STORE->value,
|
||||
'price_type' => PriceType::RETAIL->value,
|
||||
'payment_type' => PaymentType::CASH->value,
|
||||
])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('store fails without draft items', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CREATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.orders.store'), [
|
||||
'channel' => OrderChannel::STORE->value,
|
||||
'price_type' => PriceType::RETAIL->value,
|
||||
'payment_type' => PaymentType::CASH->value,
|
||||
'discount' => 0,
|
||||
])
|
||||
->assertSessionHasErrors('items');
|
||||
});
|
||||
|
||||
test('creating order decrements stock', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CREATE);
|
||||
|
||||
$variant = setupOrderDraftItems($user);
|
||||
$initialStock = $variant->stock;
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.orders.store'), [
|
||||
'channel' => OrderChannel::STORE->value,
|
||||
'price_type' => PriceType::RETAIL->value,
|
||||
'payment_type' => PaymentType::CASH->value,
|
||||
'discount' => 0,
|
||||
]);
|
||||
|
||||
expect($variant->fresh()->stock)->toBe($initialStock - 5);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Show ─────────────────────────────────────────────────
|
||||
|
||||
describe('Order Show', function () {
|
||||
test('authenticated user with permission can view order details', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW);
|
||||
|
||||
$order = createOrderWithItems($user);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.orders.show', $order))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('guest is redirected to login', function () {
|
||||
$order = createOrderWithItems();
|
||||
|
||||
$this->get(route('admin.manage.orders.show', $order))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Edit ─────────────────────────────────────────────────
|
||||
|
||||
describe('Order Edit', function () {
|
||||
test('authenticated user with permission can view edit form', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_UPDATE);
|
||||
|
||||
$order = createOrderWithItems($user);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.orders.edit', $order))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('guest is redirected to login', function () {
|
||||
$order = createOrderWithItems();
|
||||
|
||||
$this->get(route('admin.manage.orders.edit', $order))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without update permission is forbidden', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW);
|
||||
|
||||
$order = createOrderWithItems($user);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.orders.edit', $order))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('completed order cannot be edited', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_UPDATE);
|
||||
|
||||
$order = Order::factory()->completed()->create(['created_by_id' => $user->id]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.orders.edit', $order))
|
||||
->assertRedirect(route('admin.manage.orders.index'));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Destroy ──────────────────────────────────────────────
|
||||
|
||||
describe('Order Destroy', function () {
|
||||
test('authenticated user with permission can delete an order', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_DELETE);
|
||||
|
||||
$order = createOrderWithItems($user);
|
||||
|
||||
$this->actingAs($user)
|
||||
->delete(route('admin.manage.orders.destroy', $order))
|
||||
->assertRedirect(route('admin.manage.orders.index'));
|
||||
|
||||
$this->assertSoftDeleted('orders', ['id' => $order->id]);
|
||||
});
|
||||
|
||||
test('guest cannot delete an order', function () {
|
||||
$order = createOrderWithItems();
|
||||
|
||||
$this->delete(route('admin.manage.orders.destroy', $order))
|
||||
->assertRedirect(route('login'));
|
||||
|
||||
$this->assertNotSoftDeleted('orders', ['id' => $order->id]);
|
||||
});
|
||||
|
||||
test('user without delete permission is forbidden', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW);
|
||||
|
||||
$order = createOrderWithItems($user);
|
||||
|
||||
$this->actingAs($user)
|
||||
->delete(route('admin.manage.orders.destroy', $order))
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertNotSoftDeleted('orders', ['id' => $order->id]);
|
||||
});
|
||||
|
||||
test('deleting order increments stock back', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_DELETE);
|
||||
|
||||
$order = createOrderWithItems($user);
|
||||
$item = $order->items->first();
|
||||
$variant = $item->productVariant;
|
||||
$stockBeforeDelete = $variant->fresh()->stock;
|
||||
|
||||
$this->actingAs($user)
|
||||
->delete(route('admin.manage.orders.destroy', $order));
|
||||
|
||||
expect($variant->fresh()->stock)->toBe($stockBeforeDelete + $item->quantity);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Status Transition ────────────────────────────────────
|
||||
|
||||
describe('Order Status Transition', function () {
|
||||
test('authenticated user can send an order', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_SEND);
|
||||
|
||||
$order = Order::factory()->create([
|
||||
'created_by_id' => $user->id,
|
||||
'status' => OrderStatus::PENDING->value,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.orders.transition_status', $order), [
|
||||
'status' => OrderStatus::PROCESSING->value,
|
||||
])
|
||||
->assertRedirect(route('admin.manage.orders.index'));
|
||||
|
||||
expect($order->fresh()->status)->toBe(OrderStatus::PROCESSING);
|
||||
});
|
||||
|
||||
test('authenticated user can complete an order', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_COMPLETE);
|
||||
|
||||
$order = Order::factory()->create([
|
||||
'created_by_id' => $user->id,
|
||||
'status' => OrderStatus::PROCESSING->value,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.orders.transition_status', $order), [
|
||||
'status' => OrderStatus::COMPLETED->value,
|
||||
]);
|
||||
|
||||
expect($order->fresh()->status)->toBe(OrderStatus::COMPLETED);
|
||||
});
|
||||
|
||||
test('authenticated user can cancel an order', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CANCEL);
|
||||
|
||||
$order = Order::factory()->create([
|
||||
'created_by_id' => $user->id,
|
||||
'status' => OrderStatus::PENDING->value,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.orders.transition_status', $order), [
|
||||
'status' => OrderStatus::CANCELLED->value,
|
||||
]);
|
||||
|
||||
expect($order->fresh()->status)->toBe(OrderStatus::CANCELLED);
|
||||
});
|
||||
|
||||
test('guest cannot transition status', function () {
|
||||
$order = Order::factory()->create(['status' => OrderStatus::PENDING->value]);
|
||||
|
||||
$this->post(route('admin.manage.orders.transition_status', $order), [
|
||||
'status' => OrderStatus::PROCESSING->value,
|
||||
])->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('invalid status transition is rejected', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_COMPLETE);
|
||||
|
||||
$order = Order::factory()->create([
|
||||
'created_by_id' => $user->id,
|
||||
'status' => OrderStatus::PENDING->value,
|
||||
]);
|
||||
|
||||
// PENDING cannot go directly to COMPLETED
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.orders.transition_status', $order), [
|
||||
'status' => OrderStatus::COMPLETED->value,
|
||||
])
|
||||
->assertSessionHasErrors('status');
|
||||
});
|
||||
|
||||
test('cancelling order increments stock back', function () {
|
||||
$user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CANCEL);
|
||||
|
||||
$order = createOrderWithItems($user);
|
||||
$item = $order->items->first();
|
||||
$variant = $item->productVariant;
|
||||
$stockBeforeCancel = $variant->fresh()->stock;
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.orders.transition_status', $order), [
|
||||
'status' => OrderStatus::CANCELLED->value,
|
||||
]);
|
||||
|
||||
expect($variant->fresh()->stock)->toBe($stockBeforeCancel + $item->quantity);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Order Model ──────────────────────────────────────────
|
||||
|
||||
describe('Order Model', function () {
|
||||
test('order uses soft deletes', function () {
|
||||
$order = Order::factory()->create();
|
||||
|
||||
$order->delete();
|
||||
|
||||
expect($order->trashed())->toBeTrue();
|
||||
});
|
||||
|
||||
test('order has amounts cast to integer', function () {
|
||||
$order = Order::factory()->create([
|
||||
'subtotal' => 100000,
|
||||
'discount' => 5000,
|
||||
'total_amount' => 95000,
|
||||
]);
|
||||
|
||||
expect($order->subtotal)->toBeInt();
|
||||
expect($order->discount)->toBeInt();
|
||||
expect($order->total_amount)->toBeInt();
|
||||
});
|
||||
|
||||
test('order has formatted amount accessors', function () {
|
||||
$order = Order::factory()->create([
|
||||
'subtotal' => 100000,
|
||||
'discount' => 5000,
|
||||
'total_amount' => 95000,
|
||||
]);
|
||||
|
||||
expect($order->subtotal_formatted)->toBe('Rp 100.000');
|
||||
expect($order->discount_formatted)->toBe('Rp 5.000');
|
||||
expect($order->total_amount_formatted)->toBe('Rp 95.000');
|
||||
});
|
||||
|
||||
test('order has status cast to enum', function () {
|
||||
$order = Order::factory()->create(['status' => OrderStatus::PENDING->value]);
|
||||
|
||||
expect($order->status)->toBe(OrderStatus::PENDING);
|
||||
});
|
||||
|
||||
test('order has status label accessor', function () {
|
||||
$order = Order::factory()->create(['status' => OrderStatus::PENDING->value]);
|
||||
|
||||
expect($order->status_label)->toBe('Menunggu');
|
||||
});
|
||||
|
||||
test('order belongs to customer', function () {
|
||||
$customer = Customer::factory()->create();
|
||||
$order = Order::factory()->create(['customer_id' => $customer->id]);
|
||||
|
||||
expect($order->customer)->not->toBeNull();
|
||||
expect($order->customer->id)->toBe($customer->id);
|
||||
});
|
||||
|
||||
test('order belongs to created by user', function () {
|
||||
$user = User::factory()->create();
|
||||
$order = Order::factory()->create(['created_by_id' => $user->id]);
|
||||
|
||||
expect($order->createdBy)->not->toBeNull();
|
||||
expect($order->createdBy->id)->toBe($user->id);
|
||||
});
|
||||
|
||||
test('order can have items', function () {
|
||||
$order = Order::factory()->create();
|
||||
OrderItem::factory()->count(3)->create(['order_id' => $order->id]);
|
||||
|
||||
expect($order->fresh()->items)->toHaveCount(3);
|
||||
});
|
||||
|
||||
test('order status is editable for pending and processing', function () {
|
||||
$pending = Order::factory()->create(['status' => OrderStatus::PENDING->value]);
|
||||
$processing = Order::factory()->create(['status' => OrderStatus::PROCESSING->value]);
|
||||
|
||||
expect($pending->status->isEditable())->toBeTrue();
|
||||
expect($processing->status->isEditable())->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── OrderItem Model ──────────────────────────────────────
|
||||
|
||||
describe('OrderItem Model', function () {
|
||||
test('order item has amounts cast to integer', function () {
|
||||
$order = Order::factory()->create();
|
||||
$item = OrderItem::factory()->create([
|
||||
'order_id' => $order->id,
|
||||
'quantity' => 10,
|
||||
'unit_price' => 50000,
|
||||
'subtotal' => 500000,
|
||||
]);
|
||||
|
||||
expect($item->quantity)->toBeInt();
|
||||
expect($item->unit_price)->toBeInt();
|
||||
expect($item->subtotal)->toBeInt();
|
||||
});
|
||||
|
||||
test('order item has formatted amount accessors', function () {
|
||||
$order = Order::factory()->create();
|
||||
$item = OrderItem::factory()->create([
|
||||
'order_id' => $order->id,
|
||||
'quantity' => 10,
|
||||
'unit_price' => 50000,
|
||||
'subtotal' => 500000,
|
||||
]);
|
||||
|
||||
expect($item->quantity_formatted)->toBe('10 pcs');
|
||||
expect($item->unit_price_formatted)->toBe('Rp 50.000');
|
||||
expect($item->subtotal_formatted)->toBe('Rp 500.000');
|
||||
});
|
||||
|
||||
test('order item belongs to order', function () {
|
||||
$order = Order::factory()->create();
|
||||
$item = OrderItem::factory()->create(['order_id' => $order->id]);
|
||||
|
||||
expect($item->order)->not->toBeNull();
|
||||
expect($item->order->id)->toBe($order->id);
|
||||
});
|
||||
|
||||
test('order item uses soft deletes', function () {
|
||||
$order = Order::factory()->create();
|
||||
$item = OrderItem::factory()->create(['order_id' => $order->id]);
|
||||
|
||||
$item->delete();
|
||||
|
||||
expect($item->trashed())->toBeTrue();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user