Add "Get My Active Self-Serve Wash" endpoint and corresponding tests
- Introduced a new `/modules/self-serve/lane/wash/my-active-wash` endpoint to retrieve the authenticated customer's active self-serve wash. - Implemented authentication and permission checks for secure access. - Added detailed response handling for various scenarios, including 401, 403, and 404 statuses. - Extended API documentation and OpenAPI spec to support the new endpoint. - Updated unit and API tests to validate endpoint functionality and route wiring.
This commit is contained in:
+41
-1
@@ -8843,6 +8843,47 @@ paths:
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
|
||||
/modules/self-serve/lane/wash/my-active-wash:
|
||||
get:
|
||||
tags:
|
||||
- Modules
|
||||
summary: Get the authenticated customer's active self-serve wash
|
||||
description: |
|
||||
Returns the latest active self-serve wash for the authenticated customer,
|
||||
without requiring the frontend to know or poll a lane id.
|
||||
operationId: getMyActiveSelfServeWash
|
||||
responses:
|
||||
'200':
|
||||
description: Active self-serve wash details resolved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
lane_id:
|
||||
type: integer
|
||||
status:
|
||||
type: string
|
||||
in_progress:
|
||||
type: boolean
|
||||
elapsed_minutes:
|
||||
type: integer
|
||||
session:
|
||||
type: object
|
||||
nullable: true
|
||||
customer:
|
||||
type: object
|
||||
nullable: true
|
||||
vehicle:
|
||||
type: object
|
||||
nullable: true
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
|
||||
/modules/self-serve/sessions:
|
||||
get:
|
||||
tags:
|
||||
@@ -18688,4 +18729,3 @@ components:
|
||||
data:
|
||||
$ref: '#/components/schemas/DepartmentDailyReportOutsideHoursTrendPayload'
|
||||
|
||||
|
||||
|
||||
@@ -9208,6 +9208,47 @@ paths:
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
|
||||
/modules/self-serve/lane/wash/my-active-wash:
|
||||
get:
|
||||
tags:
|
||||
- Modules
|
||||
summary: Get the authenticated customer's active self-serve wash
|
||||
description: |
|
||||
Returns the latest active self-serve wash for the authenticated customer,
|
||||
without requiring the frontend to know or poll a lane id.
|
||||
operationId: getMyActiveSelfServeWash
|
||||
responses:
|
||||
'200':
|
||||
description: Active self-serve wash details resolved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
lane_id:
|
||||
type: integer
|
||||
status:
|
||||
type: string
|
||||
in_progress:
|
||||
type: boolean
|
||||
elapsed_minutes:
|
||||
type: integer
|
||||
session:
|
||||
type: object
|
||||
nullable: true
|
||||
customer:
|
||||
type: object
|
||||
nullable: true
|
||||
vehicle:
|
||||
type: object
|
||||
nullable: true
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
|
||||
/modules/self-serve/sessions:
|
||||
get:
|
||||
tags:
|
||||
|
||||
@@ -334,6 +334,23 @@ class moduleSelfServeRoute
|
||||
]
|
||||
);
|
||||
|
||||
/** Modules > Self Serve > Lane > Wash > My active wash */
|
||||
$this->get('/modules/self-serve/lane/wash/my-active-wash', function () {
|
||||
global $response;
|
||||
|
||||
$customer_number = $this->requireMyActiveWashCustomerNumber();
|
||||
$session = $this->findLatestActiveSelfServeSessionForCustomer($customer_number);
|
||||
if (!$session->exists()) {
|
||||
$response->error('No active self-serve wash found.', 404);
|
||||
}
|
||||
|
||||
$response->success($this->buildActiveSelfServeSessionResponse($session));
|
||||
},
|
||||
[
|
||||
'list_own_department_selfserve_vehicle_conditions' => 'View the authenticated customer\'s active self-serve wash',
|
||||
]
|
||||
);
|
||||
|
||||
/** Modules > Self Serve > Sessions */
|
||||
$this->get('/modules/self-serve/sessions', function () {
|
||||
global $response;
|
||||
@@ -1307,6 +1324,188 @@ class moduleSelfServeRoute
|
||||
return null;
|
||||
}
|
||||
|
||||
private function requireMyActiveWashCustomerNumber(): int
|
||||
{
|
||||
global $response;
|
||||
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Authentication failed. Invalid or missing token.', 401);
|
||||
}
|
||||
|
||||
if (!self::hasPermission(self::CUSTOMER_SELFSERVE_PERMISSION)) {
|
||||
$this->emitForbidden([self::CUSTOMER_SELFSERVE_PERMISSION]);
|
||||
}
|
||||
|
||||
$customer_number = $this->resolveEffectiveCustomerNumber();
|
||||
if ($customer_number === null || $customer_number <= 0) {
|
||||
$response->error('No customer number found for authenticated user.', 404);
|
||||
}
|
||||
|
||||
return (int)$customer_number;
|
||||
}
|
||||
|
||||
private function findLatestActiveSelfServeSessionForCustomer(int $customer_number): selfserve_wash_sessions_o
|
||||
{
|
||||
if ($customer_number <= 0) {
|
||||
return new selfserve_wash_sessions_o();
|
||||
}
|
||||
|
||||
$rows = (new selfserve_wash_sessions_o())->getFieldsWhere([
|
||||
'customer_number' => $customer_number,
|
||||
'completed_at' => null,
|
||||
'deleted_at' => null,
|
||||
], ['id', 'status']);
|
||||
|
||||
$active_statuses = $this->activeSelfServeWashSessionStatusValues();
|
||||
$rows = array_values(array_filter(
|
||||
$rows,
|
||||
static fn(array $row): bool => in_array((string)($row['status'] ?? ''), $active_statuses, true)
|
||||
));
|
||||
if ($rows === []) {
|
||||
return new selfserve_wash_sessions_o();
|
||||
}
|
||||
|
||||
usort($rows, static fn(array $a, array $b): int => (int)$b['id'] <=> (int)$a['id']);
|
||||
return (new selfserve_wash_sessions_o())->select((int)$rows[0]['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,string>
|
||||
*/
|
||||
private function activeSelfServeWashSessionStatusValues(): array
|
||||
{
|
||||
return array_map(
|
||||
static fn(selfserve_wash_session_status $status): string => $status->value,
|
||||
[
|
||||
selfserve_wash_session_status::MACHINE_RELAY_ENABLED,
|
||||
selfserve_wash_session_status::READY_FOR_MACHINE_START,
|
||||
selfserve_wash_session_status::MACHINE_STARTED,
|
||||
selfserve_wash_session_status::PENDING_QUESTIONS,
|
||||
selfserve_wash_session_status::MACHINE_NOT_ALLOWED,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function buildActiveSelfServeSessionResponse(selfserve_wash_sessions_o $session): array
|
||||
{
|
||||
$lane_id = (int)$session->lane_id->value();
|
||||
$customer_number = $session->customer_number->value() === null ? null : (int)$session->customer_number->value();
|
||||
$vehicle_id = $session->vehicle_id->value() === null ? null : (int)$session->vehicle_id->value();
|
||||
$session_reg = trim((string)$session->reg->value());
|
||||
if ($session_reg === '') {
|
||||
$session_reg = null;
|
||||
}
|
||||
|
||||
$machine_start_triggered_at = $session->machine_start_triggered_at->value() === null ? null : (string)$session->machine_start_triggered_at->value();
|
||||
$wash_started_at = $session->wash_started_at->value() === null ? null : (string)$session->wash_started_at->value();
|
||||
if ($wash_started_at === null) {
|
||||
$wash_started_at = $machine_start_triggered_at;
|
||||
}
|
||||
|
||||
$machine_relay_enabled = (bool)$session->machine_relay_enabled->value();
|
||||
$included_minutes = null;
|
||||
if ($machine_relay_enabled) {
|
||||
$included_minutes = (int)(new selfserve())->config->machine_wash_minutes_included->getVariableValue();
|
||||
if ($included_minutes < 0) {
|
||||
$included_minutes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'lane_id' => $lane_id,
|
||||
'status' => (string)$session->status->value(),
|
||||
'in_progress' => true,
|
||||
'elapsed_minutes' => $session->getElapsedMinutes(),
|
||||
'session' => [
|
||||
'id' => (int)$session->id,
|
||||
'status' => (string)$session->status->value(),
|
||||
'department_id' => $session->department_id->value() === null ? null : (int)$session->department_id->value(),
|
||||
'lane_id' => $lane_id,
|
||||
'reg' => $session_reg,
|
||||
'customer_number' => $customer_number,
|
||||
'vehicle_id' => $vehicle_id,
|
||||
'vehicle_type_id' => $session->vehicle_type_id->value() === null ? null : (int)$session->vehicle_type_id->value(),
|
||||
'included_minutes' => $included_minutes ?? 0,
|
||||
'machine_type_id' => $session->machine_type_id->value() === null ? null : (int)$session->machine_type_id->value(),
|
||||
'machine_relay_enabled' => $machine_relay_enabled,
|
||||
'machine_relay_enabled_at' => $session->machine_relay_enabled_at->value() === null ? null : (string)$session->machine_relay_enabled_at->value(),
|
||||
'machine_start_triggered' => (bool)$session->machine_start_triggered->value(),
|
||||
'machine_start_triggered_at' => $machine_start_triggered_at,
|
||||
'wash_started_at' => $wash_started_at,
|
||||
'created_at' => (string)$session->created_at->value(),
|
||||
'updated_at' => $session->updated_at->value() === null ? null : (string)$session->updated_at->value(),
|
||||
],
|
||||
'customer' => $this->buildInProgressWashCustomerPayload($customer_number),
|
||||
'vehicle' => $this->buildInProgressWashVehiclePayload($vehicle_id, $session_reg),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
private function buildInProgressWashCustomerPayload(?int $customer_number): ?array
|
||||
{
|
||||
if ($customer_number === null || $customer_number <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$customer_obj = (new users_o())->getUserByCustomerNumber($customer_number);
|
||||
if ($customer_obj->exists()) {
|
||||
return [
|
||||
'id' => (int)$customer_obj->id,
|
||||
'customer_number' => $customer_number,
|
||||
'display_name' => $customer_obj->display_name->value() === null ? null : (string)$customer_obj->display_name->value(),
|
||||
'email' => $customer_obj->email->value() === null ? null : (string)$customer_obj->email->value(),
|
||||
'phone_country_code' => $customer_obj->phone_country_code->value() === null ? null : (int)$customer_obj->phone_country_code->value(),
|
||||
'phone' => $customer_obj->phone->value() === null ? null : (string)$customer_obj->phone->value(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => null,
|
||||
'customer_number' => $customer_number,
|
||||
'display_name' => null,
|
||||
'email' => null,
|
||||
'phone_country_code' => null,
|
||||
'phone' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
private function buildInProgressWashVehiclePayload(?int $vehicle_id, ?string $reg): ?array
|
||||
{
|
||||
$vehicle_obj = null;
|
||||
if ($vehicle_id !== null && $vehicle_id > 0) {
|
||||
$tmp_vehicle = (new customer_vehicles_o())->select($vehicle_id);
|
||||
if ($tmp_vehicle->exists()) {
|
||||
$vehicle_obj = $tmp_vehicle;
|
||||
}
|
||||
}
|
||||
if ($vehicle_obj === null && $reg !== null && trim($reg) !== '') {
|
||||
$tmp_vehicle = (new customer_vehicles_o())->selectByPlate(trim($reg));
|
||||
if ($tmp_vehicle->exists()) {
|
||||
$vehicle_obj = $tmp_vehicle;
|
||||
}
|
||||
}
|
||||
if ($vehicle_obj === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int)$vehicle_obj->id,
|
||||
'customer_id' => (int)$vehicle_obj->customer_id->value(),
|
||||
'type' => (int)$vehicle_obj->type->value(),
|
||||
'reg' => (string)$vehicle_obj->reg->value(),
|
||||
'reference' => $vehicle_obj->reference->value() === null ? null : (string)$vehicle_obj->reference->value(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $status
|
||||
* @param array<string,mixed> $extra
|
||||
|
||||
@@ -10,6 +10,14 @@ it('requires authentication before checking in-progress wash permissions', funct
|
||||
->assertMessage('Authentication failed. Invalid or missing token.');
|
||||
});
|
||||
|
||||
it('requires authentication before checking the current customers active self-serve wash', function (): void {
|
||||
$response = api_client()->get('/modules/self-serve/lane/wash/my-active-wash');
|
||||
|
||||
$response
|
||||
->assertStatus(401)
|
||||
->assertMessage('Authentication failed. Invalid or missing token.');
|
||||
});
|
||||
|
||||
it('reports both elevated and customer self-serve permissions when lane polling is not allowed', function (): void {
|
||||
$session = api_fixtures()->createUserSession([]);
|
||||
|
||||
@@ -26,6 +34,18 @@ it('reports both elevated and customer self-serve permissions when lane polling
|
||||
]);
|
||||
});
|
||||
|
||||
it('requires customer self-serve permission before checking my active wash', function (): void {
|
||||
$session = api_fixtures()->createUserSession([]);
|
||||
|
||||
$response = api_client()->get('/modules/self-serve/lane/wash/my-active-wash', $session['headers']);
|
||||
|
||||
$response
|
||||
->assertStatus(403)
|
||||
->assertMissingPermissions([
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows customer self-serve permission to view their own in-progress wash details', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
@@ -51,6 +71,46 @@ it('allows customer self-serve permission to view their own in-progress wash det
|
||||
->and($response->data()['vehicle']['reg'] ?? null)->toBe($scenario['vehicle']['reg']);
|
||||
});
|
||||
|
||||
it('returns the authenticated customers active self-serve wash without requiring a lane id', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => [
|
||||
'group_id' => $group['id'],
|
||||
],
|
||||
]);
|
||||
$token = api_fixtures()->createAuthToken((int)$scenario['customer']['id']);
|
||||
|
||||
$response = api_client()->get(
|
||||
'/modules/self-serve/lane/wash/my-active-wash',
|
||||
api_fixtures()->bearerHeaders($token)
|
||||
);
|
||||
|
||||
$response
|
||||
->assertStatus(200)
|
||||
->assertSuccess(true);
|
||||
|
||||
expect($response->data()['in_progress'] ?? null)->toBeTrue()
|
||||
->and($response->data()['lane_id'] ?? null)->toBe((int)$scenario['lane']['id'])
|
||||
->and($response->data()['session']['id'] ?? null)->toBe((int)$scenario['session']['id'])
|
||||
->and($response->data()['session']['lane_id'] ?? null)->toBe((int)$scenario['lane']['id'])
|
||||
->and($response->data()['session']['customer_number'] ?? null)->toBe((int)$scenario['customer']['customer_number'])
|
||||
->and($response->data()['vehicle']['reg'] ?? null)->toBe($scenario['vehicle']['reg']);
|
||||
});
|
||||
|
||||
it('returns 404 when the authenticated customer has no active self-serve wash', function (): void {
|
||||
$session = api_fixtures()->createUserSession([
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
|
||||
$response = api_client()->get('/modules/self-serve/lane/wash/my-active-wash', $session['headers']);
|
||||
|
||||
$response
|
||||
->assertStatus(404)
|
||||
->assertMessage('No active self-serve wash found.');
|
||||
});
|
||||
|
||||
it('redacts another customers in-progress wash from customer self-serve lane polling', function (): void {
|
||||
$scenario = api_fixtures()->createSelfServeScenario();
|
||||
$otherSession = api_fixtures()->createUserSession([
|
||||
|
||||
@@ -52,6 +52,7 @@ it('documents self-serve machine type, eligibility, summary, and webhook endpoin
|
||||
expect($content)->toContain('/relay/button/press/post:');
|
||||
expect($content)->toContain('/relay/machine/on/post:');
|
||||
expect($content)->toContain('/modules/self-serve/lane/wash/in-progress:');
|
||||
expect($content)->toContain('/modules/self-serve/lane/wash/my-active-wash:');
|
||||
expect($content)->toContain('/modules/self-serve/lane/relay/machine/status:');
|
||||
expect($content)->toContain('/modules/self-serve/lane/relay/machine/set:');
|
||||
expect($content)->toContain('/modules/self-serve/lane/gate/open:');
|
||||
|
||||
@@ -286,9 +286,12 @@ it('wires in-progress self-serve wash details endpoint', function (): void {
|
||||
|
||||
expect($moduleSelfServeRoute)->not->toBeFalse();
|
||||
expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/wash/in-progress');
|
||||
expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/wash/my-active-wash');
|
||||
expect($moduleSelfServeRoute)->toContain('modules_selfserve_lane_wash_in_progress_view');
|
||||
expect($moduleSelfServeRoute)->toContain('list_own_department_selfserve_vehicle_conditions');
|
||||
expect($moduleSelfServeRoute)->toContain('requireInProgressWashDetailsAccess($lane_id)');
|
||||
expect($moduleSelfServeRoute)->toContain('findLatestActiveSelfServeSessionForCustomer($customer_number)');
|
||||
expect($moduleSelfServeRoute)->toContain('buildActiveSelfServeSessionResponse($session)');
|
||||
expect($moduleSelfServeRoute)->toContain('(new department_lanes_o())->select($lane_id)');
|
||||
expect($moduleSelfServeRoute)->toContain('self::requireDepartmentAccess((string)$department_lane->department->value())');
|
||||
expect($moduleSelfServeRoute)->toContain('scopeInProgressWashResponseForCustomer');
|
||||
|
||||
Reference in New Issue
Block a user