Implements **TRU-71 (DRIFT 10)** for the api repo: rewrites the
customer-facing Stripe invoice email body so it no longer exposes the
payment-processor name 'Stripe' to the customer. The artefact is now
described as a *betalingslink* (payment link) in plain Danish, matching
the wording used by the rest of the system.
## Changes
-
services/nginx/app/modules/email/templates/email_template_stripe_invoice.php
— replaces 'på Stripe' with 'et betalingslink til din faktura' in the
customer body.
- services/nginx/app/tests/auth/StripeInvoiceEmailTemplateTest.php (new)
— regression test that asserts the rendered HTML contains no 'stripe'
token and includes the new 'betalingslink' wording.
## Tests
- php8.4
services/nginx/app/tests/auth/StripeInvoiceEmailTemplateTest.php → PASS
- Full Pest Unit suite: 1348 tests pass, 11 pre-existing failures in
Bird/Scanner/SchemaHealthCheck/Selfserve/Tooling — unrelated to this
change.
Refs: TRU-71. Frontend companion PR copenhagentruckwash/pleno-vue ships
the same wording change in InvoiceOrdersPagination.vue.
---------
Co-authored-by: perf-investigator <[email protected]>
## Summary
Adds an optional `invoice_email` (Danish: *faktura email*) field to
customers, so e-conomic can deliver invoices to a dedicated accounting
mailbox instead of the customer's primary email.
Linear: **TRU-77** (DRIFT 16)
## Changes
- **Migration** (additive, via existing schema_bootstrap pattern)
- New `customer_invoice_email_schema_bootstrap` adds the `invoice_email
VARCHAR(255) NULL` column to `users` after `wash_certificate_email`.
Idempotent — skips when the column already exists.
- **Domain object — `objects/users_o.php`**
- New `invoice_email` object property.
- `getInvoiceEmail()` returns the dedicated address or falls back to the
primary `email`.
- `getInvoiceEmailOverride()` returns only the explicit override (no
fallback).
- `setInvoiceEmail($email)` validates and writes the value; `null`/empty
clears it.
- `add($customer_number, $password, $role, ?$invoice_email = null)` now
accepts the optional field and persists it.
- The user payload output now exposes `invoice_email` and
`invoice_email_fallback`.
- **API — `routes/usersRoute.php`**
- `POST /users` accepts an optional `invoice_email`, validated before
insert.
- `PUT /users` accepts `invoice_email` (including null/empty to clear)
on existing users.
- **Customer mass import — `classes/customer_mass_import_service.php`**
- Payload now accepts `invoice_email`.
- `normalizeInvoiceEmail()` rejects malformed addresses before any
e-conomic call.
- `resolveInvoiceEmail()` / `resolveCreateEmail()` route the e-conomic
customer email to the dedicated address when set, otherwise the primary
`email` (with the existing `jb@truckwash.dk` fallback when neither is
provided).
- `syncLocalCustomer()` persists `invoice_email` on the local user.
- `import()` result now includes the resolved `invoice_email`.
- **Tests — `tests/Unit/Customers/CustomerInvoiceEmailTest.php` (new)**
- Schema bootstrap adds the column when missing.
- Schema bootstrap is a no-op when the column already exists.
- Schema bootstrap skips when the `users` table is not present.
- e-conomic customer email is set to `invoice_email` when provided.
- e-conomic customer email falls back to `email` when `invoice_email` is
omitted.
- Invalid `invoice_email` is rejected before any e-conomic call.
## Backwards compatibility
- The column is nullable; existing rows are unaffected.
- The `add()` signature is additive (new optional parameter with default
`null`).
- The route payloads ignore `invoice_email` unless supplied, so no
client change is required.
## Linear
- TRU-77 (DRIFT 16: "Add 'faktura email' field to customer creation
form")
---------
Co-authored-by: Jeppe B <jeppe@copenhagentruckwash.io>
Co-authored-by: OpenClaw Bugfix Bot <openclaw-bot@truckwash.dk>
Co-authored-by: bugfix sub-agent <bugfix@openclaw.local>
## Summary
Fixes **TRU-128** ("Jeg kan ikke fakturere") — a customer in
#afdelingsansvarlige could not invoice because the customer-facing
invoice PUT endpoint returned a misleading 400 error.
## Root cause
`PUT /collected-invoices` in
`services/nginx/app/routes/userInvoicesRoute.php` had two related bugs:
1. **Misleading error message** — the 'both fields missing' guard
errored with
`'Missing required parameters: po_number, closed_at'`, which reads as
if BOTH fields are required. The actual condition (`&&`) only fires
when neither is set, so only one is required. Customers who tried
different combinations kept getting the same error and concluded the
system was broken.
2. **Inconsistent `closed_at` clearing** — the 'forbidden closed_at for
non-superusers' guard fired for ANY present `closed_at` key,
including `null` and `""`. That blocked customers from CLEARING a
previously-set `closed_at`, even though the handler further down
already nulls the field when it receives an empty value.
## Fix
- Reword the missing-fields error to state the actual contract:
*"At least one of po_number or closed_at must be provided"*.
- Narrow the forbidden guard to *non-empty* `closed_at`, so customers
can still pass `null` / `""` to clear a previously-set value.
The clear-on-null/empty logic further down in the handler is unchanged
— the guard now matches it.
## Test
`tests/Unit/Invoicing/UserCollectedInvoiceUpdateRouteValidationTest.php`
- Locks in the new error message.
- Locks in the new `$closed_at_is_non_empty` guard shape with the
`if (self::isParametersSet(['closed_at'])) { ... }` pre-check.
- Locks in the regression: the previous 'any present closed_at -> 403'
pattern is explicitly asserted to be absent.
## Files changed
- `services/nginx/app/routes/userInvoicesRoute.php`
-
`services/nginx/app/tests/Unit/Invoicing/UserCollectedInvoiceUpdateRouteValidationTest.php`
## Refs
- TRU-128
- Slack: #afdelingsansvarlige (kunde-rapport)
---------
Co-authored-by: OpenClaw Backend Agent <agent@openclaw.ai>
Co-authored-by: Jeppe B <jeppe@copenhagentruckwash.io>
Co-authored-by: jeppemaxclaw[bot] <bot@jeppemaxclaw.local>
Co-authored-by: Bugfix Subagent <bugfix-subagent@openclaw.local>
Adds `reason_code`, `reason_label_snapshot`, `reason_comment` columns to `order_items` and integrates the `order_item_reason_policy` class into the POST and PUT /order/items routes.
Validation order on audited products (consistent across POST and PUT):
1. If `reason_code` is present, validate reason first — emits the most specific error (invalid code, deprecated code, missing reason_comment).
2. If notes are provided but empty/whitespace, return "Notes is required for this product" (the legacy message).
3. Otherwise run reason validation — covers the missing-reason_code case.
PHP api suite went from 284/290 to 290/290 (was 6 OrderItemsApiTest failures, now 0). Wired `addItemToOrder`, `updateOrderItem`, and `getItemAsArray` to persist and return the new columns.
## 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
- 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
- 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.
Resolve recommended-profile Critical and High findings, retain narrow analyzer exceptions, and update the edge-broker WebSocket dependency to a non-vulnerable release.
- Introduced tests for validating note requirements on order items.
- Updated subuser route management contract tests with new route coverage.
- Added endpoints to manage department lane and self-serve lane statuses, with associated tests.
- Implemented `InvoicingPeriodPaginationTest` for testing period pagination modes, normalization of options, search functionality, and visibility filters.
- Added comprehensive tests to validate scenarios such as active period views, exact counts, and customer-card level search.
- Improved cURL timeout settings with `CURLOPT_CONNECTTIMEOUT` and `CURLOPT_TIMEOUT` adjustments.
- Introduced and documented helper classes/methods for local caching, pagination response structure, and customer name retrieval.
- Introduced `invoice_period_flag_schema_bootstrap` to initialize the schema for invoice period flags.
- Added `invoice_period_flag_service` to handle manual and automatic flag creation, updates, filtering, and context resolution.
- Implemented lifecycle methods such as `createManualFlag`, `updateAutomaticFlagStatus`, and `applyFlagsToPeriodTypes` for handling invoice period flags and their usage in processing periods.
- Included context-specific resolution methods for efficient flag management in invoicing workflows.
- Implemented `InvoicingPeriodDraftOverlayTest` with coverage for blocking and permitting invoicing actions based on draft states, transactions, and metadata.
- Created `ReferenceSuggestionsApiTest` to validate ranked and filtered suggestions across bookings, orders, and vehicles with varied match relevance, context, and frequency.
- Added `order_reference_suggestions_service` class, including query methods, normalization utilities, and aggregation logic for reference suggestions.
- Enhanced query handling in `InvoicingPeriodDraftOverlayFakeDb` to validate SQL constraints and column cache resets in overlapping invoicing contexts.
- Added `archived` column and index to `departments` table, ensuring schema initialization via `departments_schema_bootstrap`.
- Updated OpenAPI spec to include `archived` attribute and `filters=archived` query parameter with superuser access control.
- Enhanced `Departments` API to support archived department filtering and retrieval.
- Modified `ApiFixtures`, `departments_o`, and related tests to validate behavior for archived departments.
- Added unit and API tests to ensure correct handling of archived departments and filter enforceability.
- Add branding management feature: API routes, payload handling, and OpenAPI schema updates.
- Implement department branding logic: CRUD operations, validation, and permissions.
- Add order deletion confirmation support with conflict handling and OpenAPI schema updates.
- Enhance tests and API methods for improved order handling and branding workflows.
- Deleted `complete_booking_f` and `generate_booking_wash_certificate_f` classes.
- Updated tests to ensure legacy booking completion routes are disabled.
- Introduced tests for POST `/order-bookings/complete` to enforce POS-based booking completion management.
- Added `/collected-invoices/split-by-month` route with API and unit tests for splitting collections into monthly periods.
- Refactored impacted files to exclude legacy references and ensure continued compatibility with POS processes.