Add subuser permission templates service and related tests

This commit is contained in:
Jeppe Bundgaard
2026-07-08 11:49:40 +02:00
parent f26a427510
commit 6b7592921d
8 changed files with 1937 additions and 0 deletions
+387
View File
@@ -21,6 +21,306 @@ class vehiclesRoute
{
use route_t;
private function routePositiveInt(string $name): int
{
global $response;
$raw = $this->fromRoute($name);
if (!is_string($raw) || !preg_match('/^[1-9][0-9]*$/', $raw)) {
$response->error('Invalid route parameter', 400);
}
return (int)$raw;
}
private function resolveSuperuserVehicleTargetUser(int $userId): array
{
global $response;
$targetUser = (new users_o())->select($userId);
if (!$targetUser->exists()) {
$response->error('User not found', 404);
}
$targetUser->getObjectProperties();
$customerNumber = (int)$targetUser->customer_number->value();
if ($customerNumber <= 0) {
$response->error('Selected user does not have a customer number', 400);
}
return [
'user_id' => (int)$targetUser->id,
'customer_number' => $customerNumber,
'customer_name' => (string)$targetUser->getCustomerName($customerNumber),
];
}
private function addUserScopedVehicleMeta(array $targetUser): void
{
global $response;
$response->add_meta('user_context', $targetUser);
$response->add_meta('vehicles_summary', $this->buildVehicleSummaryForCustomer((int)$targetUser['customer_number']));
}
private function buildVehiclePayload(array $vehicle): array
{
return [...(new customer_vehicles_o())->select((int)$vehicle['id'])->asArray()];
}
private function listVehiclesForCustomer(int $customerNumber): array
{
$vehicles = new customer_vehicles_o();
return $vehicles->listObjectsWithPaginationIfSet(
fn ($vehicle) => $this->buildVehiclePayload($vehicle),
$vehicles->forceRestrictFilters([
'customer_id' => [$customerNumber],
])
);
}
private function buildVehicleSummaryForCustomer(int $customerNumber): array
{
$rows = (new customer_vehicles_o())->getFieldsWhere([
'customer_id' => $customerNumber,
'deleted_at' => null,
], [
'id',
'wash_subscription',
]);
$summary = [
'total' => 0,
'wash_subscription' => 0,
'self_service' => 0,
];
foreach ($rows as $row) {
$summary['total']++;
if ((int)($row['wash_subscription'] ?? 0) === 1) {
$summary['wash_subscription']++;
}
try {
$vehicle = (new customer_vehicles_o())->select((int)$row['id']);
if ($vehicle->exists() && $vehicle->hasXLVask()) {
$summary['self_service']++;
}
} catch (\Throwable) {
// XLVask availability should not prevent the customer vehicle summary from loading.
}
}
return $summary;
}
private function requireScopedVehicle(int $vehicleId, int $customerNumber): customer_vehicles_o
{
global $response;
$vehicle = (new customer_vehicles_o())->select($vehicleId);
if (!$vehicle->exists()) {
$response->error('Vehicle not found', 404);
}
$vehicle->getObjectProperties();
if ((int)$vehicle->customer_id->value() !== $customerNumber) {
$response->error('Vehicle does not belong to selected user', 404);
}
return $vehicle;
}
private function validateOptionalCustomerIdMatches(int $customerNumber): void
{
global $response;
if (!self::isParametersSet(['customer_id'])) {
return;
}
$requestedCustomerNumber = (int)self::getParameter('customer_id');
self::requireType($requestedCustomerNumber, self::type_int());
if ($requestedCustomerNumber !== $customerNumber) {
$response->error('Customer number does not match selected user', 400);
}
}
private function createVehicleForCustomer(int $customerNumber): array
{
global $response;
self::requireParameters([
'type',
'reg',
]);
$this->validateOptionalCustomerIdMatches($customerNumber);
$reference = null;
if (self::isParametersSet(['reference']) && !empty(self::getParameter('reference'))) {
$reference = (string)self::getParameter('reference');
self::requireType($reference, self::type_string());
self::requireMinLength('reference', 1);
self::requireMaxLength('reference', 255);
}
self::requireType(self::getParameter('reg'), self::type_string());
self::requireType(self::getParameter('type'), self::type_int());
$subscription = false;
if (self::isParametersSet(['wash_subscription'])) {
self::requireType(self::getParameter('wash_subscription'), self::type_bool());
$subscription = (bool)self::getParameter('wash_subscription');
}
$reg = trim((string)self::getParameter('reg'));
self::requireMinLength('reg', 2);
self::requireMaxLength('reg', 12);
$type = (int)self::getParameter('type');
$vehicle = new customer_vehicles_o();
$vehicle->add($customerNumber, $type, $reg, $subscription, $reference);
try {
(new economic_v2_versioning_service())->recordVehicleSubscriptionVersion(
[
'vehicle_id' => (int)$vehicle->id,
'customer_number' => $customerNumber,
'reg' => $reg,
'vehicle_type' => $type,
'wash_subscription' => $subscription,
],
date('Y-m-d H:i:s'),
'live.vehicle.route',
1.0,
false,
[
'route' => '/superuser/users/{user_id}/vehicles',
'method' => 'POST',
'actor_user_id' => (int)((new authentication())->get_user()->id ?? 0),
]
);
} catch (\Throwable $exception) {
(new logs_o())->add('vehicles', 'global', 0, (int)((new authentication())->get_user()->id ?? 0), 'ADD_VEHICLE_VERSIONING_FAILED', $exception->getMessage());
}
return $vehicle->asArray();
}
private function updateScopedVehicle(customer_vehicles_o $vehicle, int $customerNumber): array
{
global $response;
$this->validateOptionalCustomerIdMatches($customerNumber);
$beforeState = [
'vehicle_id' => (int)$vehicle->id,
'customer_number' => (int)$vehicle->customer_id->value(),
'reg' => (string)$vehicle->reg->value(),
'vehicle_type' => (int)$vehicle->type->value(),
'wash_subscription' => (bool)$vehicle->wash_subscription->value(),
];
if (self::isParametersSet(['type'])) {
$type = (int)self::getParameter('type');
self::requireType($type, self::type_int());
self::requireMinValue($type, 0);
if ($type === 0) {
$vehicle->type->set(0);
$vehicle->wash_subscription->set(0);
} else {
$product = new products_o();
$product->select($type);
if (!$product->exists() || !$product->subscription_allowed->value()) {
$response->error('Invalid type', 400);
}
$vehicle->type->set($type);
}
}
if (self::isParametersSet(['reg'])) {
$reg = (string)self::getParameter('reg');
self::requireType($reg, self::type_string());
self::requireMinLength('reg', 2);
self::requireMaxLength('reg', 12);
$vehicle->reg->set(preg_replace('/\s+/', '', $reg));
}
if (self::isParametersSet(['wash_subscription'])) {
$subscription = (bool)self::getParameter('wash_subscription');
self::requireType($subscription, self::type_bool());
if ((int)$vehicle->type->value() === 0 && $subscription) {
$response->error('Unable to set subscription, type is not set', 400);
}
$vehicle->wash_subscription->set($subscription ? 1 : 0);
}
if (self::isParametersSet(['reference'])) {
if (empty(self::getParameter('reference'))) {
$vehicle->reference->nullify();
} else {
$reference = (string)self::getParameter('reference');
self::requireType($reference, self::type_string());
self::requireMinLength('reference', 1);
self::requireMaxLength('reference', 255);
$vehicle->reference->set($reference);
}
}
$vehicle->objectChanged();
$afterState = [
'vehicle_id' => (int)$vehicle->id,
'customer_number' => (int)$vehicle->customer_id->value(),
'reg' => (string)$vehicle->reg->value(),
'vehicle_type' => (int)$vehicle->type->value(),
'wash_subscription' => (bool)$vehicle->wash_subscription->value(),
];
$versionRelevantChange = (
(string)$beforeState['reg'] !== (string)$afterState['reg'] ||
(int)$beforeState['vehicle_type'] !== (int)$afterState['vehicle_type'] ||
(bool)$beforeState['wash_subscription'] !== (bool)$afterState['wash_subscription']
);
if ($versionRelevantChange) {
try {
$versioning = new economic_v2_versioning_service();
$effectiveAt = date('Y-m-d H:i:s');
if ((string)$beforeState['reg'] !== (string)$afterState['reg']) {
$versioning->closeActiveVehicleSubscriptionVersion(
(int)$beforeState['customer_number'],
(string)$beforeState['reg'],
$effectiveAt,
'live.vehicle.route',
1.0,
false,
[
'route' => '/superuser/users/{user_id}/vehicles',
'method' => 'PUT',
'actor_user_id' => (int)((new authentication())->get_user()->id ?? 0),
'reason' => 'identity_change',
]
);
}
$versioning->recordVehicleSubscriptionVersion(
$afterState,
$effectiveAt,
'live.vehicle.route',
1.0,
false,
[
'route' => '/superuser/users/{user_id}/vehicles',
'method' => 'PUT',
'actor_user_id' => (int)((new authentication())->get_user()->id ?? 0),
]
);
} catch (\Throwable $exception) {
(new logs_o())->add('vehicles', 'global', 0, (int)((new authentication())->get_user()->id ?? 0), 'EDIT_VEHICLE_VERSIONING_FAILED', $exception->getMessage());
}
}
return $vehicle->asArray();
}
public function run(): void
{
$this->get('/vehicles', function () {
@@ -119,6 +419,93 @@ class vehiclesRoute
]
);
$this->get('/superuser/users/{user_id}/vehicles', function () {
global $response;
$this->requirePermission('list_vehicles_other');
$targetUser = $this->resolveSuperuserVehicleTargetUser($this->routePositiveInt('user_id'));
$this->addUserScopedVehicleMeta($targetUser);
$response->success($this->listVehiclesForCustomer((int)$targetUser['customer_number']));
}, [
'list_vehicles_other' => 'List vehicles for a selected superuser customer account.',
]);
$this->get('/superuser/users/{user_id}/vehicles/summary', function () {
global $response;
$this->requirePermission('list_vehicles_other');
$targetUser = $this->resolveSuperuserVehicleTargetUser($this->routePositiveInt('user_id'));
$response->add_meta('user_context', $targetUser);
$response->success($this->buildVehicleSummaryForCustomer((int)$targetUser['customer_number']));
}, [
'list_vehicles_other' => 'Summarize vehicles for a selected superuser customer account.',
]);
$this->post('/superuser/users/{user_id}/vehicles', function () {
global $response;
$this->requirePermission('add_vehicle_other');
$targetUser = $this->resolveSuperuserVehicleTargetUser($this->routePositiveInt('user_id'));
$response->add_meta('user_context', $targetUser);
$response->success($this->createVehicleForCustomer((int)$targetUser['customer_number']));
}, [
'add_vehicle_other' => 'Add a vehicle for a selected superuser customer account.',
]);
$this->put('/superuser/users/{user_id}/vehicles', function () {
global $response;
$this->requirePermission('edit_vehicle_other');
self::requireParameters(['id']);
$targetUser = $this->resolveSuperuserVehicleTargetUser($this->routePositiveInt('user_id'));
$vehicleId = (int)self::getParameter('id');
self::requireType($vehicleId, self::type_int());
self::requireMinValue($vehicleId, 1);
$vehicle = $this->requireScopedVehicle($vehicleId, (int)$targetUser['customer_number']);
$response->add_meta('user_context', $targetUser);
$response->success($this->updateScopedVehicle($vehicle, (int)$targetUser['customer_number']));
}, [
'edit_vehicle_other' => 'Edit a vehicle for a selected superuser customer account.',
]);
$this->delete('/superuser/users/{user_id}/vehicles', function () {
global $response;
$this->requirePermission('delete_vehicle_other');
self::requireParameters(['id']);
$targetUser = $this->resolveSuperuserVehicleTargetUser($this->routePositiveInt('user_id'));
$vehicleId = (int)self::getParameter('id');
self::requireType($vehicleId, self::type_int());
self::requireMinValue($vehicleId, 1);
$vehicle = $this->requireScopedVehicle($vehicleId, (int)$targetUser['customer_number']);
$beforeState = [
'customer_number' => (int)$vehicle->customer_id->value(),
'reg' => (string)$vehicle->reg->value(),
];
$vehicle->delete();
try {
(new economic_v2_versioning_service())->closeActiveVehicleSubscriptionVersion(
(int)$beforeState['customer_number'],
(string)$beforeState['reg'],
date('Y-m-d H:i:s'),
'live.vehicle.route',
1.0,
false,
[
'route' => '/superuser/users/{user_id}/vehicles',
'method' => 'DELETE',
'actor_user_id' => (int)((new authentication())->get_user()->id ?? 0),
]
);
} catch (\Throwable $exception) {
(new logs_o())->add('vehicles', 'global', 0, (int)((new authentication())->get_user()->id ?? 0), 'DELETE_VEHICLE_VERSIONING_FAILED', $exception->getMessage());
}
$response->add_meta('user_context', $targetUser);
$response->success([
'success' => true,
'message' => 'Vehicle deleted successfully',
]);
}, [
'delete_vehicle_other' => 'Delete a vehicle for a selected superuser customer account.',
]);
$this->post('/vehicles', function () {
global $response;
$auth = new authentication();