Back to Course |
Laravel Web to Mobile API: Reuse Old Code with Services

Staff Orders API, Service and Tests

We are still working on orders. It is time to implement API endpoints to manage orders for staff users.


API Controller

Create API resources for Order Model with the staff scope. We want to separate these resources to avoid affecting responses when customers request their orders.

php artisan make:resource Api/V1/Staff/OrderResource
php artisan make:resource Api/V1/Staff/OrderCollection

Create a new API controller for staff members.

php artisan make:controller Api/V1/Staff/OrderController

Unlike app/Http/Controllers/Staff/OrderController.php, returning current and past orders in a single method, we separate them in the API. Applications consuming API won't have to group or filter them manually.

app/Http/Controllers/Api/V1/Staff/OrderController.php

namespace App\Http\Controllers\Api\V1\Staff;
 
use App\Http\Controllers\Controller;
use App\Http\Requests\Staff\UpdateOrderRequest;
use App\Http\Resources\Api\V1\Staff\OrderCollection;
use App\Http\Resources\Api\V1\Staff\OrderResource;
use App\Models\Order;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
 
class OrderController extends Controller
{
public function index(): OrderCollection
{
$orders = Order::current()
->with(['customer', 'products'])
->where('restaurant_id', auth()->user()->restaurant_id)
->latest()
->get();
 
return new OrderCollection($orders);
}
 
public function past(): OrderCollection
{
$orders = Order::past()
->with(['customer', 'products'])
->where('restaurant_id', auth()->user()->restaurant_id)
->latest()
->get();
 
return new OrderCollection($orders);
}
 
public function update(UpdateOrderRequest $request, $orderId): JsonResponse
{
$order = Order::where('restaurant_id', $request->user()->restaurant_id)
->findOrFail($orderId);
 
$order->update($request->validated());
 
return (new OrderResource($order))
->response()
->setStatusCode(Response::HTTP_ACCEPTED);
}
}

Then create a new API routes file for staff members.

routes/api/v1/staff.php

use App\Http\Controllers\Api\V1\Staff\OrderController;
use Illuminate\Support\Facades\Route;
 
Route::group([
'prefix' => 'staff',
'as' => 'staff.',
'middleware' => ['auth'],
], function () {
Route::get('orders/past', [OrderController::class, 'past'])->name('orders.past');
Route::apiResource('orders', OrderController::class);
});

And include it in the main api.php file.

routes/api.php

include __DIR__ . '/api/v1/admin.php';
include __DIR__ . '/api/v1/vendor.php';
include __DIR__ . '/api/v1/customer.php';
include __DIR__ . '/api/v1/staff.php';

Extend OrderService

We already have OrderService present. We can add more methods to retrieve restaurant orders.

app/Services/OrderService.php

public function getOrders(?string $period = null): Collection
{
$query = Order::query()->with(['customer', 'products'])
->where('restaurant_id', auth()->user()->restaurant_id);
 
match ($period) {
'current' => $query->current()->latest(),
'past' => $query->past()->latest('updated_at'),
default => $query->latest(),
};
 
return $query->get();
}
 
public function getCurrentOrders(): Collection
{
return $this->getOrders('current');
}
 
public function getPastOrders(): Collection
{
return $this->getOrders('past');
}
 
public function updateOrder(Order $order, array $attributes): Order
{
$order->update($attributes);
 
return $order;
}

We retrieve all restaurant orders in a single getOrders method to avoid repeating ourselves in the code. It accepts the period as an argument and applies scopes depending on that argument. That allows us to create helper methods such as getCurrentOrders and getPastOrders without manually passing an argument each time.

Update API Controller

Now we can update staff API OrderController to consume service.

app/Http/Controllers/Api/V1/Staff/OrderController.php

use App\Http\Resources\Api\V1\Staff\OrderCollection;
use App\Http\Resources\Api\V1\Staff\OrderResource;
use App\Models\Order;
use App\Services\OrderService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
 
class OrderController extends Controller
{
public function __construct(public OrderService $orderService)
{
}
 
public function index(): OrderCollection
{
$orders = Order::current()
->with(['customer', 'products'])
->where('restaurant_id', auth()->user()->restaurant_id)
->latest()
->get();
 
return new OrderCollection($orders);
return new OrderCollection($this->orderService->getCurrentOrders());
}
 
public function past(): OrderCollection
{
$orders = Order::past()
->with(['customer', 'products'])
->where('restaurant_id', auth()->user()->restaurant_id)
->latest()
->get();
 
return new OrderCollection($orders);
return new OrderCollection($this->orderService->getPastOrders());
}
 
public function update(UpdateOrderRequest $request, $orderId): JsonResponse
{
$order = Order::where('restaurant_id', $request->user()->restaurant_id)
->findOrFail($orderId);
 
$order->update($request->validated());
$order = $this->orderService->updateOrder($order, $request->validated());
 
return (new OrderResource($order))
->response()
->setStatusCode(Response::HTTP_ACCEPTED);
}
}

Update Web Controller

In the same way, we update the Web OrderController controller.

app/Http/Controllers/Staff/OrderController.php

use App\Http\Controllers\Controller;
use App\Http\Requests\Staff\UpdateOrderRequest;
use App\Models\Order;
use App\Services\OrderService;
use Inertia\Inertia;
use Inertia\Response;
 
