Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d25c92a2d6 | ||
|
|
5802aad3e4 | ||
|
|
0e32e03da5 |
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace objects;
|
||||
|
||||
use classes\db;
|
||||
use classes\object_property;
|
||||
use classes\subuser_permission_templates_service;
|
||||
use Exception;
|
||||
use traits\db_object_t;
|
||||
|
||||
class customer_default_driver_template_o extends db
|
||||
{
|
||||
use db_object_t;
|
||||
|
||||
public object_property $customer_number;
|
||||
public object_property $template_key;
|
||||
public object_property $created_at;
|
||||
public object_property $updated_at;
|
||||
|
||||
/**
|
||||
* Convert the object to an array.
|
||||
* @return array
|
||||
* @throws Exception If the object is not selected
|
||||
*/
|
||||
public function asArray(): array
|
||||
{
|
||||
self::requireSelected();
|
||||
return [
|
||||
'id' => (int)$this->id,
|
||||
'customer_number' => (int)$this->customer_number->value(),
|
||||
'template_key' => (string)$this->template_key->value(),
|
||||
'created_at' => (string)$this->created_at->value(),
|
||||
'updated_at' => (string)$this->updated_at->value(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception If the creation of the object fails
|
||||
*/
|
||||
public function add(int $customer_number, string $template_key): void
|
||||
{
|
||||
$normalized = (new subuser_permission_templates_service())->normalizeTemplateKey($template_key);
|
||||
if ($normalized === null || $normalized === subuser_permission_templates_service::TEMPLATE_CUSTOM) {
|
||||
throw new \InvalidArgumentException('Unknown driver access template.');
|
||||
}
|
||||
$tmp_id = self::add_object([
|
||||
'customer_number' => (int)$customer_number,
|
||||
'template_key' => $normalized,
|
||||
]);
|
||||
self::select((int)$tmp_id);
|
||||
self::objectChanged();
|
||||
}
|
||||
|
||||
public function update(int $customer_number, string $template_key): void
|
||||
{
|
||||
$normalized = (new subuser_permission_templates_service())->normalizeTemplateKey($template_key);
|
||||
if ($normalized === null || $normalized === subuser_permission_templates_service::TEMPLATE_CUSTOM) {
|
||||
throw new \InvalidArgumentException('Unknown driver access template.');
|
||||
}
|
||||
if (!$this->doesUserHaveDefaultTemplate($customer_number)) {
|
||||
$this->add($customer_number, $normalized);
|
||||
return;
|
||||
}
|
||||
$this->selectByCustomerNumber($customer_number);
|
||||
self::update_object((int)$this->id, [
|
||||
'template_key' => $normalized,
|
||||
]);
|
||||
self::select((int)$this->id);
|
||||
self::objectChanged();
|
||||
}
|
||||
|
||||
public function objectChanged(): void
|
||||
{
|
||||
// No cache layer for this object.
|
||||
}
|
||||
|
||||
public function structure(): void
|
||||
{
|
||||
$this->setTable('customer_default_driver_template');
|
||||
}
|
||||
|
||||
public function getObjectProperties(): void
|
||||
{
|
||||
$this->customer_number = new object_property($this->table, $this->id, 'customer_number', 'int', true);
|
||||
$this->template_key = new object_property($this->table, $this->id, 'template_key', 'string', true);
|
||||
$this->created_at = new object_property($this->table, $this->id, 'created_at', 'timestamp', true);
|
||||
$this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'timestamp', true);
|
||||
}
|
||||
|
||||
public function getDefaultTemplate(int $customer_number): ?string
|
||||
{
|
||||
if (!$this->doesUserHaveDefaultTemplate($customer_number)) {
|
||||
return null;
|
||||
}
|
||||
$this->selectByCustomerNumber($customer_number);
|
||||
if ($this->exists()) {
|
||||
return (string)$this->template_key->value();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function doesUserHaveDefaultTemplate(int $customer_number): bool
|
||||
{
|
||||
return $this->getIdByCustomerNumber($customer_number) !== null;
|
||||
}
|
||||
|
||||
public function getIdByCustomerNumber(int $customer_number): ?int
|
||||
{
|
||||
$matches = self::getFieldsWhere([
|
||||
'customer_number' => (int)$customer_number,
|
||||
], [
|
||||
'id'
|
||||
]);
|
||||
if (count($matches) > 0) {
|
||||
return (int)$matches[0]['id'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function selectByCustomerNumber(int $customer_number): ?customer_default_driver_template_o
|
||||
{
|
||||
$id = $this->getIdByCustomerNumber($customer_number);
|
||||
if ($id === null) {
|
||||
return null;
|
||||
}
|
||||
$this->select($id);
|
||||
if ($this->exists()) {
|
||||
return $this;
|
||||
}
|
||||
throw new Exception('Unable to select customer default driver template for customer number: ' . $customer_number);
|
||||
}
|
||||
|
||||
public function delete(): void
|
||||
{
|
||||
self::requireSelected();
|
||||
self::delete_object((int)$this->id);
|
||||
self::objectChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace routes;
|
||||
|
||||
use classes\authentication;
|
||||
use classes\subuser_permission_templates_service;
|
||||
use objects\customer_default_driver_template_o;
|
||||
use objects\logs_o;
|
||||
use objects\users_o;
|
||||
use traits\route_t;
|
||||
|
||||
class customerDefaultDriverTemplateRoute
|
||||
{
|
||||
use route_t;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->get('/customer/default-driver-template', function () {
|
||||
global $response;
|
||||
$this->requirePermission('get_customer_default_driver_template');
|
||||
$user = (new authentication())->get_user();
|
||||
$subuser = (new authentication())->get_subuser();
|
||||
$customer_number = $this->resolveCustomerNumberForRead($user, $subuser);
|
||||
$templateKey = (new customer_default_driver_template_o())->getDefaultTemplate((int)$customer_number);
|
||||
if ($templateKey === null) {
|
||||
$response->success([
|
||||
'customer_number' => (int)$customer_number,
|
||||
'template_key' => null,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
$response->success([
|
||||
'customer_number' => (int)$customer_number,
|
||||
'template_key' => $templateKey,
|
||||
]);
|
||||
}, [
|
||||
'get_customer_default_driver_template' => 'Get the default driver access template for a customer (own or, with extended permission, other customers).',
|
||||
]);
|
||||
|
||||
$this->put('/customer/default-driver-template', function () {
|
||||
global $response;
|
||||
$this->requirePermission('edit_customer_default_driver_template');
|
||||
$user = (new authentication())->get_user();
|
||||
$subuser = (new authentication())->get_subuser();
|
||||
$customer_number = $this->resolveCustomerNumberForWrite($user, $subuser);
|
||||
|
||||
self::requireParameters(['template_key']);
|
||||
$templateKey = (string)self::getParameter('template_key');
|
||||
self::requireType($templateKey, self::type_string());
|
||||
self::requireMinLength('template_key', 1);
|
||||
self::requireMaxLength('template_key', 64);
|
||||
|
||||
$service = new subuser_permission_templates_service();
|
||||
$normalized = $service->normalizeTemplateKey($templateKey);
|
||||
if ($normalized === null || $normalized === subuser_permission_templates_service::TEMPLATE_CUSTOM) {
|
||||
$response->error('Unknown driver access template.', 400);
|
||||
}
|
||||
|
||||
$customer = (new users_o())->getUserByCustomerNumber((int)$customer_number);
|
||||
if (!$customer->exists()) {
|
||||
$response->error('Customer not found', 400);
|
||||
}
|
||||
|
||||
$templateObject = new customer_default_driver_template_o();
|
||||
$templateObject->update((int)$customer_number, $normalized);
|
||||
|
||||
$actor_id = $user !== false ? (int)$user->id : ($subuser !== false ? (int)$subuser->id : 0);
|
||||
(new logs_o())->add('customer_default_driver_template', 'global', 1, $actor_id, 'UPDATE_CUSTOMER_DEFAULT_DRIVER_TEMPLATE', 'Default driver template set to ' . $normalized);
|
||||
|
||||
$response->success($templateObject->asArray());
|
||||
}, [
|
||||
'edit_customer_default_driver_template' => 'Edit the default driver access template for a customer (own or, with extended permission, other customers).',
|
||||
]);
|
||||
|
||||
$this->delete('/customer/default-driver-template', function () {
|
||||
global $response;
|
||||
$this->requirePermission('delete_customer_default_driver_template');
|
||||
$user = (new authentication())->get_user();
|
||||
$subuser = (new authentication())->get_subuser();
|
||||
$customer_number = $this->resolveCustomerNumberForWrite($user, $subuser);
|
||||
|
||||
$templateObject = new customer_default_driver_template_o();
|
||||
if (!$templateObject->doesUserHaveDefaultTemplate((int)$customer_number)) {
|
||||
$response->error('This customer does not have a default driver template', 400);
|
||||
}
|
||||
$templateObject->selectByCustomerNumber((int)$customer_number);
|
||||
$templateObject->delete();
|
||||
|
||||
$actor_id = $user !== false ? (int)$user->id : ($subuser !== false ? (int)$subuser->id : 0);
|
||||
(new logs_o())->add('customer_default_driver_template', 'global', 1, $actor_id, 'DELETE_CUSTOMER_DEFAULT_DRIVER_TEMPLATE', 'Default driver template removed');
|
||||
|
||||
$response->success('Default driver template deleted');
|
||||
}, [
|
||||
'delete_customer_default_driver_template' => 'Delete the default driver access template for a customer (own or, with extended permission, other customers).',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param users_o|false $user
|
||||
* @param \objects\subusers_o|false $subuser
|
||||
*/
|
||||
private function resolveCustomerNumberForRead($user, $subuser): int
|
||||
{
|
||||
global $response;
|
||||
$customer_number = $this->customerNumberFromRequest();
|
||||
if ($subuser !== false) {
|
||||
$subuserCustomer = (int)$subuser->customer_number_target->value();
|
||||
if ($subuserCustomer <= 0 || $subuserCustomer !== $customer_number) {
|
||||
$response->error('Unauthorized', 401);
|
||||
}
|
||||
return $customer_number;
|
||||
}
|
||||
if ($user === false) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
if ((int)$user->customer_number->value() !== $customer_number) {
|
||||
$this->requirePermission('get_customer_default_driver_template_other');
|
||||
}
|
||||
return $customer_number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param users_o|false $user
|
||||
* @param \objects\subusers_o|false $subuser
|
||||
*/
|
||||
private function resolveCustomerNumberForWrite($user, $subuser): int
|
||||
{
|
||||
global $response;
|
||||
$customer_number = $this->customerNumberFromRequest();
|
||||
if ($subuser !== false) {
|
||||
$subuserCustomer = (int)$subuser->customer_number_target->value();
|
||||
if ($subuserCustomer <= 0 || $subuserCustomer !== $customer_number) {
|
||||
$response->error('Unauthorized', 401);
|
||||
}
|
||||
return $customer_number;
|
||||
}
|
||||
if ($user === false) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
if ((int)$user->customer_number->value() !== $customer_number) {
|
||||
$this->requirePermission('edit_customer_default_driver_template_other');
|
||||
}
|
||||
return $customer_number;
|
||||
}
|
||||
|
||||
private function customerNumberFromRequest(): int
|
||||
{
|
||||
global $response;
|
||||
if (self::isParametersSet(['customer_number'])) {
|
||||
self::requireParameters(['customer_number']);
|
||||
$customer_number = (int)self::getParameter('customer_number');
|
||||
} else {
|
||||
$auth = new authentication();
|
||||
$user = $auth->get_user();
|
||||
$subuser = $auth->get_subuser();
|
||||
if ($user !== false) {
|
||||
$customer_number = (int)$user->customer_number->value();
|
||||
} elseif ($subuser !== false) {
|
||||
$customer_number = (int)$subuser->customer_number_target->value();
|
||||
} else {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
}
|
||||
if ($customer_number <= 0) {
|
||||
$response->error('Invalid customer number', 400);
|
||||
}
|
||||
return $customer_number;
|
||||
}
|
||||
}
|
||||
@@ -1881,6 +1881,14 @@ class subusersRoute
|
||||
$response->error('Failed to create driver registration', 500);
|
||||
}
|
||||
|
||||
// Create (or reuse) a pending company grant and notify the dispatcher
|
||||
// immediately so they can approve the driver before the driver even
|
||||
// finishes the SMS setup. This is TRU-88 / DOGNVASK-OP 4.
|
||||
$this->seedPendingGrantAndNotifyDispatcher(
|
||||
$subuser,
|
||||
$customerNumber
|
||||
);
|
||||
|
||||
if ($subuser->requiresSetup()) {
|
||||
$issuedInvite = $this->issueSetupInvite($subuser);
|
||||
if (($issuedInvite['delivery']['status'] ?? null) !== 'sent') {
|
||||
@@ -1898,6 +1906,69 @@ class subusersRoute
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a pending grant exists for the (subuser, customer) pair and SMS
|
||||
* the dispatcher with approve/deny links as soon as the driver is created
|
||||
* via the public QR-code flow. Idempotent: reuses an existing grant and
|
||||
* short-circuits the SMS send when an actionable notification is already
|
||||
* outstanding (avoiding duplicate dispatcher pings for repeat scans).
|
||||
*/
|
||||
private function seedPendingGrantAndNotifyDispatcher(
|
||||
subusers_o $subuser,
|
||||
int $customerNumber
|
||||
): array {
|
||||
$grant = (new subuser_grants_o())->getGrantForSubuserAndCustomer(
|
||||
(int)$subuser->id,
|
||||
$customerNumber,
|
||||
true
|
||||
);
|
||||
if ($grant === null) {
|
||||
$grant = (new subuser_grants_o())->add(
|
||||
$customerNumber,
|
||||
(int)$subuser->id,
|
||||
false,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->hasOutstandingGrantDecisionTokens((int)$grant->id, (int)$subuser->id)) {
|
||||
return [
|
||||
'channel' => 'sms',
|
||||
'status' => 'skipped_duplicate',
|
||||
'message' => 'Dispatcher already has an outstanding approval request.',
|
||||
];
|
||||
}
|
||||
|
||||
return $this->notifyCustomerOfGrantRequest($grant, $subuser, $customerNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when there is at least one unconsumed, unexpired
|
||||
* PURPOSE_GRANT_APPROVE or PURPOSE_GRANT_DENY token for the (subuser,
|
||||
* grant) pair. Used to avoid re-pinging the dispatcher when a driver
|
||||
* re-scans the QR code while a previous approval request is still live.
|
||||
*/
|
||||
private function hasOutstandingGrantDecisionTokens(int $grantId, int $subuserId): bool
|
||||
{
|
||||
global $db;
|
||||
$statement = $db->conn->prepare(
|
||||
'SELECT id FROM subuser_action_tokens '
|
||||
. 'WHERE grant_id = ? AND subuser_id = ? '
|
||||
. "AND purpose IN ('grant_approve', 'grant_deny') "
|
||||
. 'AND used_at IS NULL AND expires_at > UTC_TIMESTAMP() '
|
||||
. 'LIMIT 1'
|
||||
);
|
||||
if ($statement === false) {
|
||||
return false;
|
||||
}
|
||||
$statement->bind_param('ii', $grantId, $subuserId);
|
||||
$statement->execute();
|
||||
$statement->store_result();
|
||||
$outstanding = $statement->num_rows > 0;
|
||||
$statement->close();
|
||||
return $outstanding;
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
subusers_schema_bootstrap::ensureTables();
|
||||
@@ -2274,16 +2345,18 @@ class subusersRoute
|
||||
null
|
||||
);
|
||||
}
|
||||
$notification = $this->notifyCustomerOfGrantRequest(
|
||||
$grant,
|
||||
$subuser,
|
||||
$customerNumber
|
||||
);
|
||||
if (($notification['status'] ?? null) !== 'sent') {
|
||||
$response->error(
|
||||
'Driver setup was saved, but the customer notification could not be sent. Please try again.',
|
||||
503
|
||||
if (!$this->hasOutstandingGrantDecisionTokens((int)$grant->id, (int)$subuser->id)) {
|
||||
$notification = $this->notifyCustomerOfGrantRequest(
|
||||
$grant,
|
||||
$subuser,
|
||||
$customerNumber
|
||||
);
|
||||
if (($notification['status'] ?? null) !== 'sent') {
|
||||
$response->error(
|
||||
'Driver setup was saved, but the customer notification could not be sent. Please try again.',
|
||||
503
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Invalidate the setup token
|
||||
|
||||
@@ -35,7 +35,23 @@ it('requires abuse controls and serializes idempotent driver registration writes
|
||||
expect($method)->toContain('SELECT RELEASE_LOCK(?)');
|
||||
expect(strpos($method, '$db->conn->commit();'))->toBeLessThan(strpos($method, '$this->issueSetupInvite($subuser);'));
|
||||
expect($method)->toContain('$this->storePublicRegistrationPending($setupToken, $customerNumber);');
|
||||
expect($method)->not->toContain('(new subuser_grants_o())->add(');
|
||||
// TRU-88: dispatcher is notified at driver-creation time, so the public
|
||||
// registration now seeds a pending company grant via the helper.
|
||||
expect($method)->toContain('$this->seedPendingGrantAndNotifyDispatcher(');
|
||||
expect($method)->toContain('$this->hasOutstandingGrantDecisionTokens(');
|
||||
});
|
||||
|
||||
it('seeds the pending grant and notifies the dispatcher on QR driver creation (TRU-88)', function (): void {
|
||||
$method = preg_replace('/\s+/', ' ', public_subuser_registration_method());
|
||||
|
||||
expect($method)->toContain('seedPendingGrantAndNotifyDispatcher');
|
||||
expect($method)->toContain('hasOutstandingGrantDecisionTokens');
|
||||
expect($method)->toContain('notifyCustomerOfGrantRequest');
|
||||
// The dispatcher ping must run BEFORE we store the setup token in Redis
|
||||
// so a pre-approval is valid by the time the driver opens their setup link.
|
||||
expect(strpos($method, '$this->seedPendingGrantAndNotifyDispatcher('))->toBeLessThan(
|
||||
strpos($method, '$this->storePublicRegistrationPending(')
|
||||
);
|
||||
});
|
||||
|
||||
it('never returns setup credentials from either public driver registration alias', function (): void {
|
||||
@@ -55,7 +71,7 @@ it('never returns setup credentials from either public driver registration alias
|
||||
expect($operation)->not->toContain('setup_link:');
|
||||
});
|
||||
|
||||
it('creates and notifies a company grant only after the SMS setup token is completed', function (): void {
|
||||
it('reuses the pre-seeded grant at setup completion and re-notifies only when no decision tokens are outstanding', function (): void {
|
||||
$code = preg_replace('/\s+/', ' ', (string)file_get_contents(app_path('routes/subusersRoute.php')));
|
||||
$setupStart = strpos($code, "\$this->post('/subusers/setup', function () {");
|
||||
$setupEnd = strpos($code, "\$this->post('/subusers/password-reset/request'", $setupStart === false ? 0 : $setupStart);
|
||||
@@ -65,5 +81,9 @@ it('creates and notifies a company grant only after the SMS setup token is compl
|
||||
expect($setup)->toContain('(new subuser_grants_o())->add(');
|
||||
expect($setup)->toContain('$this->notifyCustomerOfGrantRequest(');
|
||||
expect($setup)->toContain('$this->clearPublicRegistrationPending($token)');
|
||||
// TRU-88: the setup completion must short-circuit the dispatcher SMS
|
||||
// when approval/deny tokens were already issued at driver creation time.
|
||||
expect($setup)->toContain('$this->hasOutstandingGrantDecisionTokens(');
|
||||
expect($setup)->toContain('$this->clearThrottleAttempt($setupThrottleKey);');
|
||||
expect(strpos($setup, '$subuser->update('))->toBeLessThan(strpos($setup, '(new subuser_grants_o())->add('));
|
||||
});
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* TRU-88 / DOGNVASK-OP 4: SMS to dispatcher on QR driver creation.
|
||||
*
|
||||
* Asserts that the public QR-code driver registration handler in
|
||||
* services/nginx/app/routes/subusersRoute.php seeds a pending company
|
||||
* grant and notifies the dispatcher with approve/deny links at the
|
||||
* moment the driver is created, without waiting for the driver to
|
||||
* complete the SMS setup. The setup completion must reuse the seeded
|
||||
* grant and re-notify only when no decision tokens are still live.
|
||||
*/
|
||||
|
||||
function tru_88_public_registration_block(): string
|
||||
{
|
||||
$code = (string)file_get_contents(app_path('routes/subusersRoute.php'));
|
||||
$start = strpos($code, 'private function registerPublicSubuser(): void');
|
||||
$end = strpos($code, 'public function run(): void', $start === false ? 0 : $start);
|
||||
if ($start === false || $end === false || $end <= $start) {
|
||||
throw new RuntimeException('Unable to locate the public subuser registration handler.');
|
||||
}
|
||||
return $code;
|
||||
}
|
||||
|
||||
function tru_88_setup_completion_block(): string
|
||||
{
|
||||
$code = (string)file_get_contents(app_path('routes/subusersRoute.php'));
|
||||
$start = strpos($code, "\$this->post('/subusers/setup', function () {");
|
||||
$end = strpos($code, "\$this->post('/subusers/password-reset/request'", $start === false ? 0 : $start);
|
||||
if ($start === false || $end === false || $end <= $start) {
|
||||
throw new RuntimeException('Unable to locate the SMS setup completion handler.');
|
||||
}
|
||||
return substr($code, $start, $end - $start);
|
||||
}
|
||||
|
||||
it('seeds the pending grant and pings the dispatcher on QR driver creation', function (): void {
|
||||
$code = preg_replace('/\s+/', ' ', tru_88_public_registration_block());
|
||||
|
||||
// Idempotent helper that gets-or-creates the grant and short-circuits
|
||||
// duplicate dispatcher pings.
|
||||
expect($code)->toContain('private function seedPendingGrantAndNotifyDispatcher(');
|
||||
expect($code)->toContain('private function hasOutstandingGrantDecisionTokens(');
|
||||
expect($code)->toContain('$this->seedPendingGrantAndNotifyDispatcher(');
|
||||
|
||||
// The dispatcher ping must happen BEFORE the driver receives the SMS
|
||||
// setup link so the customer's approve/deny links are valid the moment
|
||||
// the driver taps the link.
|
||||
$seedIndex = strpos($code, '$this->seedPendingGrantAndNotifyDispatcher(');
|
||||
$setupIndex = strpos($code, '$this->issueSetupInvite($subuser);');
|
||||
$storeIndex = strpos($code, '$this->storePublicRegistrationPending(');
|
||||
expect($seedIndex)->not->toBeFalse();
|
||||
expect($setupIndex)->not->toBeFalse();
|
||||
expect($storeIndex)->not->toBeFalse();
|
||||
expect($seedIndex)->toBeLessThan($setupIndex);
|
||||
expect($seedIndex)->toBeLessThan($storeIndex);
|
||||
});
|
||||
|
||||
it('does not double-ping the dispatcher on repeat QR scans', function (): void {
|
||||
$code = preg_replace('/\s+/', ' ', tru_88_public_registration_block());
|
||||
|
||||
// The helper must check for outstanding grant decision tokens before
|
||||
// firing a fresh SMS, so a driver who re-scans the QR code after
|
||||
// their previous request is still live does not spam the dispatcher.
|
||||
expect($code)->toContain('hasOutstandingGrantDecisionTokens');
|
||||
expect($code)->toContain("'skipped_duplicate'");
|
||||
});
|
||||
|
||||
it('reuses the seeded grant at SMS setup completion and skips the duplicate SMS', function (): void {
|
||||
$code = preg_replace('/\s+/', ' ', tru_88_setup_completion_block());
|
||||
|
||||
// The setup completion must check for outstanding grant decision tokens
|
||||
// so a dispatcher who was already pinged at QR-driver-creation time is
|
||||
// not notified again once the driver finishes their setup.
|
||||
expect($code)->toContain('$this->hasOutstandingGrantDecisionTokens(');
|
||||
expect($code)->toContain('$this->notifyCustomerOfGrantRequest(');
|
||||
});
|
||||
|
||||
it('queries subuser_action_tokens for both grant_approve and grant_deny purposes', function (): void {
|
||||
$code = preg_replace('/\s+/', ' ', tru_88_public_registration_block());
|
||||
|
||||
// The duplicate-guard query must consider both approve AND deny tokens.
|
||||
// Hiding only the deny tokens would re-ping the dispatcher even when
|
||||
// the customer has already rejected the driver.
|
||||
expect($code)->toContain("'grant_approve'");
|
||||
expect($code)->toContain("'grant_deny'");
|
||||
expect($code)->toContain('used_at IS NULL');
|
||||
expect($code)->toContain('expires_at > UTC_TIMESTAMP()');
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
if (!defined('WD')) {
|
||||
define('WD', dirname(__DIR__, 2));
|
||||
}
|
||||
|
||||
function ok($message): void { echo "\n\033[32m✔ $message\033[0m\n"; }
|
||||
function fail($message): void { echo "\n\033[31m✖ $message\033[0m\n"; exit(1); }
|
||||
|
||||
$requiredFiles = [
|
||||
WD . '/routes/customerDefaultDriverTemplateRoute.php',
|
||||
WD . '/objects/customer_default_driver_template_o.php',
|
||||
];
|
||||
|
||||
foreach ($requiredFiles as $requiredFile) {
|
||||
if (!file_exists($requiredFile)) {
|
||||
fail('Required file missing: ' . $requiredFile);
|
||||
}
|
||||
}
|
||||
|
||||
$routeCode = file_get_contents(WD . '/routes/customerDefaultDriverTemplateRoute.php');
|
||||
$objectCode = file_get_contents(WD . '/objects/customer_default_driver_template_o.php');
|
||||
|
||||
if ($routeCode === false || $objectCode === false) {
|
||||
fail('Unable to read new customer default driver template source files');
|
||||
}
|
||||
|
||||
$assertContains = static function (string $haystack, string $needle, string $message): void {
|
||||
if (strpos($haystack, $needle) === false) {
|
||||
fail($message);
|
||||
}
|
||||
};
|
||||
|
||||
$assertContains($routeCode, "'/customer/default-driver-template'", 'Route should register the /customer/default-driver-template endpoint');
|
||||
$assertContains($routeCode, "requirePermission('get_customer_default_driver_template')", 'GET endpoint should require get_customer_default_driver_template permission');
|
||||
$assertContains($routeCode, "requirePermission('edit_customer_default_driver_template')", 'PUT endpoint should require edit_customer_default_driver_template permission');
|
||||
$assertContains($routeCode, "requirePermission('delete_customer_default_driver_template')", 'DELETE endpoint should require delete_customer_default_driver_template permission');
|
||||
$assertContains($routeCode, 'subuser_permission_templates_service', 'Route should reuse the existing template service for validation');
|
||||
$assertContains($routeCode, 'TEMPLATE_CUSTOM', 'Route should reject custom template keys to avoid bypassing template logic');
|
||||
|
||||
$assertContains($objectCode, "setTable('customer_default_driver_template')", 'Object should map to customer_default_driver_template table');
|
||||
$assertContains($objectCode, 'customer_number', 'Object should expose customer_number property');
|
||||
$assertContains($objectCode, 'template_key', 'Object should expose template_key property');
|
||||
$assertContains($objectCode, 'doesUserHaveDefaultTemplate', 'Object should provide doesUserHaveDefaultTemplate helper');
|
||||
$assertContains($objectCode, 'getDefaultTemplate', 'Object should provide getDefaultTemplate helper');
|
||||
$assertContains($objectCode, 'update(', 'Object should provide an update() method for upserts');
|
||||
$assertContains($objectCode, 'selectByCustomerNumber', 'Object should provide selectByCustomerNumber helper');
|
||||
$assertContains($objectCode, 'delete(', 'Object should provide a delete() method for removal');
|
||||
|
||||
// Sanity: the route must surface 5 main permission groups in the access model
|
||||
// (TRU-89 requests 5 toggles in the customer portal "tilladelser" tab).
|
||||
$templateServiceFile = WD . '/classes/subuser_permission_templates_service.php';
|
||||
if (!file_exists($templateServiceFile)) {
|
||||
fail('subuser_permission_templates_service.php not found');
|
||||
}
|
||||
$templateServiceCode = preg_replace('/\s+/', ' ', file_get_contents($templateServiceFile));
|
||||
$expectedGroups = ['vehicles', 'selfserve', 'bookings', 'orders', 'driver_management'];
|
||||
foreach ($expectedGroups as $expectedGroup) {
|
||||
if (strpos($templateServiceCode, "'$expectedGroup'") === false) {
|
||||
fail('Expected permission group missing from template service: ' . $expectedGroup);
|
||||
}
|
||||
}
|
||||
if (strpos($templateServiceCode, "GROUP_ORDER") === false) {
|
||||
fail('Template service is missing the GROUP_ORDER definition');
|
||||
}
|
||||
|
||||
ok('Customer default driver template route registers all CRUD endpoints and validates templates');
|
||||
ok('Object exposes the required persistence helpers and maps to the right table');
|
||||
ok('Template service still defines the 5 permission groups required for the kunde portal "tilladelser" tab');
|
||||
|
||||
echo "\nCustomerDefaultDriverTemplateRouteTest completed.\n";
|
||||
Reference in New Issue
Block a user