Skip to content

Commit 8317321

Browse files
committed
feat(api): add status-only update endpoints for orders and reservations
1 parent 0fcc561 commit 8317321

File tree

9 files changed

+306
-1
lines changed

9 files changed

+306
-1
lines changed

docs/orders.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,41 @@ Status: 200 OK
501501
}
502502
```
503503

504+
### Update order status
505+
506+
Updates only the order status. Use this when you only need to change the status (e.g. from pending to completed) without sending other order fields.
507+
508+
Required abilities: `orders:write`
509+
510+
```
511+
PATCH /api/orders/:order_id/status
512+
```
513+
514+
#### Parameters
515+
516+
| Key | Type | Description |
517+
|-------------------|----------|-----------------------------------------------------------------------------|
518+
| `status_id` | `integer`| **Required**. The Unique Identifier of the status to assign to the order. |
519+
| `comment` | `string` | Optional comment to attach to the status change (max 500 characters). |
520+
| `notify` | `boolean`| Optional. Whether to notify the customer of the status change (default: false). |
521+
522+
#### Payload example
523+
524+
```json
525+
{
526+
"status_id": 4,
527+
"status_comment": "Order ready for collection"
528+
}
529+
```
530+
531+
#### Response
532+
533+
```html
534+
Status: 200 OK
535+
```
536+
537+
Returns the updated order object in the same shape as the retrieve/update response (including the new `status_id` and `status_updated_at`).
538+
504539
### Delete an order
505540

506541
Permanently deletes an order. It cannot be undone.

docs/reservations.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,6 @@ PATCH /api/reservations/:reservation_id
364364
| `email` | `string` | The reservation's email address |
365365
| `telephone` | `string` | The reservation's telephone number |
366366
| `newsletter` | `boolean` | Whether the reservation opts into newsletter marketing |
367-
| `reservation_group_id` | `integer` | The group the reservation belongs to, if any. |
368367
| `status` | `boolean` | Has the value `true` if the reservation is enabled or the value `false` if the reservation is disabled. |
369368
| `addresses` | `array` | The reservation's addresses, if any |
370369

@@ -418,6 +417,42 @@ Status: 200 OK
418417
}
419418
```
420419

420+
### Update reservation status
421+
422+
Updates only the reservation status. Use this when you only need to change the status (e.g. from pending to confirmed) without sending other reservation fields. Only staff tokens may update reservation status; customer tokens receive 403.
423+
424+
Required abilities: `reservations:write`
425+
426+
```
427+
PATCH /api/reservations/:reservation_id/status
428+
```
429+
430+
#### Parameters
431+
432+
| Key | Type | Description |
433+
|--------------|----------|-----------------------------------------------------------------------------|
434+
| `status_id` | `integer`| **Required**. The Unique Identifier of the status to assign (must exist in `statuses`). |
435+
| `comment` | `string` | Optional comment to attach to the status change (max 500 characters). |
436+
| `notify` | `boolean`| Whether to notify the customer of the status change. |
437+
438+
#### Payload example
439+
440+
```json
441+
{
442+
"status_id": 8,
443+
"comment": "Table confirmed",
444+
"notify": true
445+
}
446+
```
447+
448+
#### Response
449+
450+
```html
451+
Status: 200 OK
452+
```
453+
454+
Returns the updated reservation object in the same shape as the retrieve/update response (including the new `status_id` and `status_updated_at`).
455+
421456
### Delete a reservation
422457

423458
Permanently deletes a reservation. It cannot be undone.

src/ApiResources/Orders.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66

77
use Igniter\Api\ApiResources\Repositories\OrderRepository;
88
use Igniter\Api\ApiResources\Requests\OrderRequest;
9+
use Igniter\Api\ApiResources\Requests\StatusRequest;
910
use Igniter\Api\ApiResources\Transformers\OrderTransformer;
1011
use Igniter\Api\Classes\ApiController;
1112
use Igniter\Api\Http\Actions\RestController;
13+
use Symfony\Component\HttpFoundation\Response;
14+
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
1215

