Implement subuser verification and invoice/self-serve API fixes

This commit is contained in:
Jeppe Bundgaard
2026-07-13 15:11:49 +02:00
parent a4fafaf7fb
commit 582edd3e6c
29 changed files with 2215 additions and 124 deletions
+199 -14
View File
@@ -93,6 +93,131 @@ it('returns a generic error for invalid subuser credentials', function (): void
->assertMessage('Invalid credentials');
});
it('lets drivers verify their email with a one-time code', function (): void {
api_test_covers('GET /subusers/me/verification', 'happy');
api_test_covers('POST /subusers/me/verification/{channel}/send', 'happy');
api_test_covers('POST /subusers/me/verification/{channel}/verify', 'happy');
api_test_covers('POST /subusers/me/verification/{channel}/verify', 'failure');
$previousFakeMode = getenv('EMAIL_FAKE_MODE');
$previousFakePath = getenv('EMAIL_FAKE_DELIVERIES_PATH');
$fakePath = sys_get_temp_dir() . '/truckwash-subuser-verification-' . bin2hex(random_bytes(6)) . '.jsonl';
putenv('EMAIL_FAKE_MODE=1');
putenv('EMAIL_FAKE_DELIVERIES_PATH=' . $fakePath);
\classes\email::resetFakeDeliveries();
api_fixtures()->setModuleConfig('Email', 'mailersend_enabled', 'true', 'bool');
api_test_runtime()->restartServer();
try {
$customer = api_fixtures()->createUser(['display_name' => 'Verification Customer']);
$session = api_fixtures()->createSubuserSession(
(int)$customer['customer_number'],
['VEHICLES_LIST'],
[
'name' => 'Verification Driver',
'email' => 'driver.verify@example.test',
'email_verified_at' => null,
'phone_verified_at' => null,
]
);
$status = api_client()->get('/subusers/me/verification', $session['headers']);
$send = api_client()->post('/subusers/me/verification/email/send', [], $session['headers']);
$status
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$send
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
expect($status->data()['email']['verified'] ?? null)->toBeFalse();
expect($send->data()['delivery']['status'] ?? null)->toBe('sent');
expect($send->data()['verification']['email']['verified'] ?? null)->toBeFalse();
\classes\email::syncFakeDeliveries();
$deliveries = \classes\email::$fake_deliveries;
expect($deliveries)->toHaveCount(1);
$message = (string)($deliveries[0]['message'] ?? '');
preg_match('/\b([0-9]{6})\b/', $message, $matches);
expect($matches[1] ?? null)->toBeString();
$wrong = api_client()->post('/subusers/me/verification/email/verify', [
'code' => '000000',
], $session['headers']);
$wrong
->assertStatus(400)
->assertEnvelope()
->assertSuccess(false);
$verify = api_client()->post('/subusers/me/verification/email/verify', [
'code' => (string)$matches[1],
], $session['headers']);
$verify
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
expect($verify->data()['verification']['email']['verified'] ?? null)->toBeTrue();
expect($verify->data()['subuser']['email_verified'] ?? null)->toBeTrue();
$row = api_fixtures()->fetchRowById('subusers', (int)$session['subuser']['id']);
expect($row['email_verified_at'] ?? null)->not->toBeNull();
expect($row['phone_verified_at'] ?? null)->toBeNull();
} finally {
api_test_runtime()->restartServer();
\classes\email::resetFakeDeliveries();
if ($previousFakeMode === false) {
putenv('EMAIL_FAKE_MODE');
} else {
putenv('EMAIL_FAKE_MODE=' . $previousFakeMode);
}
if ($previousFakePath === false) {
putenv('EMAIL_FAKE_DELIVERIES_PATH');
} else {
putenv('EMAIL_FAKE_DELIVERIES_PATH=' . $previousFakePath);
}
if (is_file($fakePath)) {
unlink($fakePath);
}
}
});
it('clears email verification when a driver changes their email', function (): void {
api_test_covers('PUT /subusers/me', 'happy');
$customer = api_fixtures()->createUser(['display_name' => 'Driver Email Change Customer']);
$session = api_fixtures()->createSubuserSession(
(int)$customer['customer_number'],
['VEHICLES_LIST'],
[
'email' => 'old.driver.email@example.test',
'email_verified_at' => '2026-07-13 10:00:00',
'phone_verified_at' => '2026-07-13 10:00:00',
]
);
$response = api_client()->request('PUT', '/subusers/me', [
'email' => 'new.driver.email@example.test',
], $session['headers']);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
expect($response->data()['email'] ?? null)->toBe('new.driver.email@example.test');
expect($response->data()['email_verified'] ?? null)->toBeFalse();
expect($response->data()['phone_verified'] ?? null)->toBeTrue();
$row = api_fixtures()->fetchRowById('subusers', (int)$session['subuser']['id']);
expect($row['email_verified_at'] ?? null)->toBeNull();
expect($row['phone_verified_at'] ?? null)->not->toBeNull();
});
it('requires subuser management access before exposing permission nodes', function (): void {
api_test_covers('GET /subusers/permission-nodes', 'auth');
@@ -206,7 +331,7 @@ it('validates setup identifiers before setting the driver password', function ()
}
});
it('lists chauffeur grants across customers for superusers', function (): void {
it('lists chauffeurs once with grouped grants across customers for superusers', function (): void {
$session = api_fixtures()->createUserSession(['list_subusers']);
$firstCustomer = api_fixtures()->createUser([
'display_name' => 'Fleet Customer Alpha',
@@ -253,23 +378,77 @@ it('lists chauffeur grants across customers for superusers', function (): void {
&& in_array((int)($item['id'] ?? 0), [(int)$firstSubuser['id'], (int)$secondSubuser['id']], true)
));
expect($rows)->toHaveCount(3);
expect($rows)->toHaveCount(2);
expect($response->meta()['pagination']['total'] ?? null)->toBe(2);
$byGrantId = [];
$bySubuserId = [];
foreach ($rows as $row) {
$byGrantId[(int)$row['grant_id']] = $row;
$bySubuserId[(int)$row['id']] = $row;
}
expect($byGrantId[$firstGrantId]['id'])->toBe((int)$firstSubuser['id']);
expect($byGrantId[$firstGrantId]['customer_number'])->toBe((int)$firstCustomer['customer_number']);
expect($byGrantId[$firstGrantId]['assigned_vehicle_id'])->toBe((int)$firstVehicle['id']);
expect($byGrantId[$firstGrantId]['assigned_vehicle_reg'])->toBe('AB12345');
expect($byGrantId[$firstGrantId]['dognvask_enabled'])->toBeTrue();
expect($byGrantId[$sharedGrantId]['id'])->toBe((int)$firstSubuser['id']);
expect($byGrantId[$sharedGrantId]['customer_number'])->toBe((int)$secondCustomer['customer_number']);
expect($byGrantId[$sharedGrantId]['dognvask_enabled'])->toBeFalse();
expect($byGrantId[$secondGrantId]['id'])->toBe((int)$secondSubuser['id']);
expect($byGrantId[$secondGrantId]['customer_name'])->toBe('Fleet Customer Beta');
expect($bySubuserId)->toHaveKeys([(int)$firstSubuser['id'], (int)$secondSubuser['id']]);
$firstRow = $bySubuserId[(int)$firstSubuser['id']];
expect($firstRow['grant_count'] ?? null)->toBe(2);
expect($firstRow['customer_numbers'] ?? [])->toContain((int)$firstCustomer['customer_number']);
expect($firstRow['customer_numbers'] ?? [])->toContain((int)$secondCustomer['customer_number']);
$firstGrantsById = [];
foreach ($firstRow['grants'] ?? [] as $grant) {
$firstGrantsById[(int)$grant['grant_id']] = $grant;
}
expect($firstGrantsById[$firstGrantId]['customer_number'])->toBe((int)$firstCustomer['customer_number']);
expect($firstGrantsById[$firstGrantId]['assigned_vehicle_id'])->toBe((int)$firstVehicle['id']);
expect($firstGrantsById[$firstGrantId]['assigned_vehicle_reg'])->toBe('AB12345');
expect($firstGrantsById[$firstGrantId]['dognvask_enabled'])->toBeTrue();
expect($firstGrantsById[$sharedGrantId]['customer_number'])->toBe((int)$secondCustomer['customer_number']);
expect($firstGrantsById[$sharedGrantId]['dognvask_enabled'])->toBeFalse();
$secondRow = $bySubuserId[(int)$secondSubuser['id']];
expect($secondRow['grant_count'] ?? null)->toBe(1);
expect($secondRow['grants'][0]['grant_id'] ?? null)->toBe($secondGrantId);
expect($secondRow['grants'][0]['customer_name'] ?? null)->toBe('Fleet Customer Beta');
});
it('paginates superuser chauffeur lists by unique chauffeur instead of grant count', function (): void {
$session = api_fixtures()->createUserSession(['list_subusers']);
$suffix = (string)random_int(100000, 999999);
$firstCustomer = api_fixtures()->createUser(['display_name' => 'Grouped Page Alpha ' . $suffix]);
$secondCustomer = api_fixtures()->createUser(['display_name' => 'Grouped Page Beta ' . $suffix]);
$firstSubuser = api_fixtures()->createSubuser(['name' => 'Grouped Page Driver Alpha ' . $suffix]);
$secondSubuser = api_fixtures()->createSubuser(['name' => 'Grouped Page Driver Beta ' . $suffix]);
api_fixtures()->grantSubuser((int)$firstSubuser['id'], (int)$firstCustomer['customer_number'], ['BOOKINGS_LIST']);
api_fixtures()->grantSubuser((int)$firstSubuser['id'], (int)$secondCustomer['customer_number'], ['ORDERS_LIST']);
api_fixtures()->grantSubuser((int)$secondSubuser['id'], (int)$secondCustomer['customer_number'], ['VEHICLES_LIST']);
$pageOne = api_client()->get(
'/superuser/subusers?page=1&limit=1&search=' . rawurlencode($suffix) . '&order=name:ASC',
$session['headers']
);
$pageTwo = api_client()->get(
'/superuser/subusers?page=2&limit=1&search=' . rawurlencode($suffix) . '&order=name:ASC',
$session['headers']
);
$pageOne
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$pageTwo
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
expect($pageOne->meta()['pagination']['total'] ?? null)->toBe(2);
expect($pageTwo->meta()['pagination']['total'] ?? null)->toBe(2);
expect($pageOne->data())->toHaveCount(1);
expect($pageTwo->data())->toHaveCount(1);
expect($pageOne->data()[0]['id'] ?? null)->toBe((int)$firstSubuser['id']);
expect($pageOne->data()[0]['grant_count'] ?? null)->toBe(2);
expect($pageTwo->data()[0]['id'] ?? null)->toBe((int)$secondSubuser['id']);
expect($pageTwo->data()[0]['grant_count'] ?? null)->toBe(1);
});
it('lets superusers invite chauffeurs for a selected customer', function (): void {
@@ -565,6 +744,8 @@ it('lets superusers edit chauffeur account details and set passwords', function
'email' => null,
'phone_country_code' => 45,
'phone' => 73123456,
'email_verified_at' => '2026-07-13 10:00:00',
'phone_verified_at' => '2026-07-13 10:00:00',
]);
$subuserObject = (new \objects\subusers_o())->select((int)$subuser['id']);
$subuserObject->getObjectProperties();
@@ -602,11 +783,15 @@ it('lets superusers edit chauffeur account details and set passwords', function
expect($profile->data()['subuser']['email'] ?? null)->toBe('admin.updated.driver@example.com');
expect($profile->data()['subuser']['phone_country_code'] ?? null)->toBe(46);
expect($profile->data()['subuser']['phone'] ?? null)->toBe(73123457);
expect($profile->data()['subuser']['email_verified'] ?? null)->toBeFalse();
expect($profile->data()['subuser']['phone_verified'] ?? null)->toBeFalse();
expect($password->data()['subuser']['setup_required'] ?? null)->toBeFalse();
expect((new \objects\subusers_o())->getSubuserBySetupToken($setupToken))->toBeNull();
$row = api_fixtures()->fetchRowById('subusers', (int)$subuser['id']);
expect(password_verify('ValidPass123', (string)($row['password'] ?? '')))->toBeTrue();
expect($row['email_verified_at'] ?? null)->toBeNull();
expect($row['phone_verified_at'] ?? null)->toBeNull();
} finally {
(new \objects\subusers_o())->invalidateSetupToken($setupToken);
}