class OrderController extends Controller
{
public function __construct(public OrderService $orderService)
{
}
 
public function index(): Response
{
$currentOrders = Order::current()
->with(['customer', 'products'])
->where('restaurant_id', auth()->user()->restaurant_id)
->latest()
->get();
 
$pastOrders = Order::past()
->with(['customer', 'products'])
->where('restaurant_id', auth()->user()->restaurant_id)
->latest()
->get();
 
return Inertia::render('Staff/Orders', [
'current_orders' => $currentOrders,
'past_orders' => $pastOrders,
'current_orders' => $this->orderService->getCurrentOrders(),
'past_orders' => $this->orderService->getPastOrders(),
'order_status' => OrderStatus::toArray(),
]);
}
 
public function update(UpdateOrderRequest $request, $orderId)
{
$order = Order::where('restaurant_id', $request->user()->restaurant_id)
->findOrFail($orderId);
 
$order->update($request->validated());
$this->orderService->updateOrder($order, $request->validated());
 
return back();
}
}

Extend Tests

We didn't add the orders relationship to the Restaurant Model yet. Let's do this now.

app/Models/Restaurant.php

public function orders(): HasMany
{
return $this->hasMany(Order::class);
}

Add more tests to API OrderTest.

tests/Feature/Api/OrderTest.php

public function test_staff_members_can_see_orders(): void
{
$staff = User::factory()->staff()->create();
 
$response = $this
->actingAs($staff)
->get(route('api.staff.orders.index'));
 
$response->assertOk();
}
 
public function test_staff_members_can_past_orders(): void
{
$staff = User::factory()->staff()->create();
 
$response = $this
->actingAs($staff)
->get(route('api.staff.orders.past'));
 
$response->assertOk();
}
 
public function test_staff_can_update_order(): void
{
$customer = User::factory()->customer()->create();
$restaurant = Restaurant::first();
$product = $restaurant->categories()->first()
->products()->first();
 
$this->actingAs($customer)->postJson(route('api.customer.cart.add', $product));
$this->actingAs($customer)->postJson(route('api.customer.orders.store'));
 
$staff = $restaurant->staff()->first();
$order = $restaurant->orders()->first();
 
$request = $this->actingAs($staff)->putJson(route('api.staff.orders.update', $order), [
'status' => OrderStatus::PREPARING->value,
]);
 
$request->assertAccepted();
}
 
public function test_staff_cant_update_order_with_invalid_status(): void
{
$customer = User::factory()->customer()->create();
$restaurant = Restaurant::first();
$product = $restaurant->categories()->first()
->products()->first();
 
$this->actingAs($customer)->postJson(route('api.customer.cart.add', $product));
$this->actingAs($customer)->postJson(route('api.customer.orders.store'));
 
$staff = $restaurant->staff()->first();
$order = $restaurant->orders()->first();
 
$request = $this->actingAs($staff)->putJson(route('api.staff.orders.update', $order), [
'status' => 'invalid_random_status',
]);
 
$request->assertUnprocessable();
}

Add more tests to Web OrderTest.

tests/Feature/Web/OrderTest.php

public function test_staff_members_can_see_orders(): void
{
$staff = User::factory()->staff()->create();
 
$response = $this
->actingAs($staff)
->get(route('staff.orders.index'));
 
$response->assertInertia(function (AssertableInertia $page) {
return $page->component('Staff/Orders')
->has('current_orders')
->has('past_orders')
->has('order_status');
});
}
 
public function test_staff_can_update_order(): void
{
$customer = User::factory()->customer()->create();
$restaurant = Restaurant::first();
$product = $restaurant->categories()->first()
->products()->first();
 
$this->actingAs($customer)->post(route('customer.cart.add', $product));
$this->actingAs($customer)->post(route('customer.orders.store'));
 
$staff = $restaurant->staff()->first();
$order = $restaurant->orders()->first();
 
$request = $this->actingAs($staff)->put(route('staff.orders.update', $order), [
'status' => OrderStatus::PREPARING->value,
]);
 
$request->assertRedirect()->assertSessionDoesntHaveErrors();
}
 
public function test_staff_cant_update_order_with_invalid_status(): void
{
$customer = User::factory()->customer()->create();
$restaurant = Restaurant::first();
$product = $restaurant->categories()->first()
->products()->first();
 
$this->actingAs($customer)->post(route('customer.cart.add', $product));
$this->actingAs($customer)->post(route('customer.orders.store'));
 
$staff = $restaurant->staff()->first();
$order = $restaurant->orders()->first();
 
$request = $this->actingAs($staff)->put(route('staff.orders.update', $order), [
'status' => 'invalid_random_status',
]);
 
$request->assertRedirect()->assertSessionHasErrors(['status']);
}

Finally, we can run all OrderTest cases.

php artisan test --filter OrderTest
 
PASS Tests\Feature\Api\OrderTest
customer can place order 1.18s
customer cant place empty order 0.24s
customer can see orders 0.26s
staff members can see orders 0.21s
staff members can past orders 0.31s
staff can update order 0.31s
staff cant update order with invalid status 0.25s
 
PASS Tests\Feature\Web\OrderTest
customer can place order 0.26s
customer cant place empty order 0.25s
customer can see orders 0.25s
staff members can see orders 0.22s
staff can update order 0.25s
staff cant update order with invalid status 0.27s
 
Tests: 13 passed (36 assertions)
Duration: 4.33s