Skip to content

Commit 43aa63a

Browse files
authored
✨ add ticket management and statistics (closed beta) (#4590)
1 parent a2e19c6 commit 43aa63a

34 files changed

+3756
-5
lines changed

API_CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ Check back here regularly to stay ahead of removals.
3535

3636
---
3737

38+
# 2026-03-15 (Ticket management – closed beta)
39+
40+
Added new `GET|POST|PUT|DELETE /api/v1/tickets` endpoints for managing transit tickets.
41+
Only available to users with the `closed-beta` role.
42+
43+
Added `ticket` field to `StatusResource`: only present when the authenticated user is the status owner and a ticket is assigned.
44+
45+
Added `PUT /api/v1/statuses/{id}/tickets` endpoint for assigning or removing a ticket from a status (`ticketId`: UUID or `null`).
46+
47+
---
48+
3849
# 2026-03-14 (Freight train category)
3950

4051
Added new transport category `freightTrain` to `HafasTravelType`. Users can now create manual trips with the freight train type.

app/Dto/TicketStatisticsDto.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Dto;
6+
7+
/**
8+
* @phpstan-type PurposeBreakdown array{reason: string|null, count: int, distance: int}
9+
* @phpstan-type CategoryBreakdown array{name: string|null, count: int, distance: int}
10+
* @phpstan-type OperatorBreakdown array{name: string|null, count: int, distance: int}
11+
*/
12+
readonly class TicketStatisticsDto
13+
{
14+
/**
15+
* @param array<PurposeBreakdown> $purposes
16+
* @param array<CategoryBreakdown> $categories
17+
* @param array<OperatorBreakdown> $operators
18+
*/
19+
public function __construct(
20+
public int $tripCount,
21+
public int $distance,
22+
public int $duration,
23+
public ?string $firstUsed,
24+
public ?string $lastUsed,
25+
public ?float $costPerTrip,
26+
public ?float $costPerKm,
27+
public ?float $costPerHour,
28+
public array $purposes,
29+
public array $categories,
30+
public array $operators,
31+
) {}
32+
}

app/Http/Controllers/API/v1/StatusController.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use App\Http\Resources\StopoverResource;
1717
use App\Models\Status;
1818
use App\Models\Stopover;
19+
use App\Models\Ticket;
1920
use App\Models\Trip;
2021
use Illuminate\Auth\Access\AuthorizationException;
2122
use Illuminate\Database\Eloquent\ModelNotFoundException;
@@ -47,6 +48,14 @@
4748
new OA\Property(property: 'destinationArrivalPlanned', description: 'Destination arrival time', type: 'string', format: 'date', example: '2020-01-01 13:00:00', nullable: true),
4849
],
4950
)]
51+
#[OA\Schema(
52+
schema: 'StatusAssignTicketBody',
53+
title: 'StatusAssignTicketBody',
54+
description: 'Assign or remove a ticket from a status',
55+
properties: [
56+
new OA\Property(property: 'ticketId', description: 'UUID of the ticket to assign, or null to remove the assignment', type: 'string', format: 'uuid', example: '00000000-0000-0000-0000-000000000000', nullable: true),
57+
],
58+
)]
5059
#[OA\Schema(
5160
schema: 'Polyline',
5261
title: 'Polyline',
@@ -564,6 +573,7 @@ public function update(Request $request, int $statusId): JsonResponse
564573
if (array_key_exists('eventId', $validated)) { // don't use isset here as it would return false if eventId is null
565574
$updatePayload['event_id'] = $validated['eventId'];
566575
}
576+
567577
$status->update($updatePayload);
568578

569579
if (array_key_exists('manualDeparture', $validated)) {
@@ -616,6 +626,67 @@ public function update(Request $request, int $statusId): JsonResponse
616626
}
617627
}
618628

629+
#[OA\Put(
630+
path: '/statuses/{id}/tickets',
631+
operationId: 'assignTicketToStatus',
632+
description: 'Assign or remove a ticket from a status. Only the status owner can perform this action.',
633+
summary: 'Assign or remove a ticket from a status',
634+
security: [['passport' => ['write-statuses']], ['token' => []]],
635+
requestBody: new OA\RequestBody(
636+
required: true,
637+
content: new OA\JsonContent(ref: '#/components/schemas/StatusAssignTicketBody'),
638+
),
639+
tags: ['Tickets'],
640+
parameters: [
641+
new OA\Parameter(
642+
name: 'id',
643+
description: 'Status-ID',
644+
in: 'path',
645+
schema: new OA\Schema(type: 'integer'),
646+
example: 1337,
647+
),
648+
],
649+
responses: [
650+
new OA\Response(
651+
response: 200,
652+
description: 'successful operation',
653+
content: new OA\JsonContent(
654+
properties: [new OA\Property(property: 'data', ref: '#/components/schemas/StatusResource')],
655+
),
656+
),
657+
new OA\Response(response: 404, description: 'Status or ticket not found'),
658+
],
659+
)]
660+
public function assignTicket(Request $request, int $id): JsonResponse
661+
{
662+
$status = Status::find($id);
663+
if ($status === null || $status->user_id !== auth()->id()) {
664+
return $this->sendError('Status not found.', 404);
665+
}
666+
667+
$validated = Validator::make($request->all(), [
668+
'ticketId' => ['present', 'nullable', 'uuid'],
669+
])->validate();
670+
671+
if ($validated['ticketId'] !== null) {
672+
$ticket = Ticket::where('id', $validated['ticketId'])
673+
->where('user_id', auth()->id())
674+
->first();
675+
676+
if ($ticket === null) {
677+
return $this->sendError('Ticket not found.', 404);
678+
}
679+
680+
$status->ticket_id = $ticket->id;
681+
} else {
682+
$status->ticket_id = null;
683+
}
684+
685+
$status->save();
686+
687+
return $this->sendResponse(new StatusResource($status->fresh()));
688+
}
689+
619690
/**
620691
* @todo extract this to backend
621692
* @todo does this conform to the private checkin-shit?

0 commit comments

Comments
 (0)