- Introduced `safety_seal` column in the `orders` table. - Updated order creation and completion logic to handle safety seal values. - Enhanced order and booking classes to manage safety seal attachment and retrieval. - Added tests to validate safety seal functionality in order processing.
1083 lines
48 KiB
PHP
1083 lines
48 KiB
PHP
<?php
|
|
|
|
namespace routes;
|
|
|
|
use classes\authentication;
|
|
use classes\economic;
|
|
use classes\gatewayapi;
|
|
use classes\response;
|
|
use classes\virkdata;
|
|
use Exception;
|
|
use modules\virkdata\helpers\virkdata_response;
|
|
use objects\logs_o;
|
|
use objects\subuser_grants_o;
|
|
use objects\subusers_o;
|
|
use objects\users_o;
|
|
use modules\subusers\permissions\subusers_permission_nodes_bookings;
|
|
use modules\subusers\permissions\subusers_permission_nodes_orders;
|
|
use modules\subusers\permissions\subusers_permission_nodes_selfserve;
|
|
use modules\subusers\permissions\subusers_permission_nodes_subusers;
|
|
use modules\subusers\permissions\subusers_permission_nodes_vehicles;
|
|
use modules\subusers\helpers\subusers_permission_node_key;
|
|
use traits\route_t;
|
|
|
|
class subusersRoute
|
|
{
|
|
use route_t;
|
|
|
|
private function getOwnPermissionForNode(subusers_permission_node_key $node)
|
|
{
|
|
return match ($node) {
|
|
subusers_permission_node_key::SUBUSERS_LIST => self::definePermission('list_own_subusers', subusers_permission_node_key::SUBUSERS_LIST),
|
|
subusers_permission_node_key::SUBUSERS_ADD => self::definePermission('add_own_subusers', subusers_permission_node_key::SUBUSERS_ADD),
|
|
subusers_permission_node_key::SUBUSERS_EDIT => self::definePermission('edit_own_subusers', subusers_permission_node_key::SUBUSERS_EDIT),
|
|
subusers_permission_node_key::SUBUSERS_DELETE => self::definePermission('delete_own_subusers', subusers_permission_node_key::SUBUSERS_DELETE),
|
|
default => self::definePermission(strtolower($node->name), $node),
|
|
};
|
|
}
|
|
|
|
private static function requireRegex(string $input, string $regex, string $error_message): void
|
|
{
|
|
if (!preg_match($regex, $input)) {
|
|
global $response;
|
|
$response->error($error_message, 400);
|
|
}
|
|
}
|
|
|
|
private function requireManagedCustomerScope(subusers_permission_node_key $node, ?int $targetCustomerNumber = null): int
|
|
{
|
|
global $response;
|
|
|
|
$auth = new authentication();
|
|
$permission = $this->getOwnPermissionForNode($node);
|
|
|
|
$subuser = $auth->get_subuser();
|
|
if ($subuser !== false) {
|
|
$customerNumber = (int)$auth->get_subuser_customer_number_target();
|
|
if ($customerNumber <= 0) {
|
|
$response->error('Unauthorized', 401);
|
|
}
|
|
if ($targetCustomerNumber !== null && $customerNumber !== (int)$targetCustomerNumber) {
|
|
$this->emitForbidden([$permission]);
|
|
}
|
|
self::requirePermission($permission);
|
|
return $customerNumber;
|
|
}
|
|
|
|
$user = $auth->get_user();
|
|
if ($user !== false) {
|
|
$customerNumber = (int)$user->customer_number->value();
|
|
if ($customerNumber <= 0) {
|
|
$response->error('Unauthorized', 401);
|
|
}
|
|
if ($targetCustomerNumber !== null && $customerNumber !== (int)$targetCustomerNumber) {
|
|
$this->emitForbidden([$permission]);
|
|
}
|
|
return $customerNumber;
|
|
}
|
|
|
|
$response->error('Unauthorized', 401);
|
|
return 0;
|
|
}
|
|
|
|
private function parsePermissionsPayload(mixed $raw, ?array $default = null): ?array
|
|
{
|
|
global $response;
|
|
|
|
if ($raw === null) {
|
|
return $default;
|
|
}
|
|
|
|
if (is_string($raw)) {
|
|
$decoded = json_decode($raw, true);
|
|
if (!is_array($decoded)) {
|
|
$response->error('Invalid permissions payload', 400);
|
|
}
|
|
$raw = $decoded;
|
|
}
|
|
|
|
if (!is_array($raw)) {
|
|
$response->error('Invalid permissions type', 400);
|
|
}
|
|
|
|
$permissions = [];
|
|
foreach ($raw as $permission) {
|
|
if (!is_string($permission) || subusers_permission_node_key::tryFrom($permission) === null) {
|
|
$response->error('Unknown permission key: ' . (string)$permission, 400);
|
|
}
|
|
$permissions[] = strtoupper(trim($permission));
|
|
}
|
|
|
|
return array_values(array_unique($permissions));
|
|
}
|
|
|
|
private function normalizeOptionalString(mixed $value): ?string
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
|
|
$normalized = trim((string)$value);
|
|
return $normalized === '' ? null : $normalized;
|
|
}
|
|
|
|
private function assertSubuserIdentifiersAvailable(
|
|
?int $phoneCountryCode,
|
|
?int $phone,
|
|
?string $username = null,
|
|
?string $email = null,
|
|
?int $ignoreSubuserId = null
|
|
): void {
|
|
global $response;
|
|
|
|
if ($phoneCountryCode !== null && $phone !== null) {
|
|
$existingByPhone = (new subusers_o())->getSubuserByPhone($phoneCountryCode, $phone);
|
|
if ($existingByPhone !== null && (int)$existingByPhone->id !== (int)$ignoreSubuserId) {
|
|
$response->error('Account already exists with this phone number', 400);
|
|
}
|
|
}
|
|
|
|
if ($username !== null) {
|
|
$existingByUsername = (new subusers_o())->getSubuserByUsername($username);
|
|
if ($existingByUsername !== null && (int)$existingByUsername->id !== (int)$ignoreSubuserId) {
|
|
$response->error('Account already exists with this username', 400);
|
|
}
|
|
}
|
|
|
|
if ($email !== null) {
|
|
$existingByEmail = (new subusers_o())->getSubuserByEmail($email);
|
|
if ($existingByEmail !== null && (int)$existingByEmail->id !== (int)$ignoreSubuserId) {
|
|
$response->error('Account already exists with this email address', 400);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function resolveSubuserByIdentifiers(
|
|
?int $phoneCountryCode,
|
|
?int $phone,
|
|
?string $username = null,
|
|
?string $email = null
|
|
): ?subusers_o {
|
|
global $response;
|
|
|
|
$matches = [];
|
|
|
|
if ($phoneCountryCode !== null && $phone !== null) {
|
|
$existingByPhone = (new subusers_o())->getSubuserByPhone($phoneCountryCode, $phone);
|
|
if ($existingByPhone !== null) {
|
|
$matches[(int)$existingByPhone->id] = $existingByPhone;
|
|
}
|
|
}
|
|
|
|
if ($username !== null) {
|
|
$existingByUsername = (new subusers_o())->getSubuserByUsername($username);
|
|
if ($existingByUsername !== null) {
|
|
$matches[(int)$existingByUsername->id] = $existingByUsername;
|
|
}
|
|
}
|
|
|
|
if ($email !== null) {
|
|
$existingByEmail = (new subusers_o())->getSubuserByEmail($email);
|
|
if ($existingByEmail !== null) {
|
|
$matches[(int)$existingByEmail->id] = $existingByEmail;
|
|
}
|
|
}
|
|
|
|
if (count($matches) > 1) {
|
|
$response->error('Provided driver identifiers match multiple existing accounts', 409);
|
|
}
|
|
|
|
return count($matches) === 1 ? array_values($matches)[0] : null;
|
|
}
|
|
|
|
private function buildSetupLink(string $token): string
|
|
{
|
|
return 'https://truckwash.io/complete-registration?token=' . $token;
|
|
}
|
|
|
|
private function issueSetupInvite(subusers_o $subuser): array
|
|
{
|
|
if (!$subuser->requiresSetup()) {
|
|
return [
|
|
'setup_token' => null,
|
|
'setup_link' => null,
|
|
'delivery' => [
|
|
'channel' => 'sms',
|
|
'status' => 'not_required',
|
|
'message' => 'Driver account already accepted the invitation.',
|
|
],
|
|
];
|
|
}
|
|
|
|
$token = $subuser->generateSetupToken();
|
|
$link = $this->buildSetupLink($token);
|
|
$delivery = [
|
|
'channel' => 'sms',
|
|
'status' => 'unavailable',
|
|
'message' => 'SMS delivery is not configured.',
|
|
];
|
|
|
|
try {
|
|
$gatewayAPI = new gatewayapi();
|
|
if ($gatewayAPI->isEnabled()) {
|
|
$phoneNumber = (string)$subuser->phone_country_code->value() . (string)$subuser->phone->value();
|
|
$message = 'Tak for din oprettelse af chaufførkonto hos Truck Wash! Klik på linket for at fuldføre registreringen: ' . $link;
|
|
$gatewayAPI->send([$phoneNumber], $message);
|
|
$delivery = [
|
|
'channel' => 'sms',
|
|
'status' => 'sent',
|
|
'message' => 'Invite sent successfully.',
|
|
];
|
|
}
|
|
} catch (Exception $exception) {
|
|
$delivery = [
|
|
'channel' => 'sms',
|
|
'status' => 'failed',
|
|
'message' => $exception->getMessage(),
|
|
];
|
|
}
|
|
|
|
return [
|
|
'setup_token' => $token,
|
|
'setup_link' => $link,
|
|
'delivery' => $delivery,
|
|
];
|
|
}
|
|
|
|
private function buildSubuserManagementPayload(subusers_o $subuser, int $customerNumber): array
|
|
{
|
|
$grant = (new subuser_grants_o())->getGrantForSubuserAndCustomer((int)$subuser->id, $customerNumber, true);
|
|
$grantPermissions = $grant ? $this->parsePermissionsPayload($grant->permissions->value(), []) : [];
|
|
$setupRequired = $subuser->requiresSetup();
|
|
$grantEnabled = $grant ? (bool)$grant->enabled->value() : false;
|
|
$inviteAccepted = !$setupRequired;
|
|
|
|
$accessState = 'inactive';
|
|
if ($grant !== null && $grantEnabled) {
|
|
$accessState = $setupRequired ? 'pending_setup' : 'active';
|
|
} elseif ($grant !== null) {
|
|
$accessState = 'disabled';
|
|
}
|
|
|
|
return [
|
|
'id' => (int)$subuser->id,
|
|
'username' => $subuser->username->value(),
|
|
'name' => $subuser->name->value(),
|
|
'email' => $subuser->email->value(),
|
|
'phone_country_code' => $subuser->phone_country_code->value() !== null ? (int)$subuser->phone_country_code->value() : null,
|
|
'phone' => $subuser->phone->value() !== null ? (int)$subuser->phone->value() : null,
|
|
'created_at' => $subuser->created_at->value() ?? null,
|
|
'updated_at' => $subuser->updated_at->value() ?? null,
|
|
'suspended_at' => $subuser->suspended_at->value() ?? null,
|
|
'two_factor_enabled' => $subuser->isTwoFactorEnabled(),
|
|
'setup_required' => $setupRequired,
|
|
'invite_accepted' => $inviteAccepted,
|
|
'can_resend_invite' => $setupRequired,
|
|
'profile_editable_by_manager' => false,
|
|
'grant_id' => $grant ? (int)$grant->id : null,
|
|
'grant_enabled' => $grantEnabled,
|
|
'grant_note' => $grant ? $grant->note->value() : null,
|
|
'grant_permissions' => $grantPermissions,
|
|
'permissions' => $grantPermissions,
|
|
'access_state' => $accessState,
|
|
];
|
|
}
|
|
|
|
private function buildCurrentSubuserPayload(subusers_o $subuser): array
|
|
{
|
|
$grants = (new subuser_grants_o())->getFieldsWhere([
|
|
'subuser' => $subuser->id,
|
|
'enabled' => 1,
|
|
'deleted_at' => null,
|
|
], ['permissions', 'billing_customer_number']);
|
|
|
|
return [
|
|
'id' => (int)$subuser->id,
|
|
'username' => $subuser->username->value(),
|
|
'name' => $subuser->name->value(),
|
|
'email' => $subuser->email->value(),
|
|
'phone_country_code' => $subuser->phone_country_code->value() !== null ? (int)$subuser->phone_country_code->value() : null,
|
|
'phone' => $subuser->phone->value() !== null ? (int)$subuser->phone->value() : null,
|
|
'grants' => array_map(function ($grant) {
|
|
return [
|
|
'name' => (new users_o())->getCustomerName((int)$grant['billing_customer_number']),
|
|
'billing_customer_number' => (int)$grant['billing_customer_number'],
|
|
'permissions' => $this->parsePermissionsPayload($grant['permissions'] ?? null, []),
|
|
];
|
|
}, $grants),
|
|
'created_at' => $subuser->created_at->value() ?? null,
|
|
'updated_at' => $subuser->updated_at->value() ?? null,
|
|
'suspended_at' => $subuser->suspended_at->value() ?? null,
|
|
'two_factor_enabled' => $subuser->isTwoFactorEnabled(),
|
|
];
|
|
}
|
|
|
|
public function run(): void
|
|
{
|
|
// =============================
|
|
// Subuser Grant Management
|
|
// =============================
|
|
$this->get('/subusers/grants', function () {
|
|
global $response;
|
|
/** Permissions (subuser-aware) */
|
|
$permission_own = self::definePermission('list_own_subuser_grants', subusers_permission_node_key::SUBUSERS_LIST);
|
|
$permission_other = self::definePermission('list_subuser_grants');
|
|
$has_permission_other = self::hasPermission($permission_other);
|
|
|
|
// Optional filters: customer_number, subuser_id
|
|
$filters = ['deleted_at' => null];
|
|
$hasFilter = false;
|
|
$requestedCustomer = null;
|
|
if (self::isParametersSet(['customer_number'])) {
|
|
$customer_number = (int)self::getParameter('customer_number');
|
|
self::requireType($customer_number, self::type_int());
|
|
$filters['billing_customer_number'] = $customer_number;
|
|
$requestedCustomer = (int)$customer_number;
|
|
$hasFilter = true;
|
|
}
|
|
if (self::isParametersSet(['subuser_id'])) {
|
|
$subuser_id = (int)self::getParameter('subuser_id');
|
|
self::requireType($subuser_id, self::type_int());
|
|
$filters['subuser'] = $subuser_id;
|
|
$hasFilter = true;
|
|
}
|
|
|
|
// Effective customer context (classic or subuser header)
|
|
$effectiveCustomer = self::resolveEffectiveCustomerNumber();
|
|
|
|
// Allow via own-scope or admin-scope
|
|
self::allowOwnOrDepartmentAccess(
|
|
$permission_own,
|
|
$permission_other,
|
|
$requestedCustomer !== null ? (int)$requestedCustomer : ($effectiveCustomer !== null ? (int)$effectiveCustomer : null),
|
|
null,
|
|
null,
|
|
'You do not have permission to list subuser grants.'
|
|
);
|
|
|
|
// If not admin/department permission, force restrict to effective customer
|
|
if (!$has_permission_other) {
|
|
if ($effectiveCustomer === null) {
|
|
$response->forbidden([$permission_other->permission]);
|
|
}
|
|
$filters['billing_customer_number'] = (int)$effectiveCustomer;
|
|
$hasFilter = true; // ensure we don't fail below
|
|
}
|
|
|
|
// Fallback: try to infer from session for classic users
|
|
if (!$hasFilter) {
|
|
$user = (new authentication())->get_user();
|
|
if ($user && !empty($user->customer_number->value())) {
|
|
$filters['billing_customer_number'] = (int)$user->customer_number->value();
|
|
$hasFilter = true;
|
|
}
|
|
}
|
|
|
|
if (!$hasFilter) {
|
|
$response->error('You must provide at least one of: customer_number or subuser_id', 400);
|
|
}
|
|
|
|
$paginatedResponse = (new subuser_grants_o())->listObjectsWithPaginationIfSet(function ($o) {
|
|
$o = (object)$o;
|
|
$subuser = (new subusers_o())->select((int)$o->subuser);
|
|
return [
|
|
'id' => $o->id,
|
|
'billing_customer_number' => $o->billing_customer_number,
|
|
'subuser' => $o->subuser,
|
|
'name' => $subuser->name->value(),
|
|
'enabled' => $o->enabled,
|
|
'note' => $o->note,
|
|
'permissions' => json_decode($o->permissions, true) ?: [],
|
|
'created_at' => $o->created_at,
|
|
'updated_at' => $o->updated_at,
|
|
];
|
|
}, $filters);
|
|
$response->success($paginatedResponse);
|
|
}, [
|
|
'list_subuser_grants' => 'List subuser grants for any customer (admin).',
|
|
'list_own_subuser_grants' => 'List subuser grants for the effective customer. Subusers require node: SUBUSERS_LIST and X-Customer-Number header.'
|
|
]);
|
|
|
|
$this->post('/subusers/grants', function () {
|
|
global $response;
|
|
/** Permissions (subuser-aware) */
|
|
$permission_own = self::definePermission('add_own_subusers', subusers_permission_node_key::SUBUSERS_ADD);
|
|
$permission_other = self::definePermission('manage_subuser_grants');
|
|
|
|
self::requireParameters(['customer_number', 'subuser_id']);
|
|
$customer_number = (int)self::getParameter('customer_number');
|
|
$subuser_id = (int)self::getParameter('subuser_id');
|
|
self::requireType($customer_number, self::type_int());
|
|
self::requireType($subuser_id, self::type_int());
|
|
|
|
if (!self::hasPermission($permission_other, (int)$customer_number)) {
|
|
$this->requireManagedCustomerScope(subusers_permission_node_key::SUBUSERS_ADD, (int)$customer_number);
|
|
}
|
|
|
|
$enabled = true;
|
|
if (self::isParametersSet(['enabled'])) {
|
|
$enabled = (bool)filter_var(self::getParameter('enabled'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
if ($enabled === null) { $enabled = true; }
|
|
}
|
|
$note = null;
|
|
if (self::isParametersSet(['note'])) {
|
|
$note = (string)self::getParameter('note');
|
|
self::requireType($note, self::type_string());
|
|
self::requireMaxLength('note', 65535);
|
|
}
|
|
$permissions = null;
|
|
if (self::isParametersSet(['permissions'])) {
|
|
$raw = self::getParameter('permissions');
|
|
// Expect array (already parsed) or JSON string
|
|
if (is_string($raw)) {
|
|
$decoded = json_decode($raw, true);
|
|
if (!is_array($decoded)) {
|
|
$response->error('Invalid permissions payload', 400);
|
|
}
|
|
$permissions = $decoded;
|
|
} elseif (is_array($raw)) {
|
|
$permissions = $raw;
|
|
} else {
|
|
$response->error('Invalid permissions type', 400);
|
|
}
|
|
// Validate each permission is a known key
|
|
foreach ($permissions as $perm) {
|
|
if (!is_string($perm) || subusers_permission_node_key::tryFrom($perm) === null) {
|
|
$response->error('Unknown permission key: ' . (string)$perm, 400);
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
$grant = (new subuser_grants_o())->add($customer_number, $subuser_id, (bool)$enabled, $note, $permissions);
|
|
$response->success(['grant' => $grant->asArray()]);
|
|
} catch (Exception $e) {
|
|
$response->error('Failed to add subuser grant', 500);
|
|
}
|
|
}, [
|
|
'manage_subuser_grants' => 'Create subuser grants for any customer (admin).',
|
|
'add_own_subusers' => 'Create subuser grants for own customer. Subusers require node: SUBUSERS_ADD and X-Customer-Number header.'
|
|
]);
|
|
|
|
$this->put('/subusers/grants', function () {
|
|
global $response;
|
|
/** Permissions (subuser-aware) */
|
|
$permission_own = self::definePermission('edit_own_subusers', subusers_permission_node_key::SUBUSERS_EDIT);
|
|
$permission_delete_own = self::definePermission('delete_own_subusers', subusers_permission_node_key::SUBUSERS_DELETE);
|
|
$permission_other = self::definePermission('manage_subuser_grants');
|
|
|
|
self::requireParameters(['id']);
|
|
self::requireType(self::getParameter('id'), self::type_int());
|
|
$grant = (new subuser_grants_o())->select((int)self::getParameter('id'));
|
|
if (!$grant->exists()) {
|
|
$response->error('Grant not found', 404);
|
|
}
|
|
|
|
$targetCustomer = (int)$grant->billing_customer_number->value();
|
|
if (!self::hasPermission($permission_other, $targetCustomer)) {
|
|
$this->requireManagedCustomerScope(subusers_permission_node_key::SUBUSERS_EDIT, $targetCustomer);
|
|
}
|
|
|
|
if (self::isParametersSet(['enabled'])) {
|
|
$enabledPreview = filter_var(self::getParameter('enabled'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
if ($enabledPreview === false && !self::hasPermission($permission_other)) {
|
|
$this->requireManagedCustomerScope(subusers_permission_node_key::SUBUSERS_DELETE, $targetCustomer);
|
|
}
|
|
}
|
|
|
|
// Update fields provided in the request
|
|
if (self::isParametersSet(['enabled'])) {
|
|
$enabled = (bool)filter_var(self::getParameter('enabled'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
if ($enabled === null) { $enabled = false; }
|
|
$grant->enabled->set($enabled);
|
|
}
|
|
if (self::isParametersSet(['note'])) {
|
|
$note = (string)self::getParameter('note');
|
|
self::requireType($note, self::type_string());
|
|
self::requireMaxLength('note', 65535);
|
|
$grant->note->set($note);
|
|
}
|
|
if (self::isParametersSet(['permissions'])) {
|
|
$raw = self::getParameter('permissions');
|
|
// Expect array (already parsed) or JSON string
|
|
if (is_string($raw)) {
|
|
$decoded = json_decode($raw, true);
|
|
if (!is_array($decoded)) {
|
|
$response->error('Invalid permissions payload', 400);
|
|
}
|
|
$permissions = $decoded;
|
|
} elseif (is_array($raw)) {
|
|
$permissions = $raw;
|
|
} else {
|
|
$response->error('Invalid permissions type', 400);
|
|
}
|
|
// Validate each permission is a known key
|
|
foreach ($permissions as $perm) {
|
|
if (!is_string($perm) || subusers_permission_node_key::tryFrom($perm) === null) {
|
|
$response->error('Unknown permission key: ' . (string)$perm, 400);
|
|
}
|
|
}
|
|
$grant->permissions->set($permissions);
|
|
}
|
|
$response->success($grant->asArray());
|
|
},
|
|
[
|
|
'manage_subuser_grants' => 'Edit subuser grants for any customer (admin).',
|
|
'edit_own_subusers' => 'Edit subuser grants for own customer. Subusers require node: SUBUSERS_EDIT and X-Customer-Number header.',
|
|
'delete_own_subusers' => 'Disable subuser grants for own customer. Subusers require node: SUBUSERS_DELETE and X-Customer-Number header.'
|
|
]
|
|
);
|
|
|
|
$this->get('/subusers/permission-nodes', function () {
|
|
global $response;
|
|
// Build groups
|
|
$groups = [
|
|
new subusers_permission_nodes_bookings(),
|
|
new subusers_permission_nodes_orders(),
|
|
new subusers_permission_nodes_selfserve(),
|
|
new subusers_permission_nodes_subusers(),
|
|
new subusers_permission_nodes_vehicles(),
|
|
];
|
|
$out = [];
|
|
foreach ($groups as $group) {
|
|
$nodes = [];
|
|
foreach ($group->nodes as $key => $node) {
|
|
$nodes[] = [
|
|
'key' => $key,
|
|
'name' => $node->name,
|
|
'description' => $node->description,
|
|
'type' => $node->type->name,
|
|
'default' => (bool)($node->value ?? false),
|
|
];
|
|
}
|
|
$out[] = [
|
|
'group' => $group->group_name,
|
|
'description' => $group->description,
|
|
'nodes' => $nodes,
|
|
];
|
|
}
|
|
$response->success($out);
|
|
}, []);
|
|
|
|
$this->post('/subusers', function () {
|
|
global /** @var response $response */
|
|
$response;
|
|
self::requireParameters([
|
|
'cvr',
|
|
'phone_country_code',
|
|
'phone'
|
|
]);
|
|
|
|
$cvr = (int)self::getParameter('cvr');
|
|
$phone_country_code =(int) self::getParameter('phone_country_code');
|
|
$phone = (int)self::getParameter('phone');
|
|
/** Validation */
|
|
// Check if the CVR number is valid (8 digits)
|
|
self::requireType($cvr, self::type_int());
|
|
self::requireMinLength('cvr', 8);
|
|
self::requireMaxLength('cvr', 8);
|
|
// Check if the phone country code is valid (1-3 digits)
|
|
self::requireType($phone_country_code, self::type_int());
|
|
self::requireMinLength('phone_country_code', 1);
|
|
self::requireMaxLength('phone_country_code', 3);
|
|
// Check if the phone number is valid (4-15 digits)
|
|
self::requireType($phone, self::type_int());
|
|
self::requireMinLength('phone', 4);
|
|
self::requireMaxLength('phone', 15);
|
|
// Check if the CVR is registered to a company in the database
|
|
$economic = new economic();
|
|
$results = $economic->customers->customers->search([
|
|
'corporateIdentificationNumber' => (string)$cvr,
|
|
],[
|
|
'skipPages' => 0,
|
|
'pageSize' => 1000, // Since the limit is 1000, we need to set the page size to 1000.
|
|
])->collection;
|
|
if (count($results) === 0) {
|
|
$response->error('Customer not found', 404);
|
|
}
|
|
/** At this point, we know that the CVR is valid and that the company exists in the database. */
|
|
$company_user = (new users_o())->getUserByCustomerNumber((int)$results[0]->customerNumber);
|
|
$company_user->requireSelected();
|
|
/** We can now check if a subuser already exists with the same phone number. */
|
|
$subuser = (new subusers_o())->getSubuserByPhone($phone_country_code, $phone);
|
|
if ($subuser !== null) {
|
|
$response->error('Account already exists with this phone number', 400);
|
|
}
|
|
/** We can now create the subuser and send a request to the company user to link the subuser to the company. */
|
|
$subuser = (new subusers_o())->add(
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
(int)$phone_country_code,
|
|
(int)$phone
|
|
);
|
|
// Send an SMS with a link to complete the registration process.
|
|
$gatewayAPI = new gatewayapi();
|
|
if ($gatewayAPI->isEnabled()) {
|
|
$token = $subuser->generateSetupToken();
|
|
$link = 'https://truckwash.io/complete-registration?token=' . $token;
|
|
$message = 'Tak for din oprettelse af chaufførkonto hos Truck Wash! Klik på linket for at fuldføre registreringen: ' . $link;
|
|
$phone_number_array = [(string)$phone_country_code . (string)$phone];
|
|
$gatewayAPI->send($phone_number_array, $message);
|
|
}
|
|
// Add the grant request
|
|
$subuser_grants_o = new subuser_grants_o();
|
|
try {
|
|
$subuser_grants_o->add($results[0]->customerNumber, $subuser->id, false, null);
|
|
} catch (Exception $e) {
|
|
$response->error('Failed to add subuser grant', 500);
|
|
}
|
|
$response->success(['cvr' => $cvr, 'customer_number' => $results[0]->customerNumber]);
|
|
// Code for creating a new subuser would go here
|
|
});
|
|
$this->get('/subusers/setup', function () {
|
|
// Require the user to be logged in
|
|
global $response;
|
|
self::requireParameters(['token']);
|
|
$token = self::getParameter('token');
|
|
// Get the subuser with the setup token
|
|
$subuser = (new subusers_o())->getSubuserBySetupToken($token);
|
|
if ($subuser === null) {
|
|
$response->error('Invalid or expired token', 400);
|
|
}
|
|
$response->success(['message' => 'Token is valid', 'subuser_id' => $subuser->id]);
|
|
});
|
|
$this->post('/subusers/setup', function () {
|
|
// Require the user to be logged in
|
|
global $response;
|
|
self::requireParameters(['token', 'password', 'name']);
|
|
$token = (string)self::getParameter('token');
|
|
$password = (string)self::getParameter('password');
|
|
$name = (string)self::getParameter('name');
|
|
self::requireType($password, self::type_string());
|
|
self::requireMinLength('password', 8);
|
|
self::requireMaxLength('password', 255);
|
|
self::requireRegex($password, '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/', 'Password must contain at least one uppercase letter, one lowercase letter, and one number');
|
|
self::requireType($name, self::type_string());
|
|
self::requireMinLength('name', 3);
|
|
self::requireMaxLength('name', 255);
|
|
/**
|
|
* Optional fields:
|
|
* - username: string, 3-255 characters
|
|
* - email: string, valid email format, 3-255 characters
|
|
*/
|
|
$username = null;
|
|
$email = null;
|
|
if (self::isParametersSet(['username'])) {
|
|
$username = (string)self::getParameter('username');
|
|
self::requireType($username, self::type_string());
|
|
self::requireMinLength('username', 3);
|
|
self::requireMaxLength('username', 255);
|
|
}
|
|
if (self::isParametersSet(['email'])) {
|
|
$email = (string)self::getParameter('email');
|
|
self::requireType($email, self::type_string());
|
|
self::requireMinLength('email', 3);
|
|
self::requireMaxLength('email', 255);
|
|
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
$response->error('Invalid email format', 400);
|
|
}
|
|
}
|
|
// Get the subuser with the setup token
|
|
$subuser = (new subusers_o())->getSubuserBySetupToken($token);
|
|
if ($subuser === null) {
|
|
$response->error('Invalid or expired token', 400);
|
|
}
|
|
// Set the password for the subuser
|
|
try {
|
|
$subuser->setPassword($password);
|
|
if (!empty($username) || !empty($email) || !empty($name)) {
|
|
$subuser->update([
|
|
...(!empty($username) ? ['username' => $username] : []),
|
|
...(!empty($email) ? ['email' => $email] : []),
|
|
...(!empty($name) ? ['name' => $name] : []),
|
|
]);
|
|
}
|
|
// Invalidate the setup token
|
|
$subuser->invalidateSetupToken($token);
|
|
$response->success(['message' => 'Complete registration successful']);
|
|
} catch (Exception $e) {
|
|
$response->error($e->getMessage(), 400);
|
|
}
|
|
});
|
|
|
|
$this->post('/subusers/auth/password', function () {
|
|
global $response;
|
|
/**
|
|
* Username types:
|
|
* 1. Phone number & country code
|
|
* 2. Subuser id
|
|
* 3. Username
|
|
*/
|
|
$phone_country_code = null;
|
|
$phone = null;
|
|
$subuser_id = null;
|
|
$username = null;
|
|
if (self::isParametersSet(['phone_country_code', 'phone'])) {
|
|
$phone_country_code = (int)self::getParameter('phone_country_code');
|
|
$phone = (int)self::getParameter('phone');
|
|
self::requireType($phone_country_code, self::type_int());
|
|
self::requireMinLength('phone_country_code', 1);
|
|
self::requireMaxLength('phone_country_code', 3);
|
|
self::requireType($phone, self::type_int());
|
|
self::requireMinLength('phone', 4);
|
|
self::requireMaxLength('phone', 15);
|
|
} elseif (self::isParametersSet(['subuser_id'])) {
|
|
$subuser_id = (int)self::getParameter('subuser_id');
|
|
self::requireType($subuser_id, self::type_int());
|
|
} elseif (self::isParametersSet(['username'])) {
|
|
$username = (string)self::getParameter('username');
|
|
self::requireType($username, self::type_string());
|
|
self::requireMinLength('username', 3);
|
|
self::requireMaxLength('username', 255);
|
|
} else {
|
|
$response->error('You must provide either phone_country_code & phone, subuser_id or username', 400);
|
|
}
|
|
// Get the subuser based on the provided username type
|
|
$subuser = null;
|
|
if ($phone_country_code !== null && $phone !== null) {
|
|
$subuser = (new subusers_o())->getSubuserByPhone($phone_country_code, $phone);
|
|
} elseif ($subuser_id !== null) {
|
|
$subuser = (new subusers_o())->select($subuser_id);
|
|
} elseif ($username !== null) {
|
|
$subuser = (new subusers_o())->getSubuserByUsername($username);
|
|
}
|
|
if ($subuser === null) {
|
|
$response->error('Subuser not found', 404);
|
|
}
|
|
self::requireParameters(['password']);
|
|
$password = (string)self::getParameter('password');
|
|
self::requireType($password, self::type_string());
|
|
self::requireMinLength('password', 8);
|
|
self::requireMaxLength('password', 255);
|
|
try {
|
|
if (password_verify($password, $subuser->password->value())) {
|
|
if ($subuser->isTwoFactorEnabled()) {
|
|
$token = (new authentication())->create_2fa_token($subuser->id, '2FA_VERIFICATION_SUBUSER');
|
|
$response->success(['2fa_required' => true, '2fa_token' => $token]);
|
|
}
|
|
// Generate a new session for the subuser
|
|
$session = $subuser->generateSession();
|
|
$response->success(['session' => $session]);
|
|
} else {
|
|
$response->error('Invalid password', 400);
|
|
}
|
|
} catch (Exception $e) {
|
|
$response->error($e->getMessage(), 500);
|
|
}
|
|
});
|
|
|
|
// =============================
|
|
// Subusers - List & Get (with grant visibility)
|
|
// =============================
|
|
$this->get('/subusers', function () {
|
|
global $response;
|
|
$customerNumber = $this->requireManagedCustomerScope(subusers_permission_node_key::SUBUSERS_LIST);
|
|
$includeNonEnabled = false;
|
|
if (self::isParametersSet(['include_non_enabled'])) {
|
|
$tmp = filter_var(self::getParameter('include_non_enabled'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
$includeNonEnabled = $tmp === null ? false : (bool)$tmp;
|
|
}
|
|
|
|
$existsClause = sprintf(
|
|
"EXISTS (SELECT 1 FROM `subuser_grants` sg WHERE sg.`subuser` = `subusers`.`id` AND %ssg.`deleted_at` IS NULL AND sg.`billing_customer_number` = %d)",
|
|
$includeNonEnabled ? '' : 'sg.`enabled` = 1 AND ',
|
|
$customerNumber
|
|
);
|
|
|
|
$objects = (new subusers_o())
|
|
->listObjectsWithPaginationIfSet(function ($o) use ($customerNumber) {
|
|
$subuser = (new subusers_o())->select((int)$o['id']);
|
|
if (!$subuser->exists()) {
|
|
return null;
|
|
}
|
|
$subuser->getObjectProperties();
|
|
return $this->buildSubuserManagementPayload($subuser, $customerNumber);
|
|
}, null, [], $existsClause);
|
|
|
|
if (is_array($objects)) {
|
|
$objects = array_values(array_filter($objects, static fn ($item) => $item !== null));
|
|
}
|
|
|
|
$response->success($objects);
|
|
}, [
|
|
'list_own_subusers' => 'List chauffeurs for own customer. Subusers require node: SUBUSERS_LIST and X-Customer-Number header.',
|
|
]);
|
|
|
|
$this->get('/subusers/me', function () {
|
|
global $response;
|
|
$subuser = (new authentication())->get_subuser();
|
|
if ($subuser === false) {
|
|
$response->error('Unauthorized', 401);
|
|
}
|
|
|
|
$response->success($this->buildCurrentSubuserPayload($subuser));
|
|
}, []);
|
|
|
|
$this->put('/subusers/me', function () {
|
|
global $response;
|
|
$subuser = (new authentication())->get_subuser();
|
|
if ($subuser === false) {
|
|
$response->error('Unauthorized', 401);
|
|
}
|
|
|
|
$updates = [];
|
|
|
|
if (self::isParametersSet(['name'])) {
|
|
$name = $this->normalizeOptionalString(self::getParameter('name'));
|
|
if ($name === null || strlen($name) < 3 || strlen($name) > 255) {
|
|
$response->error('Name must be between 3 and 255 characters long', 400);
|
|
}
|
|
$updates['name'] = $name;
|
|
}
|
|
|
|
if (self::isParametersSet(['username'])) {
|
|
$username = $this->normalizeOptionalString(self::getParameter('username'));
|
|
if ($username !== null && (strlen($username) < 3 || strlen($username) > 50)) {
|
|
$response->error('Username must be between 3 and 50 characters long', 400);
|
|
}
|
|
$updates['username'] = $username;
|
|
}
|
|
|
|
if (self::isParametersSet(['email'])) {
|
|
$email = $this->normalizeOptionalString(self::getParameter('email'));
|
|
if ($email !== null && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
$response->error('Invalid email format', 400);
|
|
}
|
|
if ($email !== null && strlen($email) > 255) {
|
|
$response->error('Email must be at most 255 characters long', 400);
|
|
}
|
|
$updates['email'] = $email;
|
|
}
|
|
|
|
if (count($updates) === 0) {
|
|
$response->error('No fields to update', 400);
|
|
}
|
|
|
|
$this->assertSubuserIdentifiersAvailable(
|
|
null,
|
|
null,
|
|
$updates['username'] ?? null,
|
|
$updates['email'] ?? null,
|
|
(int)$subuser->id
|
|
);
|
|
|
|
try {
|
|
$subuser->update($updates);
|
|
} catch (Exception $exception) {
|
|
$response->error($exception->getMessage(), 400);
|
|
}
|
|
|
|
$subuser = (new subusers_o())->select((int)$subuser->id);
|
|
$subuser->getObjectProperties();
|
|
$response->success($this->buildCurrentSubuserPayload($subuser));
|
|
}, []);
|
|
|
|
$this->post('/subusers/invite', function () {
|
|
global $response;
|
|
|
|
$customerNumber = $this->requireManagedCustomerScope(subusers_permission_node_key::SUBUSERS_ADD);
|
|
self::requireParameters(['name', 'phone_country_code', 'phone']);
|
|
|
|
$name = $this->normalizeOptionalString(self::getParameter('name'));
|
|
$phoneCountryCode = (int)self::getParameter('phone_country_code');
|
|
$phone = (int)self::getParameter('phone');
|
|
$note = self::isParametersSet(['note']) ? $this->normalizeOptionalString(self::getParameter('note')) : null;
|
|
$enabled = true;
|
|
if (self::isParametersSet(['enabled'])) {
|
|
$tmp = filter_var(self::getParameter('enabled'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
$enabled = $tmp === null ? true : (bool)$tmp;
|
|
}
|
|
$permissions = self::isParametersSet(['permissions'])
|
|
? $this->parsePermissionsPayload(self::getParameter('permissions'), [])
|
|
: null;
|
|
|
|
if ($name === null || strlen($name) < 3 || strlen($name) > 255) {
|
|
$response->error('Name must be between 3 and 255 characters long', 400);
|
|
}
|
|
self::requireType($phoneCountryCode, self::type_int());
|
|
self::requireType($phone, self::type_int());
|
|
self::requireMinLength('phone_country_code', 1);
|
|
self::requireMaxLength('phone_country_code', 3);
|
|
self::requireMinLength('phone', 4);
|
|
self::requireMaxLength('phone', 15);
|
|
if ($note !== null && strlen($note) > 65535) {
|
|
$response->error('Note must be at most 65535 characters long', 400);
|
|
}
|
|
|
|
$subuser = (new subusers_o())->getSubuserByPhone($phoneCountryCode, $phone);
|
|
if ($subuser === null) {
|
|
try {
|
|
$subuser = (new subusers_o())->add(
|
|
null,
|
|
null,
|
|
$name,
|
|
null,
|
|
$phoneCountryCode,
|
|
$phone
|
|
);
|
|
} catch (Exception $exception) {
|
|
$response->error($exception->getMessage(), 400);
|
|
}
|
|
}
|
|
|
|
$grant = (new subuser_grants_o())->getGrantForSubuserAndCustomer((int)$subuser->id, $customerNumber, true);
|
|
if ($grant === null) {
|
|
try {
|
|
$grant = (new subuser_grants_o())->add(
|
|
$customerNumber,
|
|
(int)$subuser->id,
|
|
$enabled,
|
|
$note,
|
|
$permissions ?? subuser_grants_o::defaultPermissions
|
|
);
|
|
} catch (Exception $exception) {
|
|
$response->error('Failed to create subuser grant', 500);
|
|
}
|
|
} else {
|
|
$grantUpdates = ['enabled' => $enabled];
|
|
if (self::isParametersSet(['note'])) {
|
|
$grantUpdates['note'] = $note;
|
|
}
|
|
if ($permissions !== null) {
|
|
$grantUpdates['permissions'] = $permissions;
|
|
}
|
|
try {
|
|
$grant->update($grantUpdates);
|
|
} catch (Exception $exception) {
|
|
$response->error('Failed to update subuser grant', 500);
|
|
}
|
|
$grant = (new subuser_grants_o())->select((int)$grant->id);
|
|
$grant->getObjectProperties();
|
|
}
|
|
|
|
$invite = $this->issueSetupInvite($subuser);
|
|
$response->success([
|
|
'subuser' => $this->buildSubuserManagementPayload($subuser, $customerNumber),
|
|
'grant' => $grant->asArray(),
|
|
'invite' => $invite,
|
|
]);
|
|
}, [
|
|
'add_own_subusers' => 'Invite or link chauffeurs for own customer. Subusers require node: SUBUSERS_ADD and X-Customer-Number header.',
|
|
]);
|
|
|
|
$this->post('/subusers/invite/resend', function () {
|
|
global $response;
|
|
|
|
self::requireParameters(['id']);
|
|
$subuserId = (int)self::getParameter('id');
|
|
self::requireType($subuserId, self::type_int());
|
|
|
|
$customerNumber = $this->requireManagedCustomerScope(subusers_permission_node_key::SUBUSERS_EDIT);
|
|
$subuser = (new subusers_o())->select($subuserId);
|
|
if (!$subuser->exists()) {
|
|
$response->error('Subuser not found', 404);
|
|
}
|
|
$subuser->getObjectProperties();
|
|
|
|
$grant = (new subuser_grants_o())->getGrantForSubuserAndCustomer($subuserId, $customerNumber, true);
|
|
if ($grant === null) {
|
|
$response->error('Subuser grant not found for selected customer', 404);
|
|
}
|
|
|
|
if (!$subuser->requiresSetup()) {
|
|
$response->error('Driver account already accepted the invitation.', 409);
|
|
}
|
|
|
|
$invite = $this->issueSetupInvite($subuser);
|
|
$response->success([
|
|
'subuser' => $this->buildSubuserManagementPayload($subuser, $customerNumber),
|
|
'grant' => $grant->asArray(),
|
|
'invite' => $invite,
|
|
]);
|
|
}, [
|
|
'edit_own_subusers' => 'Resend chauffeur invite for own customer. Subusers require node: SUBUSERS_EDIT and X-Customer-Number header.',
|
|
]);
|
|
|
|
$this->put('/subusers', function () {
|
|
global $response;
|
|
$this->requireManagedCustomerScope(subusers_permission_node_key::SUBUSERS_EDIT);
|
|
$response->error('Customers can only manage subuser grants. Drivers own their account profile.', 403);
|
|
}, [
|
|
'edit_own_subusers' => 'Customers cannot edit chauffeur account profiles. They may only manage grants, permissions, and enabled state.',
|
|
]);
|
|
// Public registration endpoint (alias of POST /subusers) matching OpenAPI: POST /subusers/me
|
|
$this->post('/subusers/me', function () {
|
|
global /** @var response $response */
|
|
$response;
|
|
// Required input
|
|
self::requireParameters([
|
|
'cvr',
|
|
'phone_country_code',
|
|
'phone'
|
|
]);
|
|
|
|
$cvr = (int)self::getParameter('cvr');
|
|
$phone_country_code = (int)self::getParameter('phone_country_code');
|
|
$phone = (int)self::getParameter('phone');
|
|
|
|
// Validation
|
|
self::requireType($cvr, self::type_int());
|
|
self::requireMinLength('cvr', 8);
|
|
self::requireMaxLength('cvr', 8);
|
|
self::requireType($phone_country_code, self::type_int());
|
|
self::requireMinLength('phone_country_code', 1);
|
|
self::requireMaxLength('phone_country_code', 3);
|
|
self::requireType($phone, self::type_int());
|
|
self::requireMinLength('phone', 4);
|
|
self::requireMaxLength('phone', 15);
|
|
|
|
// Look up company by CVR in e-conomic
|
|
$economic = new economic();
|
|
$results = $economic->customers->customers->search([
|
|
'corporateIdentificationNumber' => (string)$cvr,
|
|
], [
|
|
'skipPages' => 0,
|
|
'pageSize' => 1000,
|
|
])->collection;
|
|
if (count($results) === 0) {
|
|
$response->error('Customer not found', 404);
|
|
}
|
|
|
|
// Ensure company user exists (sanity) and phone is not already registered
|
|
$company_user = (new users_o())->getUserByCustomerNumber((int)$results[0]->customerNumber);
|
|
$company_user->requireSelected();
|
|
|
|
$existing = (new subusers_o())->getSubuserByPhone($phone_country_code, $phone);
|
|
if ($existing !== null) {
|
|
$response->error('Account already exists with this phone number', 400);
|
|
}
|
|
|
|
// Create subuser skeleton
|
|
$subuser = (new subusers_o())->add(
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
(int)$phone_country_code,
|
|
(int)$phone
|
|
);
|
|
|
|
// Optionally send SMS with setup link
|
|
$gatewayAPI = new gatewayapi();
|
|
if ($gatewayAPI->isEnabled()) {
|
|
$token = $subuser->generateSetupToken();
|
|
$link = 'https://truckwash.io/complete-registration?token=' . $token;
|
|
$message = 'Tak for din oprettelse af chaufførkonto hos Truck Wash! Klik på linket for at fuldføre registreringen: ' . $link;
|
|
$phone_number_array = [(string)$phone_country_code . (string)$phone];
|
|
$gatewayAPI->send($phone_number_array, $message);
|
|
}
|
|
|
|
// Create a pending grant request for the company
|
|
$subuser_grants_o = new subuser_grants_o();
|
|
try {
|
|
$subuser_grants_o->add($results[0]->customerNumber, $subuser->id, false, null);
|
|
} catch (Exception $e) {
|
|
$response->error('Failed to add subuser grant', 500);
|
|
}
|
|
|
|
$response->success(['cvr' => $cvr, 'customer_number' => $results[0]->customerNumber]);
|
|
});
|
|
}
|
|
}
|