DOGNVASK-OP 4: when a new driver registers via the public QR-code endpoint, the customer (dispatcher) is now notified by SMS with approve/deny links the moment the driver is created — no longer only after the driver completes their SMS setup. - registerPublicSubuser() now calls seedPendingGrantAndNotifyDispatcher(), which gets-or-creates the pending company grant, issues grant_approve / grant_deny action tokens, and SMSes the customer with the same link structure the existing /subusers/access-decision flow already consumes. - seedPendingGrantAndNotifyDispatcher() is idempotent: if a previous request is still live (unconsumed, unexpired tokens outstanding), the SMS is skipped so a driver re-scanning the QR code does not spam the dispatcher. - /subusers/setup completion reuses the seeded grant and only re-pings the dispatcher when no decision tokens are outstanding, so a fresh flow (e.g. legacy scans) still notifies once. - Public response stays uniform (no setup_token / customer_number leaked), per existing contract. - New contract tests assert the new code paths, helper methods, and duplicate-guard semantics.
89 lines
4.2 KiB
PHP
89 lines
4.2 KiB
PHP
<?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()');
|
|
});
|