1316
/**
1417
* Orders API Controller
@@ -26,6 +29,7 @@ class Orders extends ApiController
2629
'show' => [],
2730
'update' => [],
2831
'destroy' => [],
32+
'updateStatus' => [],
2933
],
3034
'request' => OrderRequest::class,
3135
'repository' => OrderRepository::class,
@@ -34,6 +38,30 @@ class Orders extends ApiController
3438

3539
protected string|array $requiredAbilities = ['orders:*'];
3640

41+
public function updateStatus(StatusRequest $request, int $orderId): Response
42+
{
43+
throw_if(
44+
($token = $this->getToken()) && $token->isForCustomer(),
45+
new AccessDeniedHttpException('Customers are not allowed to update order status.')
46+
);
47+
48+
$data = $request->validated();
49+
50+
$order = app(OrderRepository::class)->find($orderId);
51+
52+
$data['staff_id'] = $this->user()->getKey();
53+
54+
$order->updateOrderStatus((int) $data['status_id'], array_only($data, ['comment', 'notify', 'staff_id']));
55+
56+
$response = $this->fractal()
57+
->item($order->refresh())
58+
->transformWith(new OrderTransformer)
59+
->withResourceName('orders')
60+
->toArray();
61+
62+
return response()->json($response);
63+
}
64+
3765
public function restAfterSave($model): void
3866
{
3967
if ($orderMenus = (array)request()->input('order_menus', [])) {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Igniter\Api\ApiResources\Requests;
6+
7+
use Igniter\System\Classes\FormRequest;
8+
use Override;
9+
10+
class StatusRequest extends FormRequest
11+
{
12+
#[Override]
13+
public function attributes(): array
14+
{
15+
return [
16+
'comment' => lang('igniter::admin.statuses.label_comment'),
17+
'notify' => lang('igniter::admin.statuses.label_notify_customer'),
18+
];
19+
}
20+
21+
public function rules(): array
22+
{
23+
return [
24+
'status_id' => ['required', 'integer', 'exists:statuses,status_id'],
25+
'comment' => ['nullable', 'string', 'max:500'],
26+
'notify' => ['nullable', 'boolean'],
27+
];
28+
}
29+
}

src/ApiResources/Reservations.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66

77
use Igniter\Api\ApiResources\Repositories\ReservationRepository;
88
use Igniter\Api\ApiResources\Requests\ReservationRequest;
9+
use Igniter\Api\ApiResources\Requests\StatusRequest;
910
use Igniter\Api\ApiResources\Transformers\ReservationTransformer;
1011
use Igniter\Api\Classes\ApiController;
1112
use Igniter\Api\Http\Actions\RestController;
13+
use Symfony\Component\HttpFoundation\Response;
14+
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
1215

1316
/**
1417
* Reservations API Controller
@@ -26,11 +29,36 @@ class Reservations extends ApiController
2629
'show' => [],
2730
'update' => [],
2831
'destroy' => [],
32+
'updateStatus' => [],
2933
],
3034
'request' => ReservationRequest::class,
3135
'repository' => ReservationRepository::class,
3236
'transformer' => ReservationTransformer::class,
3337
];
3438

3539
protected string|array $requiredAbilities = ['reservations:*'];
40+
41+
public function updateStatus(StatusRequest $request, int $reservationId): Response
42+
{
43+
throw_if(
44+
($token = $this->getToken()) && $token->isForCustomer(),
45+
new AccessDeniedHttpException('Customers are not allowed to update reservation status.')
46+
);
47+
48+
$data = $request->validated();
49+
50+
$reservation = app(ReservationRepository::class)->find($reservationId);
51+
52+
$data['staff_id'] = $this->user()->getKey();
53+
54+
$reservation->addStatusHistory((int) $data['status_id'], array_only($data, ['comment', 'notify', 'staff_id']));
55+
56+
$response = $this->fractal()
57+
->item($reservation->refresh())
58+
->transformWith(new ReservationTransformer)
59+
->withResourceName('reservations')
60+
->toArray();
61+
62+
return response()->json($response);
63+
}
3664
}

src/Extension.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use Illuminate\Http\Request;
3232
use Illuminate\Support\Facades\Event;
3333
use Illuminate\Support\Facades\RateLimiter;
34+
use Illuminate\Support\Facades\Route;
3435
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
3536
use Laravel\Sanctum\Sanctum;
3637
use Laravel\Sanctum\SanctumServiceProvider;
@@ -70,6 +71,8 @@ public function boot(): void
7071
{
7172
$this->configureRateLimiting();
7273

74+
$this->registerStatusUpdateRoute();
75+
7376
// Register all the available API routes
7477
ApiManager::registerRoutes();
7578

@@ -286,4 +289,16 @@ protected function configureRateLimiting()
286289
{
287290
RateLimiter::for('api', fn(Request $request) => Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip()));
288291
}
292+
293+
protected function registerStatusUpdateRoute(): void
294+
{
295+
Route::middleware(config('igniter-api.middleware'))
296+
->prefix(config('igniter-api.prefix'))
297+
->group(function(): void {
298+
Route::patch('orders/{orderId}/status', [Orders::class, 'updateStatus'])
299+
->name('igniter.api.orders.update_status');
300+
Route::patch('reservations/{reservationId}/status', [Reservations::class, 'updateStatus'])
301+
->name('igniter.api.reservations.update_status');
302+
});
303+
}
289304
}

tests/ApiResources/OrdersTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,58 @@
213213
->assertJsonPath('data.attributes.order_type', Location::COLLECTION);
214214
});
215215

216+
it('updates order status', function(): void {
217+
Sanctum::actingAs(User::factory()->create(), ['orders:*']);
218+
$order = Order::factory()->create();
219+
$newStatus = Status::isForOrder()->first();
220+
221+
$this
222+
->patch(route('igniter.api.orders.update_status', [$order->getKey()]), [
223+
'status_id' => $newStatus->getKey(),
224+
])
225+
->assertOk()
226+
->assertJsonPath('data.id', (string)$order->getKey())
227+
->assertJsonPath('data.attributes.status_id', $newStatus->getKey());
228+
});
229+
230+
it('updates order status with comment', function(): void {
231+
Sanctum::actingAs(User::factory()->create(), ['orders:*']);
232+
$order = Order::factory()->create();
233+
$newStatus = Status::isForOrder()->first();
234+
235+
$this
236+
->patch(route('igniter.api.orders.update_status', [$order->getKey()]), [
237+
'status_id' => $newStatus->getKey(),
238+
'status_comment' => 'Ready for collection',
239+
])
240+
->assertOk()
241+
->assertJsonPath('data.attributes.status_id', $newStatus->getKey());
242+
});
243+
244+
it('fails to update order status when status_id is missing', function(): void {
245+
Sanctum::actingAs(User::factory()->create(), ['orders:*']);
246+
$order = Order::factory()->create();
247+
248+
$this
249+
->patch(route('igniter.api.orders.update_status', [$order->getKey()]), [
250+
'status_comment' => 'Comment only',
251+
])
252+
->assertStatus(422);
253+
});
254+
255+
it('can not update order status as customer', function(): void {
256+
$customer = Sanctum::actingAs(Customer::factory()->create(), ['orders:*']);
257+
$customer->currentAccessToken()->shouldReceive('isForCustomer')->andReturnTrue();
258+
$order = Order::factory()->create();
259+
$newStatus = Status::isForOrder()->first();
260+
261+
$this
262+
->patch(route('igniter.api.orders.update_status', [$order->getKey()]), [
263+
'status_id' => $newStatus->getKey(),
264+
])
265+
->assertStatus(403);
266+
});
267+
216268
it('deletes an order', function(): void {
217269
Sanctum::actingAs(User::factory()->create(), ['orders:*']);
218270
$order = Order::factory()->create();
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Igniter\Api\Tests\ApiResources\Requests;
6+
7+
use Igniter\Api\ApiResources\Requests\StatusRequest;
8+
9+
it('returns correct attribute labels', function(): void {
10+
$request = new StatusRequest;
11+
12+
$attributes = $request->attributes();
13+
14+
expect($attributes)->toHaveKey('comment', lang('igniter::admin.statuses.label_comment'))
15+
->and($attributes)->toHaveKey('notify', lang('igniter::admin.statuses.label_notify_customer'));
16+
});
17+
18+
it('returns correct validation rules', function(): void {
19+
$request = new StatusRequest;
20+
21+
$rules = $request->rules();
22+
23+
expect($rules)->toHaveKey('status_id')
24+
->and($rules)->toHaveKey('comment')
25+
->and($rules)->toHaveKey('notify')
26+
->and($rules['status_id'])->toContain('required', 'integer', 'exists:statuses,status_id')
27+
->and($rules['comment'])->toContain('nullable', 'string', 'max:500')
28+
->and($rules['notify'])->toContain('nullable', 'boolean');
29+
});

0 commit comments

Comments
 (0)