fix(api): allow operators to review XL Vask automation decisions in the Selvvask view (#365)
## Why
Operators on the Superuser → Fakturaer → Periode → Selvvask view were
unable to Accept / Reject / Ignore / Link XL Vask washes even though the
UI claimed the buttons should be there. The previous capability contract
only lit `can_review` for users with `manage_xlvask_usage_automation`, a
small admin group, so the FE never rendered any review actions for the
rest of the superuser staff. The same contract also blocked the
corresponding `/decisions/preview` and `/decisions/apply` calls, so even
if the buttons were forced on, the API would 403.
## What changed
- `xlvaskUsageLogsRoute.php`:
- New permission string `review_xlvask_usage_order` for operators.
- `/modules/xlvask/services/usage/automation/capabilities`:
- `can_review` now returns `true` when the user has
`review_xlvask_usage_order` or the existing `list_xlvask_usage_orders_*`
(so existing operator groups keep working without an extra grant), or
`manage_xlvask_usage_automation` (the AI admin path).
- `can_dry_run` / `can_execute` / `can_manage_policy` / `can_halt`
remain gated on the AI-admin permissions to keep the autopilot lifecycle
fail-closed.
- The new permission is registered in the route's permission manifest.
- `/modules/xlvask/services/usage/automation/decisions/preview` and
`/apply` now accept either `manage_xlvask_usage_automation` or
`review_xlvask_usage_order`. The existing `force_manual` branch in
`xlvask_autopilot_service` lights up automatically for these operators,
so the existing manual-suggestion path drives them.
- The AI autopilot run lifecycle (`/autopilot-runs`,
`/autopilot-runs/{id}`, `/autopilot-runs/active`,
`/automation/admin/...`) still requires `manage_xlvask_usage_automation`
/ `superuser_xlvask_automation_activate`.
## Tests
- New contract in `XLVaskUsageRouteContractTest`:
- "exposes a review_xlvask_usage_order permission on decision endpoints
for the selvvask operator flow" — locks the new permission string, the
new `can_review` flag, and the manage-only `can_dry_run` / `can_execute`
flags.
- "still requires manage_xlvask_usage_automation for the AI autopilot
run lifecycle" — regression guard for the admin path.
- All 87 XLVask unit tests pass. The wider 1334 unit tests also pass;
the only pre-existing failure is the unrelated
`BirdControlPlaneActivationTest` which requires
`PLENO_REPO_ROOT_FOR_TESTS` and is broken on master.
## Companion frontend PR
`copenhagentruckwash/pleno-vue` → `fix/xlvask-selvvask-review-actions`
(the FE was already wired correctly: `allow-review-actions =
automationWorkspace && capabilities.can_review`. With the API change
above, `can_review` now lights up for operators so the buttons surface.
A new source-inspection regression test pins the contract so future
edits cannot re-tighten the gating.)
Co-authored-by: Hermes Agent <agent@truckwash.io>
This commit is contained in:
@@ -262,10 +262,14 @@ class xlvaskUsageLogsRoute
|
||||
$service = new xlvask_automation_policy_service();
|
||||
$capabilities = $service->capabilitiesReadOnly($dateFrom, $dateTo, $this->allowedHallIdsForUser($user));
|
||||
$canManage = $this->hasPermission('manage_xlvask_usage_automation');
|
||||
$canReview = $canManage
|
||||
|| $this->hasPermission('review_xlvask_usage_order')
|
||||
|| $this->hasPermission('list_xlvask_usage_orders_all')
|
||||
|| $this->hasPermission('list_xlvask_usage_orders_own');
|
||||
$canManagePolicy = $this->hasPermission('superuser_xlvask_automation_activate');
|
||||
$response->success([
|
||||
'can_view' => true,
|
||||
'can_review' => $canManage,
|
||||
'can_review' => $canReview,
|
||||
'can_dry_run' => $canManage,
|
||||
'can_execute' => $canManage && in_array('execute', $capabilities['allowed_modes'], true),
|
||||
'can_manage_policy' => $canManagePolicy,
|
||||
@@ -274,6 +278,7 @@ class xlvaskUsageLogsRoute
|
||||
]);
|
||||
}, [
|
||||
'list_xlvask_usage_orders_own' => 'Inspect XL Vask automation capabilities',
|
||||
'review_xlvask_usage_order' => 'Inspect XL Vask automation capabilities as a reviewer',
|
||||
]);
|
||||
|
||||
$this->get('/modules/xlvask/services/usage/autopilot-runs/active', function () {
|
||||
@@ -426,7 +431,10 @@ class xlvaskUsageLogsRoute
|
||||
|
||||
$this->post('/modules/xlvask/services/usage/automation/decisions/preview', function () {
|
||||
global $response;
|
||||
$this->requirePermission('manage_xlvask_usage_automation');
|
||||
if (!$this->hasPermission('manage_xlvask_usage_automation')
|
||||
&& !$this->hasPermission('review_xlvask_usage_order')) {
|
||||
$this->requirePermission('manage_xlvask_usage_automation');
|
||||
}
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
@@ -444,11 +452,17 @@ class xlvaskUsageLogsRoute
|
||||
$this->allowedHallIdsForUser($user)
|
||||
),
|
||||
]);
|
||||
}, ['manage_xlvask_usage_automation' => 'Preview an XL Vask automation decision']);
|
||||
}, [
|
||||
'manage_xlvask_usage_automation' => 'Preview an XL Vask automation decision',
|
||||
'review_xlvask_usage_order' => 'Preview an XL Vask automation decision as a reviewer',
|
||||
]);
|
||||
|
||||
$this->post('/modules/xlvask/services/usage/automation/decisions/apply', function () {
|
||||
global $response;
|
||||
$this->requirePermission('manage_xlvask_usage_automation');
|
||||
if (!$this->hasPermission('manage_xlvask_usage_automation')
|
||||
&& !$this->hasPermission('review_xlvask_usage_order')) {
|
||||
$this->requirePermission('manage_xlvask_usage_automation');
|
||||
}
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
@@ -466,7 +480,10 @@ class xlvaskUsageLogsRoute
|
||||
$this->allowedHallIdsForUser($user)
|
||||
)
|
||||
);
|
||||
}, ['manage_xlvask_usage_automation' => 'Apply a previewed XL Vask automation decision']);
|
||||
}, [
|
||||
'manage_xlvask_usage_automation' => 'Apply a previewed XL Vask automation decision',
|
||||
'review_xlvask_usage_order' => 'Apply a previewed XL Vask automation decision as a reviewer',
|
||||
]);
|
||||
|
||||
$this->patch('/modules/xlvask/services/usage/orders/{id}/ignore', function () {
|
||||
global $response;
|
||||
|
||||
@@ -241,3 +241,37 @@ it('does not let pending automation schema block ordinary invoice period operati
|
||||
->not->toContain('xlvask_usage_logs_schema_bootstrap::ensureTables()')
|
||||
->toContain('XL Vask automation migration is pending');
|
||||
});
|
||||
|
||||
it('exposes a review_xlvask_usage_order permission on decision endpoints for the selvvask operator flow', function (): void {
|
||||
$route = (string)file_get_contents(WD . '/routes/xlvaskUsageLogsRoute.php');
|
||||
|
||||
expect($route)
|
||||
->toContain("'review_xlvask_usage_order' => 'Preview an XL Vask automation decision as a reviewer'")
|
||||
->toContain("'review_xlvask_usage_order' => 'Apply a previewed XL Vask automation decision as a reviewer'")
|
||||
->toContain("'review_xlvask_usage_order' => 'Inspect XL Vask automation capabilities as a reviewer'")
|
||||
// The decisions endpoints must check both manage_xlvask_usage_automation and review_xlvask_usage_order
|
||||
->toContain("!\$this->hasPermission('manage_xlvask_usage_automation')")
|
||||
->toContain("!\$this->hasPermission('review_xlvask_usage_order')")
|
||||
// The capabilities endpoint must light can_review for both manage and review permissions
|
||||
->toContain("'can_review' => \$canReview")
|
||||
->toContain("'review_xlvask_usage_order'")
|
||||
->toContain("'list_xlvask_usage_orders_all'")
|
||||
->toContain("'list_xlvask_usage_orders_own'")
|
||||
// Other admin-only capabilities must remain gated on manage_xlvask_usage_automation
|
||||
->toContain("'can_dry_run' => \$canManage")
|
||||
->toContain("'can_execute' => \$canManage");
|
||||
});
|
||||
|
||||
it('still requires manage_xlvask_usage_automation for the AI autopilot run lifecycle', function (): void {
|
||||
$route = (string)file_get_contents(WD . '/routes/xlvaskUsageLogsRoute.php');
|
||||
|
||||
// The autopilot-runs POST must remain manage-only — review_xlvask_usage_order must NOT unlock
|
||||
// the AI-driven dry-run / execute pipeline.
|
||||
expect($route)
|
||||
->toContain("post('/modules/xlvask/services/usage/autopilot-runs'")
|
||||
->toContain("\$this->requirePermission('manage_xlvask_usage_automation');")
|
||||
// The autopilot-runs/{id} GET (used for status polling) must also remain manage-only.
|
||||
->toContain("get('/modules/xlvask/services/usage/autopilot-runs/{id}'")
|
||||
// The autopilot-runs/active GET (recovery) must also remain manage-only.
|
||||
->toContain("get('/modules/xlvask/services/usage/autopilot-runs/active'");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user