Keep Bird activation outside the PHP-FPM/nginx startup path so Bird configuration failures cannot make the core API unavailable. Retain guarded explicit activation and regression coverage.
Add the Bird Control Plane gateway, signed webhook ingestion, policy-gated writes, fail-closed production auto-activation, and RSA-OAEP bootstrap credential flow.
## Summary
- Adds a backend contract guard that Superuser invoicing period search
continues to include all separate registration columns: `reg_1`,
`reg_2`, and `reg_3`.
- This preserves the API/search behavior that the frontend object-tree
registration layout relies on.
## Tests
- `vendor/bin/pest
tests/Unit/Invoicing/InvoicingPeriodRouteGuardsTest.php --colors=always`
Note: local Composer install was run with
`--ignore-platform-req=ext-mysqli --ignore-platform-req=ext-gd` because
those PHP extensions are not enabled in this worker image; the executed
guard test does not use them.
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
## Summary
Adds the missing backend contract used by Pleno Control Plane
Conversations/Suggestions to create an employee login action safely.
- issues 60–900 second one-time limited-backoffice login grants
- persists only SHA-256 bearer digests; bearer recovery is deterministic
under the server encryption key for identical idempotent retries
- enforces manager permissions, department scope, active
managed-employee constraints, one-time atomic exchange, revocation,
expiry, and account-deletion cleanup
- adds employee-create idempotency so an approved automation retry
cannot duplicate an employee
- documents the create, revoke, and unauthenticated exchange endpoints
in OpenAPI
## Security and concurrency
- bearer values are returned only in a URL fragment and are never
written to logs or database plaintext
- employee and grant rows use a consistent employee-then-grant lock
order
- deactivation revokes outstanding grants and existing sessions in the
same transaction
- consumed, revoked, expired, or payload-mismatched idempotent replays
fail closed
## Verification
- `scripts/php-ci-test.sh api`: 273 passed, 11,086 assertions (one
inherited warning)
- focused security contract: 1 passed, 21 assertions
- PHP syntax checks passed for the service and routes
- `git diff --check` passed
## Dependency
Required by copenhagentruckwash/pleno-control-plane#1. Merge before the
matching frontend and Control Plane PRs.
## Summary
- Makes Stripe Terminal card payment intents always use 25% moms in the
API, independent of any client-supplied `tax_percentage`.
- Updates amount calculation, metadata persistence, stored-intent reuse
matching, the authoritative OpenAPI contracts, and operation-specific
Writerside outputs.
- Prevents double charging and false order closure across stale,
concurrently succeeded, partially recorded, or mismatched intents.
- Serializes payment create/capture/closure with order-item changes and
every order-to-invoice-collection reassignment through shared database
locks.
- Converts expected lock contention and reconciliation cases into
deliberate 409 responses.
## Exact-head evidence
Current head: `3a0f70d315a94d2efe586a2188d2c54f8ff11cd4`
- PHP syntax passed for all changed runtime files.
- Focused Orders suite: **42 tests / 293 assertions passed**.
- `git diff --check` passed.
- Fresh exact-head Tests and Qodana are running.
- Every Codex finding has a concrete reply; a fresh exact-head review is
requested below.
## Safety behavior
- Caller-controlled VAT is absent from request contracts; fixed 25% moms
is server-owned.
- A succeeded payment is preserved, requires the full expected
`amount_received`, and cannot close a changed/mismatched or
already-claimed collection.
- A compatible partially recorded Stripe closure is completed
idempotently; conflicting partial state fails closed for manual
reconciliation.
- Every cancellation/delete caller honors a concurrent-success result
and never falsely reports a completed payment as cleared.
- Price changes and invoice-collection reassignment share the payment
lock through validation, capture, post-capture reload, and closure.
- Reader changes are persisted only for reusable matching intents, so
stale intent cancellation targets the original terminal.
- Accepted legacy succeeded intents normalize stored tax to 25% before
response construction.
---------
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
## Summary
- retire Stripe hosted payment-link creation routes used by POS and
order management
- automatically capture authorized payment intents rather than requiring
a separate manual capture action
- preserve ordinary terminal payment and payment-intent lifecycle
behavior
- add API and wiring regressions for payment-link retirement and
automatic capture
Paired frontend change:
https://github.com/copenhagentruckwash/pleno-vue/pull/233
## Verification
- focused backend unit suite: 2 tests, 36 assertions passed
- PHP syntax checks passed
- paired frontend unit and Playwright suites passed locally
- full required GitHub runner suites are required before this task may
enter Review or merge
## Security and operational notes
- no credentials, terminal secrets, or payment data are added
- no live Stripe account or physical terminal was exercised locally
- automatic merge remains gated on both paired PRs having passing
required checks and current branches
## Summary
- notify customers by SMS with approve/deny links when a subuser
requests access
- notify subusers by SMS after approval or denial, including manual
grant changes
- support subuser password reset and authenticated password changes
- add read-only token previews followed by explicit POST confirmation
- store short-lived one-time purpose-bound action tokens only as SHA-256
digests
- serialize grant decisions transactionally to prevent conflicting
concurrent actions
- document the API contract in OpenAPI
## Security
- generic reset responses reduce account enumeration
- URL tokens are removed from browser history after frontend bootstrap
- approval previews never mutate state
- concurrent decisions lock the exact grant row
- SMS failures remain non-fatal and are returned as delivery status
Residual risk: existing subuser sessions cannot all be centrally
invalidated after password reset because there is no per-subuser session
index; they expire normally within the existing session lifetime.
## Verification
- backend Pest: 14 tests, 91 assertions
- PHP syntax checks passed
- focused PHPStan passed
- OpenAPI YAML parsed successfully
- `git diff --check` passed
Database-backed API integration tests were unavailable because the local
environment lacks the required database configuration.
## Paired delivery
Paired Frontend PR:
https://github.com/copenhagentruckwash/pleno-vue/pull/231
Both PRs are required before completion. The frontend PR contains the
responsive visual comparisons.
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
## Summary
- Run Docker-producing PHP, Edge Broker, and Edge Gateway jobs on
ephemeral Ubuntu workspaces for both PR and push events.
- Keep the non-Docker Edge Agent and Release Manager gate on the trusted
backend pool.
- Preserve the explicit system-socket selection and fail-closed Docker
access check.
## Root cause
Exact-master run 29942825210 got past Docker access, then later jobs
failed during checkout because an earlier container left
`services/php/logs/error.log` root-owned in the reused self-hosted
workspace. This is workspace contamination, not a product-test failure.
The shallow frontend repair pattern would miss the depth-4 file and
would accumulate undeletable trash directories.
## Verification
- Workflow YAML parse passed.
- `git diff --check` passed.
- PR CI must be green; after merge, exact-master Required CI and the
non-skipped Release Manager gate are mandatory.
## Summary
- Keep untrusted PRs on ephemeral Ubuntu runners and trusted pushes on
the local backend pool.
- Force Docker-dependent jobs to the working system socket instead of
the unavailable default rootless context.
- Preserve the fail-closed Docker access check and never chmod the
socket.
## Evidence
- Exact master run 29942048689 failed before tests because plain Docker
commands resolved to `/run/user/1000/docker.sock`.
- Backend listener processes already have docker-group membership;
`/var/run/docker.sock` is root:docker 0660.
- `DOCKER_HOST=unix:///var/run/docker.sock docker version` succeeds
locally with server 29.3.1.
- Workflow YAML parse and `git diff --check` pass.
Exact-master Required CI and Release Manager gate success remain
mandatory after merge.
## Summary
- Add self-service deletion for the authenticated customer or subuser
identity only.
- Preserve shared customer grants, reset keys, bookings, order bookings,
vehicles, invoices, and legally required history.
- Require password/TOTP or a fresh deletion-specific, five-minute,
single-use WebAuthn assertion.
- Reject support impersonation and expired legacy plain-session tokens.
- Use durable database throttling, transactional request processing, a
durable outbox, and terminal `manual_review` state.
- Keep API and worker default-off behind separate
`account_deletion.api_enabled` and `account_deletion.worker_enabled`
module-config flags.
## Safe rollout
1. Keep both flags disabled.
2. Run `php scripts/account-deletion-schema.php check`.
3. If needed, run `php scripts/account-deletion-schema.php apply --yes`,
then rerun `check` until `ready:true`.
4. Deploy the frontend companion PR while the API remains disabled.
5. Enable `api_enabled` for a controlled canary; verify password and
passwordless request flows plus immediate authentication revocation.
6. Inspect queued request/outbox state, then enable `worker_enabled`.
7. Verify anonymization, preserved tenant/history data, outbox delivery,
retries, and manual-review behavior before broad rollout.
## Verification
- Account deletion unit tests: 2 passed, 43 assertions.
- PHP lint, both OpenAPI YAML parses, runtime-DDL scan,
destructive-scope scan, and `git diff --check` passed.
- Full API/unit/integration evidence is required from exact-head CI;
local Docker is unavailable and shared-vendor tests were explicitly
discarded.
## Security notes
- Schema mutation is CLI-only; web and cron paths perform read-only
readiness checks.
- Runtime behavior fails closed when schema/config/throttle/delivery
prerequisites are unavailable.
## Summary
- run untrusted pull-request jobs on ephemeral `ubuntu-24.04` runners
- reserve the local backend runner pool for trusted branch pushes
- remove world-writable Docker-socket fallbacks
- pin core GitHub Actions and disable checkout credential persistence
- remove the release-manager PHP parse-error fail-open path
## Why
Pull-request code previously ran on persistent self-hosted runners with
Docker access, and CI contained permission weakening and a release-gate
break-glass success path. Those behaviors were unsafe for autonomous
intake.
## Validation
- workflow YAML parsed
- backend AI workflow outputs are in sync
- pinned action SHAs match the current v4 tags
- `git diff --check`
## Risk and activation
This is an R4 CI/release-policy change. Keep the PR draft for human
review and let required CI prove the hosted-runner path before merge.
## What changed
- validate every normalized order-booking item against active customer
product rules before reservation and persistence
- return a structured HTTP 400 response containing the rejected product
and matching rule metadata
- document the rejection response in both OpenAPI specifications
- add API coverage for restricted base products, restricted add-ons, and
allowed neighboring products
## Why
Frontend rule guidance alone cannot prevent stale or crafted requests
from persisting restricted booking products. The booking write boundary
must enforce the same customer rules.
## Validation
- full backend API suite
- focused order-booking API coverage
- PHP syntax checks
- OpenAPI and diff checks
## Related frontend PR
The coordinated frontend PR provides fail-closed selection, recovery,
and responsive booking-page behavior.
Ignore the generated PHPUnit cache directory and remove its volatile test-results file from Git tracking so test runs no longer dirty protected branch checkouts.
Resolve recommended-profile Critical and High findings, retain narrow analyzer exceptions, and update the edge-broker WebSocket dependency to a non-vulnerable release.
## Summary
- configure advisory Qodana PHP 2026.1 analysis for trusted pull
requests and master, beta, canary, and internal branch scans
- install both Composer projects and the edge-agent/edge-broker Node
dependencies before analysis
- exclude generated, vendor, build, cache, legacy-test, and local-agent
trees
- keep Quick Fixes, SARIF artifacts, baselines, thresholds, and
required-check enforcement disabled during calibration
## Safety
- fails closed when QODANA_TOKEN is absent
- skips draft, fork, and Dependabot pull requests
- uses least-privilege GitHub permissions and immutable action SHAs
- uploads findings to the dedicated api Qodana Cloud project
## Validation
- actionlint 1.7.12
- SchemaStore qodana-1.0 validation
- bootstrap shell syntax and lockfile structure checks
- immutable action tag verification
- git diff --check
- independent review completed with no findings
## Live verification
- [PR-mode
scan](https://github.com/copenhagentruckwash/api/actions/runs/29494056175)
completed successfully with 0 changed-file problems, 439 inspections,
and a passed license audit ([Qodana
report](https://qodana.cloud/projects/P2nXd/reports/LJv98e))
- [full branch
scan](https://github.com/copenhagentruckwash/api/actions/runs/29495399119)
completed successfully and uploaded 8,248 current findings across 725
files, 439 inspections, and a passed license audit to the dedicated api
project ([Qodana
report](https://qodana.cloud/projects/P2nXd/reports/qJMOxX))
- the initial debt remains advisory; baseline and required-check
enforcement are intentionally deferred until findings are triaged
Guard the wash subscription distribution query after invoice-inclusion filtering removes all candidate orders, preventing an empty IN () clause on the invoicing distribution endpoint.
Verified with focused syntax, Pest, PHPStan, and invoicing unit-suite checks.
Reactivate existing relay binding rows when a gateway/relay pair is re-added after soft deletion, avoiding duplicate uniq_edge_gateway_binding inserts. Add regression coverage for the reactivation path.