Changelog
All notable changes to Signals, ordered by version.
0.8.2 — The reporting release — report builder, scheduled delivery, VAT returns, fleet scheduling, and the P6a audit & temporal platform
Added
Reporting engine
- Report definitions & query engine — validated, hashable
ReportDefinitionpayloads compiled by a whitelist-onlyReportQueryBuilder: every dimension, measure, date field and bucket must be declared by the source model's own schema (groupable/aggregatableflags), aggregate functions and buckets are closed enums, and filter values only ever arrive as Ransack bindings — a 16-case injection dataset proves nothing user-authored reaches the SELECT list. Subtotals and grand totals areGROUP BY GROUPING SETSon PostgreSQL (one query per report) with a shape-identical SQLite fallback. - Comparison periods with ordinal bucket alignment —
previous_periodandsame_period_last_yearas a second query over a shifted window merged in PHP by dimension tuple (never a join): per-measure comparison, change and signed change-% columns on rows, subtotals and grand totals. Bucketed date dimensions align by ordinal position within their window (January compares with January undersame_period_last_year, week 3 with week 3), timezone/DST-aware, with the mode exposed asmeta.comparison_alignment. - Report builder & results UI at
/reports— sidebar of System and My reports with per-user pinning, collapsible builder (source, date field + preset, dimension chips with bucket select, measure rows, Ransack filter rows, comparison toggle), results grid with dimension/aggregate badges, subtotal and grand-total rows, table/bar/line/pie visualisation through the shared Chart.js component, schedule management modal, and CSV/XLSX/PDF export buttons. Saved-report search joins the command palette. - Reports & schedules API —
GET/POST/PATCH/DELETE /api/v1/reports,POST /api/v1/reports/{report}/execute, ad-hocPOST /api/v1/reports/execute, and nested schedule CRUD at/api/v1/reports/{report}/schedules;reports:read/reports:writeabilities,reports.*permissions mapped into the system roles, and the CustomView-sibling sharing model (personal / shared-to-roles / system) enforced end to end. - Execution, caching & permission scoping —
ExecuteReportruns saved or ad-hoc definitions for a named user throughReportPermissionScope(source permission, saved-report visibility, warehouse row scoping — an explicitly requested warehouse only ever narrows); results cache by definition hash × permission context × resolved window with tiered TTLs and per-source generation busting. - Exports — CSV / XLSX through the existing export engine (a materialised-rows seam feeds the renderers; no new spreadsheet dependency) honouring the sync/queued threshold, and a landscape PDF via a framework-owned template through the PDF driver, with comparison columns and money as decimal strings throughout.
- Scheduled delivery & thresholds — report schedules with timezone-aware cadence, dispatched by
signals:dispatch-report-schedulesand executed per subscriber under that subscriber's own permissions (one recipient's attachment can never leak another's rows); delivery via registered notifications with the rendered file attached; threshold alerts over grand-total measures on a closed comparator set;report.generatedwebhook event and two new notification types. - VAT-return tax reporting — the invoice tax snapshot table becomes a reportable source (windowed on
invoice.issued_atthrough a published relation path), with seeded VAT Return and Tax by Treatment system reports that aggregate frozen snapshots only — tax is never recalculated, and a penny-exact test holds the report to the summed invoice breakdowns. - Sixteen seeded system reports — Revenue by Warehouse, Outstanding Invoices, Top Clients, VAT Return, Tax by Treatment, plus eleven covering invoiced/paid/outstanding revenue trends, cash collected, currency exposure, pipeline by stage, rental turnover and revenue mix by warehouse, upcoming shortages, uninvoiced orders, customer-base growth, and catalogue composition — each proven to answer its question against fixtures, each repairable by re-running the seeder.
- Dashboard seam — report widgets execute seeded system reports with board-filter awareness (range + warehouse, narrowing-only).
- Reporting platform docs —
docs/platform/reporting.mdcovering the whole surface, registered in the docs manifest. - Date-token vocabulary — shared
DatePresetResolver/DateBucketExpressioninapp/Support(company-timezoneDATE_TRUNCbucketing, DST-boundary tested), available for future custom-view adoption.
Fleet & transport
- Vehicles join the resource scheduling engine — the
vehicleresource type activates onresource_assignments, so vehicle bookings get the same on-write conflict detection, rental-lifecycle sync and timeline presence as crew and facilities. The Phase 5 vehicle-assignment surface was absorbed: the/api/v1/rental-vehiclesAPI is preserved unchanged as a compatibility façade overresource_assignments, pinned by a golden test suite. - Assignment route stops — ordered pickup/delivery stops on a vehicle assignment with reorder-safe CRUD, a full REST surface (
/api/v1/resource-assignments/{assignment}/route-stops), audit trail and webhook events. API-only by design. - Vehicle lanes on the resource timeline, alongside crew and facility lanes, with conflict and warning badges and warehouse filtering.
dispatch_groupresource type activated with a minimal entity for grouped-dispatch scheduling.
Scheduling
- Conflict badges are actionable — clicking a conflict on the resource timeline opens a panel listing the assignment's open conflicts with Acknowledge and Resolve (typed resolution + notes), backed by a new
AcknowledgeSchedulingConflictaction and the existing resolve action.
Plugins & notifications
- Plugin communication templates — plugins declare notification email templates in their manifest; they seed per warehouse (including new warehouses created later), survive operator edits on reseed, deactivate with the plugin, and prune when undeclared. Template attachments resolve through document-template slug indirection.
- Templates for any channel — the email-only restriction on plugin template seeding is lifted: declared
sms/slack/teamstemplates seed for their channel, resolution is channel-scoped, and delivery still gates on a registered channel driver. - Seven dead notification types wired, six removed — every registered notification type now has a live dispatch path.
signals:doctorwarns on unstaffed notification roles — when an active notification type targets a role no user holds; demo seeding now assigns representative roles so notification flows work out of the box.
Audit trail hardening (P6A-05)
- Every production write boundary now audits with before/after snapshots — shared
AuditSnapshotinfrastructure (create/update/delete capture with nested-subject support) rolled across the whole action layer: accounts and their contact details, relationships and anonymisation; activities; admin identity access (roles, users, ownership transfer, API tokens, magic links, SSO identity linking, password and two-factor mutations, self-service password changes); custom fields and live filters; tax, pricing (nested discount/price-category/surcharge subjects audit as one event), supplier costs and preferred-supplier selection; billing runs (population, issue, failure callbacks); purchase-order receipts and document snapshots; stock-check counts and recalculation; imports (profile application, queue promotion); webhooks, integrations, notification preferences, localisation and setup; attachments (delete audited after persistence); and partial shortage resolution. No-op updates deliberately suppress their audit event. AuditPayloadSanitizerfor large and binary fields — oversized text/JSON values audit as deterministic canonical-byte digests with a stable version reference, binary content is omitted rather than stored as invalid bytes, and password hashes never enter old/new payloads.- The production mutation manifest becomes a reviewed ratchet — every scanned write boundary in
app/,routes/,bootstrap/andconfig/is classified (direct, delegated, event-sourced, derived, technical or approved exemption) with pinned evidence, and the conformance test fails on any unreviewed or drifted boundary.
Audit trail follow-up (P6a review round)
- Temporal state responses wrapped —
GET /api/v1/actions/statereturns its payload under adatakey, matching the platform envelope convention. - Per-record authorization inside reconstruction —
ReconstructEntityAsAt::reconstruct()always runs the target model'sviewpolicy (including warehouse scoping), so everyas_atroute enforces per-record access, not just the blanket permission. - Empty audit diffs standardised — a mutation that produces no observable change writes no audit row anywhere; replay-sourced empty actions are preserved where the event stream requires them.
- Encrypted-only mutations still evidence themselves — webhook secret rotation and the two-factor secret/recovery-code lifecycle record an
encrypted_attributes_changedmarker naming what changed (never the values), instead of auditing an empty diff the suppression would drop. - Stock-check count audits keep the item diff out of the parent's shape —
RecordCount/RecountStockCheckItemnest theStockCheckItemdiff under anitemskey (asRecalculateExpectedalready did), sostock_checkaudit rows no longer carry item-only field names at the top level. - Archived accounts reconstruct correctly —
account.archivedrecords theis_activeflip innew_values, so point-in-time reconstruction restores an account's active state for pre-archive instants instead of silently keeping the current value. - Duplicate event-link integrity — rental event replay fails closed on a duplicated event/state association in the index.
- Unresolved-boundary ratchet — the mutation manifest additionally pins the set of unreviewed boundaries (empty on this branch) so a candidate can never linger unreviewed without failing conformance.
Point-in-time reconstruction (P6A-06)
as_atendpoints —GET /api/v1/rentals/{id}/as_at,/api/v1/invoices/{id}/as_atand/api/v1/accounts/{id}/as_atreconstruct a record at any past instant from its audit trail, returning the normal resource DTO plus ameta.reconstructioncompleteness block; future instants are rejected and pre-creation instants return a structuredaggregate_not_createdenvelope naming the earliest evidenced instant.- Temporal state replay —
GET /api/v1/actions/statereplays rental, invoice, purchase-order and virtual-stock-intake aggregates from their Verbs event streams with batch replay-integrity checks (extra, duplicate or missing event/state associations fail closed), resource authorisation and cost-visibility enforcement, and replayed money serialised as decimal strings. - Temporal event discovery indexes — targeted indexes over the event/state tables keep point-in-time discovery queries flat.
Event payload schemas (P6A-05)
- Every consumer-visible event now publishes a payload schema — full-DTO events expose their envelope key and embedded DTO shape; lean-envelope events share a canonical
id/actionschema (with a stricterwith_actionvariant where the action is always present).GET /api/v1/eventsservespayload_schemaper event, and producer-derived contract matrices (64 rental rows + billing/purchasing) pin the 77 required schemas and nine internal high-value definitions in architecture tests. - Webhook after-commit coverage — real (unfaked) coverage proves outbox rows commit and roll back with their domain transaction, and that deferred deliveries preserve typed actor attribution.
Removed
- The
rental_vehiclestable is dropped — absorbed intoresource_assignments(see Fleet & transport above). The drop is irreversible by design; the compatibility API is unaffected.
Changed
- Test coverage raised 91.95% → ~97.5% across three burn-down rounds (~2,200 new tests; suite now 20,400+ main-lane tests plus new pgvector and PostgreSQL-diagnostics lanes), with every remaining gap either covered, another lane's, or carrying a justified line-scoped ignore.
App\Livewire\Dashboard\DashboardTabsdeleted — dead code superseded byDashboardBoard.
Fixed
- JSON imports larger than 64 KB always failed — the streaming parser re-scanned retained bytes after each chunk and double-counted brace depth, so every object after the first chunk boundary was dropped and the import aborted as "truncated or malformed".
- UTF-16 CSV imports were silently corrupted — UTF-16LE files died with a bogus ragged-rows error on row 2, and UTF-16BE files imported the first mapped column as empty because the BOM was never stripped; the parser now converts the stream to UTF-8 through an iconv read filter.
- Printing from an account record crashed —
PrintMenuandGenerateDocumentstill called the pre-renamememberWarehousesrelation; account-sourced prints and unpinned account statements threwRelationNotFoundExceptioninstead of resolving a warehouse. - Surcharge lists ran an N+1 on every money format —
RentalItemSurchargechecked the extinctopportunityItemrelation name, so the eager-load short-circuit never fired. - Equipment-test results ignored three documented API filters —
description,equipment_test_atandcreated_byaliases were documented but never declared, so the predicates were silently dropped intometa.ignored_filters. - An unreadable plugin
composer.jsontook the host down at boot — the discovery guard was dead code behind the framework's warning-to-exception handler; discovery now logs and skips the plugin. - The scan-session report offered a merge field that always rendered empty (
session.rental_refregistration vs payload mismatch), and non-rental scan targets printed a lowercase morph alias; stored system templates are refreshed by migration. - Residual pre-rename identifiers cleaned up (
resolveStoreId→resolveWarehouseIdacross six classes; therevenue-by-storeseeded key shipped asrevenue-by-warehouse). - Updating a disabled plugin reactivated its notification types and templates — seeding on
update()now re-aligns both surfaces with the plugin's enabled state inside the transaction. NotificationRegistry::syncToDatabaserelabelled plugin-registered types as core, breaking prune/uninstall scoping — registry entries now carry their origin and the sync preserves it.- Two communication templates could both claim the default for one notification type/channel/warehouse — a
NULLS NOT DISTINCTpartial unique index now enforces a single default, saves clear sibling defaults, and plugin seeding only claims a free slot. - Role-targeted notifications silently vanished in demo/dev environments because no seeded user held a role — recipients now resolve out of the box.
- DTO validation was bypassed on scheduling conflict-resolve and warehouse-transfer line-receipt inputs — both now validate through their DTO rules.
- Full-interaction UAT hardening (296-route sweep) — a page-by-page functional pass fixed a set of client-side and routing regressions the render-only checks missed:
- The header notifications pane threw a
ReferenceErroron every page load because its Alpine state lived outside the body scope; state moved to a globalnotificationsAlpine store, guarded by an architecture test. - The rentals quick-add placeholder passed the Blade
__()helper into an Alpinex-bind, so it evaluated in the browser and threw; a new architecture test rejects__()inside Alpine bindings on plain HTML elements. - Creating a catalogue item returned a 500 (
PublicPropertyNotFoundException) — the form boundproductTypeafter the rename tocatalogueItemType. - Adding rental and invoice line items silently failed — a domain rename rewrote the Dexie cache call
.version(1).stores({…})to.warehouses({…}), which threw on boot; the pipeline now degrades gracefully when the cache is unavailable, guarded by an architecture test. - Several
Route::redirect/permanentRedirecttargets were relative, so the cached 301 pointed at a compounded 404 path (e.g./imports/create); all targets are now absolute, guarded by a new architecture test. - The rental and account edit forms lost their dirty-bar and disclosure-group chrome under a layout rewrap; visibility is now Alpine-driven with a stable DOM id.
- The virtual-stock and autocomplete empty state read
No catalogue_items found; the copy is now natural (No catalogue items found).
- The header notifications pane threw a
- Merge integration fixes (P6a landing on the renamed mainline) — resolving the audit/temporal work against the domain-vocabulary rename surfaced and fixed a set of gaps:
UpdateFlightcasenow routes through the deferredFlightcaseEventRecorderpath, so mid-transaction updates audit and dispatch webhooks after commit with the original actor context preserved.RetireAsset/ReinstateAssetaudited a variable that no longer existed and fired the extinctstock_level.updatedevent; both now snapshot the asset and fireasset.updated.RecalculateExpectedreferenced the droppedstock_level_idcolumn in its audit metadata; it now recordsasset_id.CreateActivity/UpdateActivitymixed pre-renamemember_idandaccount_idparticipant keys, breaking participant audit snapshots.OperationInvokercleans up its self-minted ephemeral token withauthorize: false— the explorer permission and ownership checks stay on the user-facing revoke path but no longer break CLI/MCP invocations for users withoutapi.explorer.use.rental.discount_resetandrental.item_addeddeclare thewith_actionlean-envelope variant their producers always emit.
0.8.1 — The agent-ready release — MCP & CLI, events platform, warehouse ops, dashboards, i18n, and the Pest 5 / PHP 8.5 toolchain
Added
Events platform & audit trail
- Canonical
EventRegistry— a config-seeded catalogue (config/events.php, 310 events across 69 entity groups) of immutable event definitions with per-consumer visibility (API / webhook / workflow / plugin), system-protected entries, payload schemas resolving to generated DTO shapes, atomic all-or-nothing plugin event batches, and a reserved*wildcard that can never collide with a real name. - Events API —
GET /api/v1/eventsandGET /api/v1/events/{name}(newevents.readpermission /events:readability) listing the API-visible catalogue withq[entity_eq]filtering,ignored_filtersreporting, and machine-readablenot_foundcodes. - Webhook subscriptions validate against the live catalogue —
POST/PATCH /api/v1/webhooksrejects unknown event names with group-aware suggestions (previously a static allow-list); a data migration canonicalises malformed event names stored by five earlier actions (created→role.created, etc.). - Typed actor attribution — the audit trail now records
actor_type/actor_id/actor_labelbeyonduser_id(users, API tokens, plugins, system), with every audit-timeline entry badged by actor type and agent writes stampedvia: mcp/cli/explorer. - Transactional webhook delivery — an outbox captured inside the firing transaction with a drain job and dead-letter handling replaces dispatch-at-emit, so a rolled-back action can never deliver a webhook.
- Entity history & as-at reconstruction — event-sourced entities expose history endpoints and point-in-time reconstruction; scoped optimistic concurrency and dry-run support extend to invoice issue.
Workflows
- Draft → publish → activate lifecycle — Create / Update / Publish / Activate / Deactivate / Delete actions with copy-on-publish
WorkflowSnapshotversions. Publishing validates every step against the registered handlers and each handler's own config schema, so a workflow that could not run cannot be published; activating pins runs to a published version. - Production workflow editor at
/admin/workflows— an index plus a timeline-hybrid edit page with a schema-driven step inspector and version history, authoring linear action chains today (branching waits on the execution engine). Newworkflows.*permissions arrive with policy, navigation and docs; the six new audit events are registered non-visible, so the public event catalogue, webhook surface and capability manifests are unchanged. - Constraint-hardened persistence for the future no-code workflow engine:
workflows(drafts),workflow_steps(branching step tree with same-workflow parent enforcement),workflow_versions(immutable published snapshots, Postgres trigger-guarded),workflow_runs(chained, replayable, causally-linked runs with depth/lineage CHECK constraints), andworkflow_run_steps(append-only execution log, completed/failed rows immutable). Models mirror every DB guard in PHP so SQLite behaves identically. WorkflowSnapshotvalue object — validated, acyclic, JSON-serialisable snapshots built from draft trees or persisted JSONB;WorkflowHandlerRegistryand a newworkflowssettings group (max_delay_days≤ 90,max_chain_depth≤ 5) complete the scaffold.- Production mutation inventory — a static-analysis harness (
ProductionMutationInventory) that discovers every database-mutating call site in production code (1,006 boundaries at release) and drift-fails CI when the checked-in manifest and live source diverge. 307 of them are classified against citedfile:lineevidence — 125 event-sourced Verbs projections and 182 direct audited action writes (same-functionAuditableEvent, a priorGate::authorize, and DTO-validated input, with comments token-stripped so a docblock cannot masquerade as a guard) — and the remaining 699 are held as reviewed placeholders.
MCP server & API CLI
- Signals API MCP server — HTTP transport at
/mcp/api(Sanctum +mcp.access) and local stdio viaphp artisan mcp:start signals-api, gated by Admin → MCP & CLI settings; OAuth support for Claude Desktop / claude.ai connectors (Passport-backed). - Catalogue meta-tools + 31 curated tools —
list-endpoints/describe-endpoint/execute-endpoint/get-model-schemaover the sharedApiEndpointCataloguestack, plus typed hot-path wrappers spanning identity, search + semantic search, rentals (incl. aggregate), catalogue_items, availability, accounts, activities, dispatch/returns, invoices, payments, repairs, and stock checks. - MCP resources & prompts —
schema://models(+ per-model),events://webhookresources;ResolveShortagePromptandQuoteFromEnquiryPromptgrounded flows; a structural test pins every route cited in prompts to the live router. - Plugin MCP tools — plugins declare
tools[]insignals.yamland surface asplugin:{package}:{tool}on both the MCP server and CLI, with MCP-side idempotency (idempotency_key) since plugin-bridged calls bypass HTTP middleware. php artisan signals:api—list/describe/callparity CLI over the same invoker, with plugin tool support and JSON/@filepayloads.- Agent-grade hardening (driven by end-to-end agent evaluations): strict filter feedback (
meta.ignored_filters/ignored_sorts), unconditional idempotency replay, POST-only mutations,GET /api/v1/me+whoami, enumerated valid statuses on 422s, per-callprice_weight+units_remaining, aggregation endpoint (GET /api/v1/aggregate/{model}), composite create, bulk writes, dry-run transitions, cursor pagination, per-group settings scoping (settings:read:{group}), machine-readable error codes, an MCP read-only mode, and a search result ceiling. - Golden-set evals + protocol ratchet —
tests/Evals/mcp-golden-set.yamlwith CI shape checks, andMcpSdkProtocolRatchetTestpinning the installedlaravel/mcpprotocol enum so the 2026-07-28 MCP spec revision cannot arrive silently. - Agent knowledge surface —
SIGNALS.md(domain glossary, engine map, hard prohibitions), a generatedSKILL.mdcapability manifest (API surface, 36 MCP tools, 310 events, 138 abilities, schema models, registries — drift-tested byte-for-byte), andpublic/llms.txt.
API conformance, documentation & tooling
- API conformance harness —
php artisan signals:api-conformance(+bin/conformance.shwith a dedicated reseeded database): eight probe families (contract, docs accuracy/completeness, auth, errors, filters, writes, perf) sweep all 594/api/v1routes with realDB::listen()query evidence and per-route budgets; the findings baseline is empty — every known deviation was fixed at root rather than waived, andcomposer test:allnow runs the sweep after the three Pest lanes. - OpenAPI & Postman exports — the Scramble OpenAPI document is cached and served at
/docs/api.json, transformed live into a Postman v2.1 collection at/docs/api/postman(env-var auth, per-request examples), warmed hourly, and drift-tested against the capability manifest. - Richer per-endpoint documentation — five custom Scramble extensions (price weight, console UI, collection query parameters, request-body inference, DELETE body promotion), auth documentation derived from actual route middleware, spatie/laravel-data schema resolution, and typed parameter/response annotations across 106 of 108 API controllers;
AuthDocsAccuracyTestpins the authentication docs to live settings and abilities. - Five newly filterable/sortable API fields — the schema-derived Ransack allow-lists now expose what the list views already offered:
q[tax_number_*],q[rating_*]andq[last_activity_at_*](plussort=on all three) onGET /api/v1/accounts, andq[version_count_*]withsort=version_countplusq[active_version_id_*]onGET /api/v1/rentals(the active-version foreign key filters only — ordering by a surrogate id is meaningless). /api/v1/schemanow advertises exactly what the endpoints accept. All 66 metadata gaps across 27 controllers are closed (58 flag flips, 8 polymorphic declarations): fields the API had always filtered or sorted on but the schema under-reported are now declared, eight more controllers run on byte-identical schema-derived allow-lists, and the divergence ratchet falls from 33 entries to 21 with a written reason on every survivor. No endpoint changed behaviour — only the advertisement caught up with it.- Column aliases — a column may now declare the database column it actually reads (
is_bookable→bookableis the pilot), honoured end-to-end through the DataTable, saved-view resolution and exports; previously the display key filtered a column that did not exist. Related: relation-display projection (a relation-namedprojectedFieldskey emits the joined display column) and theupdated_atcluster declared on the six models whose controllers already honoured it. - API metering & throughput — observe-mode weighted-unit metering with per-token leaky-bucket daily throughput (standard and high-throughput tiers), usage headers, an endpoint price-weight matrix, and a Usage console with CSV export.
- API Console — dashboard, interactive Explorer, request log, usage analytics, and full outbound and inbound webhook administration (secret rotation, test-event dispatch, health, delivery logs).
Warehouse operations suite
- Dispatch board (
/dispatch) — config-driven kanban (default To pick → Picking → Packed → Loaded) with urgency flags, pick progress, vehicle/crew chips, blockers, and deep links straight into scanning. - Returns board + workbench (
/returns) — stage-driven returns kanban with per-rental workbench and exception dispositions (repair / chase / replace / damage / late) that draft invoice lines. - Scan console — three input adapters (camera QR, keyboard wedge, manual) and eight context handlers (check-in, check-out, global check-in, lookup, prep, stocktake, transfer, sub-hire receiving).
- Stock checks — scoped cycle counts (store / product group / location / product) with blind counting, discrepancy triage, and counter + supervisor sign-off.
- Warehouse transfers — request → dispatch → receive lifecycle between warehouses, with idempotent receipt.
Dashboards
- Multiple named dashboards per user — create, rename, reorder, reset, and delete named layouts with a switcher, GridStack free placement, a widget gallery with live sample previews, and a new-dashboard wizard.
- 34 core widget types across Overview / Rentals / Schedule / Returns / Activity / Commercial categories (12 renderer types incl. charts, heatmaps, calendars, mini boards, pipeline funnels), permission-gated and plugin-extensible.
- Board-level date-range and store filters — one date range and one store selection applied across the whole board, folded into each widget's settings before resolution so cache keys derive from the active filters for free. A widget's own pinned range still wins over the board's (eight widgets opt in via
WidgetDefinition::$boardRangeAware); the store filter is intersected with the viewer's accessible warehouses, so it can only ever narrow the view and never widen it — enforced in the UI and the data layer. Selections persist per user, and read-only shares refuse pins. - Snapshot sharing — publish a read-only dashboard snapshot to roles or users through the shared share-assignment primitive.
- Live refresh without data leakage — Reverb broadcasts carry only entity/warehouse/event identifiers; every viewer's widgets re-query under their own permissions.
CRM, projects & fleet
- Rental Projects — cross-store grouping of rentals with a Draft → Active → Completed/Archived lifecycle, dedicated pages, per-project planner view, financial rollup (quoted / invoiced / margin across warehouses), a dashboard widget, and attach/detach API.
- Fleet management — vehicles as first-class account records (plate, payload, volume, dimensions, type) with dedicated pages and dispatch assignment (an interim assignment model, flagged for the future resource-scheduling engine).
- Customer document portal — tenant-branded public portal for quotes/invoices with approvals and payments via a pluggable payment driver.
- Payments UI — payments list and safe-field edit pages (reference / notes / paid-at editable; amounts locked once recorded; duplicate-reference protection), alongside new safe-field edit pages for Repairs and Credit Notes.
- Stripe — customer-portal payment collection supports embedded Stripe Payment Elements alongside hosted Checkout, configured from Admin → Integrations.
AI & embeddings
- Native vector embeddings — OpenAI / Gemini / Mistral providers over pgvector (HNSW indexes, graceful plain-text fallback when the extension is absent), with polymorphic per-model embeddings, sync jobs on a budget guard, and monthly token budgets.
- Semantic & hybrid search — a similarity API, similar-records surfaces, and hybrid full-text + vector search;
semantic-searchreportsembeddings_available: falseexplicitly when AI is disabled. - Admin AI settings — enable, keys, provider/model, refresh cadence, budget, and usage aggregation under Admin → Integrations (env-driven on Cloud).
Admin, UX & platform polish
-
Admin → System → Storage — a rebuilt storage metrics page: attachment totals, entity/MIME/disk/category breakdowns and upload limits with at-cap counts, computed by six aggregate queries and cached for five minutes with manual recalculation.
-
Refreshed authentication — the login page picks up tenant branding, magic-link passwordless sign-in (single-use hashed 15-minute tokens, full 2FA challenge, anti-enumeration), and a new-sign-in security email for every login path.
-
Email layouts & templates — database-managed layouts and templates with version history, live preview, merge-field/slot browsers, send-test, admin CRUD UIs, and management APIs — all sharing one CodeMirror editor component (with opt-in linting) used across every code-editing surface.
-
Branding tools — primary / accent / secondary / tertiary colours with WCAG-aware colour-theory palette suggestions, logo upload with transparency detection, in admin and the setup wizard.
-
Getting started & tours — the onboarding checklist lives in the persistent header as a progress clock; an in-house guided-tour engine runs on the dashboard and rentals index.
-
Keyboard shortcuts — a declarative shortcut vocabulary across list/show/edit surfaces, arrow-key pagination, and an interactive
?keyboard-map overlay, documented in/docs. -
Consistent listing pages — ~29 core listing pages standardised on the shared DataTable (density preference, string/UUID keys, mobile card mode) with row actions consolidated into a shared ellipsis actions menu; five guided multi-step creation wizards (Accounts, Rentals, Activities, Dashboards, Roles); pre-filter buttons with icons; dedicated create pages for stock checks, equipment_tests, repairs, and credit notes.
-
Admin layout consistency — unified admin chrome and widths, an admin sidebar search, Overview promoted to the primary rail, notification/model-link presenter (clickable notification rows), branded error pages, a seeders panel covering all boot seeders, and a docs-mirroring navigation restructure with a per-screen admin docs page.
-
Custom fields — the consolidated management UI covers 19 field types across 13 entity modules with a visibility-rules builder and role chips.
-
Pinned live filters — pin saved views into a third mega-bar column for one-click access.
-
Mobile & dark mode — responsive passes across dashboards, tables (card-list mode), boards (swipe), planner and line items, plus a full dark-mode token pass.
-
Three new plugin seams —
PluginRegistrar::workflowHandler()for contributing an automation step (vendor-prefix guarded, so a plugin can never shadow a core identifier); a keyedTaxResolverRegistrywhose default resolves the identicalZoneAwareTaxResolversingleton; and anExchangeRateProvidercontract with anExchangeRateQuotevalue object, a registry, and an explicitManualExchangeRateProviderdefault — providers only quote, the conversion path is untouched.SchemaRegistryandWorkflowHandlerRegistrywere promoted from "expose later" to public v1 seams at the same time. The registry inventory goes 95 → 97 and the documented public seams 33 → 37, each with a published contract page under/docs/development/registries. -
Plugin platform additions — manifest
channel_drivers(with a reference Slack webhook plugin), plugin tabs rendered as native module tabs, plugin actions in the shared actions menus, the shared DataTable exposed as an SDK surface, and a compiled plugin manifest cache (bootstrap/cache/plugins.php). -
Permission capability model — capability tags with grant levels powering a guided role-creation wizard and rewritten permission seeding.
-
Nightwatch monitoring — admin-configurable enable/token/sample-rate for self-hosted installs, with default PII redaction and per-category sampling.
-
Self-updating —
php artisan signals:updateand an owner-gated Admin → System → Updates page: Packagist release checking, a six-step guarded install pipeline with live progress, overlap locking, and failure notifications. -
Traffic-light status colours — a unified colour system across statuses, badges, and legends, documented at
/docs/platform/colour-coding. -
New layout prototypes for future designs of common models have been worked on.
Performance, caching & operations
- Perf-audit harness —
signals:perf-audit(query recorder, page measurer, report writer,--log-sql) drove a broad N+1/query-count reduction across dashboards, availability, invoices, accounts, calendars, and modals;QueryBudgetGuardnow enforces per-page query budgets in CI across 13 sentinel pages (dashboard ≤ 28 queries Redis-warm / ≤ 42 database-store), withflightcases.index,rentals.show,rentals.shortagesandwarehouse-transfers.showjoining the guarded set on budgets measured warm against a freshly re-seeded audit database. - Every authenticated page got lighter. The command palette's command list moves behind
GET /palette/commands, fetched on first open — so a page load no longer executes any plugin palette provider or ships ~14.8 KB of inline JSON — and Recently Viewed defers to dropdown-open, following the notification pane's convention. - Fewer queries on the heavy pages.
AccountInsightsServicecollapses its ten revenue-window queries into two portableSUM(CASE)aggregates (byte-identical output proven test-first); the rental index stat tiles go from two scans to one; the product catalogue is cached per store; and the rental page gains an identity map —AvailabilityServiceseeds its product memo from the already-eager-loaded line relation (several product lookups per page down to zero),RentalLineItemsTreeBuildershares one items load across its three computed consumers,RentalTotalsCalculatorprimes catalogue_items in a batch, andflightcases()is memoised where it previously ran twice per render. Reference data is deduplicated behindWarehouseOptionsand a newCatalogueItemGroupOptionsservice (nine raw-query sites retired), with currency lookups cached and busted by aCurrencyobserver. - Cache architecture —
CacheServiceformalises cache-aside with tags where supported and untagged fallbacks elsewhere, multi-get batching, per-domain hit/miss stats surfaced on Admin → System → Caching, and Octane-safe request-scoped memoisation for hot resolvers. - Deterministic frontend bootstrap — Livewire/Alpine start explicitly after module evaluation, eliminating a class of bundle-registration races; toasts unified into one system that survives
wire:navigate. - Scripted demo environment —
SeedsDemoStoryseeds a deterministic three-beat demo narrative through real actions on top of the refreshed full demo dataset (30 accounts, 12-SKU catalogue, 12 rental blueprints, finance/ops coverage).
Developer & test tooling
- CI runs the real matrix —
tests.ymlis now a seven-job graph (asset build, the three test lanes against a live PostgreSQL service with Playwright, Pint, PHPStan, and an aggregatecigate). The always-greenlint.ymlis gone. - A curated Rector pass over the test suite —
rector/rector+pest-plugin-rectorland with a deliberately narrow eight-rule config (45 test files, −133/+67 lines). The shipped Pest rule set was reviewed and rejected:ToBeTrueNotFalseRectoris simply wrong for non-boolean guards, thetoBeEmpty()rewrites weaken assertions, andUsesToExtendRectorfights this project'stests/Pest.phpconvention —rector.phprecords the reasoning and pins all five rejected rewrites inwithSkip(). - Date bombs defused across all three lanes — found empirically by running the suite under shifted clocks (+120 to +1,500 days) rather than by grepping for date literals. The Pgsql lane was entirely unswept and held most of them, with the first detonation due 2026-09-29; eleven tests across eight files now pin their clocks.
- Six browser tests shed redundant per-test permission seeding (
WorldSeederalready covers it pre-transaction), and webhookfull_dtopayloads gain their first custom-field coverage — including the audit-bridge path that re-reads a fresh model, which is exactly the state that used to serialise{}.
Security
- System roles are hard-locked against modification — the five seeded system roles (
is_system) reject every rename, description, permission,cost_visibilityandall_warehouseschange, and cannot be deleted. The guard lives inUpdateRole/DeleteRolerather than in a controller, soPATCH/DELETE /api/v1/roles/{role}refuses with a 422 for exactly the same reason the admin UI, the MCP tools andsignals:apido — no path around it. Customise by duplicating the role. - A 23-item security remediation sweep (SEC-01 → SEC-23) closing 32 findings: deactivation revokes access immediately, document templates reject literal PHP and allow-list Blade directives, saved-view filter allow-lists, permission-gated web twins of API routes, role-hierarchy enforcement on user update, signed job-progress tokens, export-token leak fixes, encrypted settings stay encrypted in shared caches, secret redaction in idempotency replays, webhook SSRF blocking, single-mint ownership, per-account 2FA throttling, MCP redirect allow-list, inbound-webhook audit restoration, CSV formula-injection neutralisation, bounded
_matchesregex, readiness-probe hardening, seeder-password guard, time-limited file URLs, and iCalendar control-character stripping.
Changed
- Toolchain: Pest 5 / PHPUnit 13 under PHP 8.5. The suite runs Pest 5.0.2 / PHPUnit 13.2.6 on PHP 8.5 (project constraint stays
^8.4|^8.5), with laravel/framework 13.23, laravel/ai 0.10, Pint 1.30, and Horizon/Octane/Reverb/Sanctum/Scout/Socialite minor updates. The last seven class-based PHPUnit tests were converted to Pest (the suite is now pure Pest). - Test Impact Analysis is the local standard —
vendor/bin/pest --parallel --tiare-runs only affected tests and replays the rest from a coverage-built graph (logic-aware change detection); the env-writing and Pgsql lanes stay serial. Coverage tooling gains a Herd-bundled-xdebug fallback socomposer test:coverageworks on stock installs. maatwebsite/excelremoved — no stable release permits PHP 8.5. The xlsx import/export layer now runs directly onphpoffice/phpspreadsheet^5.9 with semantics matched to the prior behaviour (chunked reads with heading-row filter, formatted uncalculated values, buffered writes, auto-size).- Webhook
events[]validation is stricter — unknown event names are rejected against the live registry with suggestions (previously accepted by a static list); a migration canonicalises historical malformed names. auditable_idis now consistently a string on action-log payloads — a breaking wire-type change under the pre-1.0 allowance (see Versioning & Deprecation Policy):GET /api/v1/actions, the actions CSV export, and rental activity all serialiseauditable_idas a JSON string ("42"or a UUID), never a number, fixing a 500 on UUID-keyed auditables; Ransack predicates accept either form as a string.- The migration chain is squashed to a single 0.8.1 schema state. The 340 historical migrations are replaced by 23 hand-authored files — 21 domain-grouped
Schema::createmigrations, one cross-domain foreign-key migration, and one reference-data migration — that build the identical schema in one pass. Verified by building both chains and diffing: on PostgreSQL the schemas are identical (only physicalattnumvalues differ, where the old chain left gaps from dropped columns), and on SQLite they differ only in the stored keyword case of twoinvoice_paymentsindex definitions. Every driver-gated structure is preserved as-is: thetstzrangedemand column with its GiST indexes and exclusion constraint, the generatedsearch_vectortsvector, the pgvectorembeddingcolumn, trigram and GIN indexes, partial unique indexes, 13 CHECK constraints, and the four workflow-integrity triggers. A barephp artisan migrate(no seeders) leavesmigrateroughly 3× faster (2.24s → 0.77s to build SQLite from scratch), which cuts test-worker boot time proportionally. - Upgrading to 0.8.1 requires an install that already ran the pre-0.8.1 migration chain — in practice any released 0.8.x. Upgrade to 0.8.0 first, then to 0.8.1. Each squashed migration calls
SquashedSchema::alreadyInstalled(), which looks for the2026_03_11_000010_create_members_tablerow in themigrationstable: present means the historical chain built this database, so the squashed files record themselves and change nothing. Simulated on both drivers — 23 migrations recorded, zero schema change. Tagv0.8.1-pre-squashpreserves the full historical chain, and the oldmigrationsrows are left in place (somigrate:rollbackpast the squash reports the missing files rather than acting on them). - Reference data that the old chain wrote at migrate time is now one explicit migration. A handful of historical
refresh_*/seed_*migrations also wrote rows, so a baremigrateproduced 12notification_types, 2email_templates, and the "Activity Type" list with its 7 values.2026_07_31_000023_seed_baseline_reference_datareproduces exactly that state (same rows, same ids) from the same seeder definitions. The remaining ~35 data migrations were upgrade-only transforms over pre-existing rows — no-ops on a fresh install — and retired with the chain. The one behavioural change: a freshmigrateno longer queuesBackfillSearchIndex, which had nothing to index on an empty database. - No raw table chrome remains on a live admin screen. Email layouts and lists move onto the shared DataTable (with scopes preserving their exact ordering); action log, caching, numbering, languages and the rebuilt Storage page move onto
x-signals.table-wrap, where a DataTable would be the wrong tool (array-backed data, inline editing, expandable rows). - Credit-note eligibility has one source. The "is this invoice creditable" rule lived longhand in four places — the policy, the action, and the picker twice — and is now
Invoice::isCreditable()/scopeCreditable(). The standalone credit-note create flow gains full coverage, including a three-way parity proof against the invoice-context path. - Conventions batch across the service layer —
ShortageController's authorisation moves into its actions; a sharedGuardsItemMutabilitytrait replaces four hand-rolled guard-only Verbs events; nine transition endpoints gain DTOs; address ids are scoped to the acting account at rule level (with exploit tests); and the charge-breakdown maths is single-sourced intoline-item-money.js— both client copies had been missing the Sale-line rule that only PHP applied. CostAdded/CostUpdatedcarry typed enums rather than loose strings, with replay proven byte-identical in both directions.- Dexie is bundled through npm — the CDN
<script>tag is gone, so offline scanning no longer depends on a third-party host at page load. - Dead code removed — the three orphaned admin role/permission views (superseded by the permissions editor) and the unused
rental_items.sort_ordercolumn; saved views that referenced the dropped column degrade gracefully rather than erroring. composer test:allnow chains the API conformance sweep after the three Pest lanes.
Fixed
- Every class-based Volt component ran its data layer twice on each render.
livewire/volt1.11.1 still self-resolveswith()while Livewire 4's ownSupportWithMethodhook calls it again — so all 183 class-based Volt components computed everything twice, and measured pages showed exact 2× duplicate queries. AVoltAwareSupportWithMethodhook supplies theskip()the upstream hook lacks, swapped in during Volt's ownregister()(the only seam early enough, since the hook registry captures hook class-strings by value before any app provider boots). Plain Livewirewith()is untouched, and seven guard tests pin the internals loudly. Duplicate-query ratios collapsed across the board — store transfers 39% → 7%, roles 47% → 8%. - Sub-requests replayed parked after-response callbacks.
Application::terminate()runs terminating callbacks but never clears them, so inside the long-lived MCP, API Explorer and Octane processes everydispatchAfterResponse()job re-ran on each later invoke — measured as a flat +16 cache inserts per subsequent route.OperationInvokernow snapshots and restores the container's terminating callbacks aroundkernel->handle(). expected_versionwas accepted unvalidated on four invoice endpoints —'abc'cast to version 0 and masqueraded as a real optimistic-concurrency precondition. Validation now lives in the sharedexpectedVersion()helper, with 409 and 422 responses documented.- MCP read-only refusals are machine-readable — an
mcp_read_onlycode at all three refusal sites lets an agent tell a policy refusal from a failure;CuratedApiToolgained the invoke-time guardPluginBridgedMcpToolalready had, closing a bypass through a cached tool list. - Stripe portal payments: webhook payload decoding could mis-record hosted-checkout payments; fixed alongside the embedded Payment Elements work.
- Outgoing email rendered in Times New Roman in Outlook/webmail — an email-safe font stack plus a CSS-inlining listener on every outgoing message.
- A session-cookie race under
wire:navigateproduced 403s on the shared two-factor confirmation screen; the confirm step is now idempotent with full-document redirects. - Chart widgets went blank on container resize; whole-widget drag and GridStack wiring fixed in dashboard customise mode.
- Two-step rental PATCHes could persist an inverted hire window (
ends_atbeforestarts_at); the merged post-update window is validated with a 422invalid_date_window. - The api-explorer silently lost endpoint metadata when merging OpenAPI documents; endpoint summaries/parameters restored.
- Datatable click/focus regression, plugin dropdown widths, print-preview modal close, autofocus with caret-at-end across create/edit forms, and a long tail of touch-target and dark-mode fixes.
0.8.0 — Import/Export & Plugin SDK — Global Search, Conversations, Addresses & API Polish
Added
Import, export & migration (Phase 6 Track B)
- Import wizard — a five-stage guided import (upload → map → validate → preview → commit) for CSV, XLSX, and JSON files. Uploads stream into per-batch SQLite staging; mapping auto-matches source columns to schema fields with scored suggestions (exact/normalised/label/alias/fuzzy) and accept-all chips; validation dry-runs every row with per-row error and warning drill-down; preview shows mapped records exactly as they will be created; commit writes through the standard action classes in resumable transactional batches with conflict strategies (skip/update/duplicate-detection) and a full created-record manifest.
- Money-aware imports — currency columns accept decimal amounts ("120.00") and convert to integer minor units through the strip_currency transform pipeline; schema field types drive coercion.
- Mapping profiles — reusable column mappings with conflict strategies and share toggles, applied from the wizard or managed at Admin › Data › Imports › Profiles.
- Migration plans — dependency-ordered multi-step imports (e.g. accounts before catalogue_items) with per-step progress, pause-not-rollback semantics, cross-reference resolution between steps, and explicit admin-only undo via reverse manifest walk.
- Export Centre — export any model or live filter to CSV, XLSX, JSON, or PDF with column pruning and Ransack filters; jobs queue with progress, produce token-secured downloads with 7-day expiry, and honour column-visibility permissions including costs.view gating. Sub-threshold exports run synchronously for instant download.
- Transform pipeline — a registry of import transforms (type coercion, string operations, strip_currency, parse_date, map_values, resolve_ref, split/concat, default_value, if_empty) with a plugin registration seam.
- Import/export APIs — full
/api/v1/imports,/api/v1/exports,/api/v1/import-plans, and/api/v1/import-profilessurfaces (Sanctum abilitiesimports:*/exports:*), Scramble-documented, with batch lifecycle endpoints (mappings, validate, commit, pause, resume, cancel, undo). - Completion notifications — import committed/failed, plan completed/undone, and export completed/failed notification types with database + mail channels and per-user preferences.
- Role defaults — Read Only receives imports/exports view access; Operations Manager and Admin receive the full set including undo.
Plugin SDK (Phase 7 Area B)
- Composer- and zip-installable plugins with a validated
signals.yamlmanifest (30 one-error-per-rule validation keys), guarded lifecycle (install → enable → disable → uninstall → update) andplugin_-prefixed-table migration enforcement with atomic rollback. - Registrar surface — permissions, settings (encrypted), navigation groups and config-driven pages, 28 UI slots across entity headers/tabs/boards/toolbars, global modals, palette commands, recents, notifications, provided events (webhook-exposable and self-subscribable), dashboard widgets, and import/export seams.
- Guarded data facade — manifest-declared field-level read/write access to core entities with denied-write auditing and plugin actor metadata; allowlisted HTTP client and confined plugin storage.
- Plugin identity icons — a top-level manifest
iconrenders through the Plugin Manager, dropdowns, and titles, and backs nav groups that omit their own icon. - Plugin Manager UI + CLI —
/pluginsadmin with manifest review-and-consent, health, hook-failure visibility, and zip upload;signals:pluginCLI (list, create scaffolder, install, remove, enable, disable, check); public manifest validator/builder in the docs and console. - Reference plugins —
signals/slots-demo(kitchen-sink exercising every SDK surface) andsignals/xero-sync(OAuth2 accounting sync exemplar). - Registry inventory & contracts — an architecture-tested registry inventory classifying 86 registries, with doc-sync-gated contract pages for the exposed set.
Platform
- Tenant-editable status vocabularies — Rental, Invoice, and Credit Note statuses are now lists backed: rename built-ins or add custom statuses (anchored to a built-in via
derives_fromfor all behaviour — demand phase, transitions, lifecycle predicates) from Admin › Data › List Names, no deploy required. Event history and replay are unaffected; API shapes unchanged. - Line-item custom fields — rental line items now use standard definition-validated custom fields (EAV) with
cf.*filtering on a new read-only line-items API; existing free-form values are preserved losslessly through a zero-loss migration. - Schema-derived API allow-lists — filter/sort allow-lists and list-view columns for Accounts, Catalogue items, Rentals, Invoices, Purchase Orders, and Import Batches now derive from each model's schema declaration (byte-identical surfaces, enforced by a divergence architecture test), making
defineSchema()the source of truth and/api/v1/schemametadata reflect true filterability. - Admin tables on the DataTable system — twelve admin settings pages (countries, exchange rates, warehouses, users, email templates, list names, and the six tax tables) move from bespoke tables to the shared DataTable with sorting, filtering, and density controls; the DataTable itself gains string/UUID primary-key support.
- Availability API — new read endpoints over the availability engine: a paginated cross-product snapshot timeline, store-level summaries, per-asset availability verdicts, and the demand-management event log — Ransack-filterable, Scramble-documented, and store-scoped at the API boundary (out-of-scope warehouses return 404; lists filter to your accessible warehouses).
- Dashboard completion — Chart.js-backed chart widgets (npm-bundled, theme-aware), role-based default layouts, live widget refresh over a private dashboard channel (payloads carry only entity/warehouse/event identifiers — widgets re-query under the viewing user's permissions), and read-only shared dashboards published by admins with a dashboard switcher.
- One sharing primitive — dashboards and live filters now share a single polymorphic share-assignment model (role and user audiences); the live-filter builder gains a role picker limited to roles you may assign. Existing visibility semantics and the API surface are unchanged.
- Standalone credit-note flow — create a credit note from the Credit Notes page via an invoice picker, deep-linking into the issue modal.
- Destructive-migration guard —
migrate:fresh,migrate:refresh,migrate:reset, anddb:wiperefuse to run against a protected development database unless passed--force-dev.
Internationalisation
- A DB-backed translation system (
languages/language_strings,DatabaseTranslationLoader,lang:syncextraction command) with ~4,385 extracted keys across every module, six fully-translated locales (French, German, Italian, Spanish, Portuguese, Chinese), and an admin Language Manager with per-language completion tracking and import/export.
Design language v2 — application-wide rebuild
- Registry-driven Dashboard v3 (
WidgetRegistry, JSONB layout persistence, edit/resize/reorder), Permissions v2 (role-centric editor with tri-state grants,PermissionSimulator, and cost-visibility enforcement switched on across web + API), Account detail v3 (open work, performance sparkline, revenue mix, credit utilisation with a newcredit_limitcolumn, relationships map), the billing-run console (billing_runsstate machine, exception triage, batch issue, 15-minute undo), the registry-driven print/document system (7 new document types), scan console v2 + shared scan kit, flightcase pack v2, blind stock-count v2 with dual sign-off, command palette v2, notification pane/matrix v2, webhooks + system health v2, the consolidated three-pane Custom Fields studio, cross-entity favourites and recently-viewed, guided tours, wizard-modal create flows, the edit-layout kit with 10+ entity form rewraps, per-user timezone self-service, a Warehouses REST API (/api/v1/warehouses), and invoice custom fields.
Global search (Phase 8 Area C)
- PostgreSQL full-text search backbone — a polymorphic
search_indextable with a generated, weightedtsvectorcolumn (search_vector) ranked viats_rank()/websearch_to_tsquery(); aSearchablemodel trait (toSearchableArray(), label/group/icon, per-instanceisSearchable()) with observer-driven create/update/delete/restore sync queued per model, plus dependent reindexing so a rename (e.g. a supplier) refreshes every record that denormalised its name at write time. SQLite (the default dev/test driver) falls back to a driver-agnosticLIKEmatch with no ranking — real ranking is only meaningful, and only tested, against PostgreSQL. - Command palette rewired onto the new engine — Cmd-K search now queries the shared
SearchServiceinstead of ad hoc per-bucket queries, with permission-filtered result groups and recent-search recording (search/record) so recently-visited results resurface first. GET /api/v1/search— grouped, ranked full-text search across every registered searchable type, restrictable bytypes[]andper_type, gated per type on the caller's search permission.q[search_cont]=value— a new Ransack predicate available on every searchable model's index endpoint, correlating tosearch_index(title/content/flattened meta values) via the same seam the API-native search endpoint uses, composable with every other filter in the request.
Rental Projects API
- Rental Projects REST surface —
/api/v1/rental_projectswith full CRUD plusPOST …/rentals(attach) andDELETE …/rentals/{rental}(detach). Sanctum abilitiesprojects:read/projects:write, Scramble-documented under the Rental Projects tag, with attach rejecting rentals already linked to another project (422) and detach idempotent (204). See Rental Projects.
Conversations (Phase 8 Area B)
- New
conversationsdomain — threaded notes, emails, and system entries attached to a Rental or Account, each with internal/external visibility and a comment thread; Markdown bodies render through a sanitising CommonMark pipeline (HTML escaped, unsafe links blocked). @[Display Name](account:id)mentions — an explicit, ID-carrying mention syntax resolved only to user-type accounts; newly mentioned accounts (excluding the author) receive aconversation.mentionednotification (database + mail) and a matching webhook event, with an inline mention-suggestion picker on the conversation panel.- Conversation panel — a Livewire component embedded on the Rental and Account show pages with inline create/edit/delete for both conversations and comments, policy-gated per record (author-or-admin).
- Conversations API — nested
GET/POST /api/v1/rentals/{rental}/conversationsand/api/v1/accounts/{account}/conversations, flatGET/PATCH/DELETE /api/v1/conversations/{id}and comment sub-routes, Ransack-filterable with?include=comments,comments.author; full webhook coverage for conversation and comment create/update/delete.
Global Address List (Phase 8 Area F)
- Addresses as a first-class, polymorphic entity — no longer embedded free-form fields, addresses now belong to an Account, Rental, or Invoice through a registry-driven addressable map (one-entry extension point for a future parent), with a global
/addresseslist, per-parent address pages, and a create flow's parent-type-and-record picker preventing orphan addresses. - what3words support — an optional what3words address field with lookup-to-coordinates and address-to-coordinates geocoding (degraded gracefully to plain-text storage when unconfigured), plus a map preview with a draggable marker.
- Addresses API —
/api/v1/addresses(global, store-scoped-by-parent-visibility) and nested/api/v1/accounts/{account}/addresses, Ransack-filterable with full CRUD. - Import/export wiring —
AddressImportTargetis now wired into the import commit registry (resolving its polymorphic parent via batch-level context or per-row columns) and addresses are exportable through the Export Centre.
API polish (Phase 8 Area D)
- CORS hardening — cross-origin requests to
/api/*are now governed by an explicit policy (allowed origins default to the app URL, configurable viaCORS_ALLOWED_ORIGINS); rate-limit, request-id, retry, idempotency-replay, and deprecation headers are exposed to browsers; credentialed cross-origin requests remain disabled (token auth needs no cookies). - Sparse fieldsets (
?fields=) — read endpoints for Accounts, Rentals, Invoices, Catalogue items, and Assets accept an opt-in?fields=a,b,c(and nestedfields[relation]=...) projection;idis always included andcustom_fieldsis addressable; omitting the parameter returns the full, byte-identical default response. - Idempotency-Key support widened — the optional
Idempotency-Keyheader is now honoured on every financial, inventory, and document-generating POST endpoint (invoice issue/void, credit notes, purchase order lifecycle, stock transactions, warehouse transfers, stock check submit, virtual stock intake actions, document generation), not just checkout; a new strict mode (api.require_idempotency_key, default off) can enforce the header on every mutating POST under/api/v1, not only the routes with replay semantics. GET /api/v1/changelog— a machine-readable, additive-only changelog of every non-breaking v1 API change (date, affected endpoints, change type, description), backing a documented versioning & deprecation policy and a reusabledeprecatedroute middleware (RFC 8594Deprecation/Sunset/Linkheaders) ready for the first future deprecation.- Uniform async job contract — every
202 Acceptedresponse now includes astatus_urlpointing at a new uniformGET /api/v1/jobs/{job}poll endpoint, alongside the existingjob_idfield and any pre-existing per-resource status endpoint (unchanged). - Webhook test tooling — a "Send test event" action on the API Console webhooks page dispatches a real, clearly-marked test delivery to a configured endpoint so integrators can verify signature verification and payload handling before going live.
- API Conventions doc — a new one-page reference (
/docs/api/conventions) consolidating pagination, filtering, includes, sparse fieldsets, idempotency, async jobs, errors, and events — the canonical checklist every/api/v1endpoint follows.
Platform runtime
- Laravel 13 + PHP 8.4/8.5 — the framework and its supporting packages (Livewire 4, Pest 4, Pint, Larastan) are upgraded onto Laravel 13's streamlined application structure; PHP 8.4 and 8.5 are both supported runtimes.
Fixed
- DataTable bulk selection, keyboard shortcuts, and row selection were inert on every list page due to an attribute-quoting fault; selection also failed on UUID-keyed tables.
- Money fields across seven models were schema-typed as plain integers, mistyping them in
/api/v1/schemaand blocking decimal money imports. - UUID-keyed entities never recorded action logs.
- Custom-field-capable index endpoints issued per-row definition and value queries (the eager load consumed the wrong relation and the definition cache was never container-bound); query counts are now constant per page, and write responses and webhook payloads carry the entity's real
custom_fieldsinstead of{}. - The api-explorer silently lost endpoint summaries, descriptions, and parameters when merging OpenAPI metadata.
- Action modals could freeze as an invisible overlay under automation and heavy Livewire re-renders; modals now open instantly with no timeline-driven fade.
InvoiceVoidedvalidated against a hardcoded status list instead of the configured transition table.
Changed
- System roles are hard-locked (see 0.7.1 note); imports/exports permissions seeded per role.
- The
finller/laravel-moneydependency is removed (unused since the FormatsMoney trait). maatwebsite/excelis the platform's single spreadsheet dependency (imports, exports, and future reporting).
API
- The repairs, stock checks, equipment_tests, and warehouse transfers endpoints now serialise
custom_fieldsas an object ({}when empty), matching the platform-wide RMS contract (previouslynull/array).
0.7.1 — Consolidation — Hardening, Warehouse Scoping, Audit Coverage & Billing Polish
Added
Phase 5.5 is a consolidation round: a cross-cutting hardening sweep over the Phase 0–5 codebase (security, store scoping, audit and webhook coverage, observability, dead-code removal) plus two billing features that close gaps found in day-to-day use.
Billing
- Draft-invoice line-items editor at rental-editor parity — draft invoice pages now use the shared line-items editor with the full rental-editor experience: quick-add-by-catalogue item bar with quantity, inline blur-save editing (description, quantity, unit price, discount %), drag-to-reorder persisted through a new replay-safe
InvoiceDraftItemsReorderedVerbs event, free-text lines, remove-with-confirm, and the charge-total footer. Rental-only concepts (availability status, days, recalculate, deal-price locks) are hidden on invoices via adapter feature flags, and sections stay disabled because invoice lines remain a flat, industry-standard document. Issued and void invoices are fully read-only. - Line-items editor seam — the editor is served through a generic
LineItemsEditorContract/LineItemsEditorAdapterresolver (registry pattern) with a shared JS money module, so future entities can mount the same editor; the rental local-first tree editor is unchanged. - Event-sourced draft-item CRUD —
InvoiceDraftItem{Added,Updated,Removed}Verbs events with draft-only validation, pure appliers, and fire-time payload baking for replay;POST/PATCH/DELETE /api/v1/invoices/{id}/items(invoices:write, issued → 403); totals and tax reuse the rental checkout penny-reconciliation path. - Partial invoicing by % and fixed amount — standard checkout accepts
amount_fixed(gross, minor units) oramount_percentage(of the full gross), mutually exclusive with item lines. Net is apportioned across remaining line weights with exact gross per invoice and 6 dp fractional quantity consumption, so the invoiced-quantity ledger, zero-item guard, and void-release behave unchanged. A partial series always sums exactly to the rental gross — the Full (remaining) closer targets the exact remaining figure and absorbs the rounding residual. - Invoice options modal — the one-click generate buttons are replaced by a modal offering Full (remaining) / By items / Percentage / Amount with a live remaining-to-invoice figure, and deposit generation gains explicit percentage/fixed-amount inputs.
Platform & administration
- Data-driven all-warehouses bypass — a
roles.all_warehousesflag replaces the implicit admin store-scoping bypass; exposed on role create/edit, seeded and backfilled for the Admin role. - NavigationService + BreadcrumbService — registry-driven navigation items with permission gating, command-palette and global-search registration, a plugin
navItem()seam, and a shared breadcrumb component for entity headers. - Structured logging & exception context — named daily JSON channels (
webhooks,imports,auth,api) with sharedrequest_id/user_id/warehouse_idcontext,X-Request-Idpreserve-and-echo, and Octane-safe context flushing. - Health probes — unauthenticated
GET /health/liveandGET /health/ready(database/Redis/queue readiness as bare booleans, 503-capable) outside the web/api middleware stacks;/api/v1/system/healthruns the same readiness subset. - Schema discovery — 13 more first-class admin entities adopt
HasSchema, and the hand-maintained schema controller map is replaced by an auto-discovering catalogue (67 resolvable slugs). - Audit old/new diffs — update-event audit rows now record exactly the changed attributes (previous and new values, encrypted attributes masked) across all plain-Eloquent update actions, with an architecture test guarding future actions.
- Webhook coverage — 36 config-entity events added (discount categories/rules, surcharges and groups, price categories, bundle rules, warehouses, tax zones/categories, accessories, serialised components, exchange rates); exchange-rate CRUD is now audited and webhook-bridged; silent audit gaps closed for supplier costs, surcharge deletes, invoice draft updates, item deal-price discounts, and stock-check discrepancy resolution.
- Relationship types from lists — the account relationship form drives from the seeded "Relationship Type" list; new values are usable without a deploy.
- Demo seeder — coverage extended (kit with BOM components, bundle rule, account addresses, in-app notifications, maintenance purchase order) and made fully idempotent and clearable under
--forcere-runs on both SQLite and PostgreSQL. - Attachment hardening —
attachment.created/attachment.deletedwebhook events and a registry-driven attachable morph map. - New favicon set — SVG + PNG + Apple touch + PWA manifest icons served from a shared partial across every layout.
Changed
- The feature-module/profile system is removed: the setup wizard is now five steps and access control is permissions and roles only.
- System roles are hard-locked — the built-in roles (Admin, Operations Manager, Sales, Warehouse, Read Only) can no longer be edited or deleted through the web UI or
PATCH/DELETE /api/v1/roles/{id}; both return a validation error ("System roles are locked"). Duplicate a system role as a custom role to customise permissions. - The command palette and global search are fully registry-driven (including Document Templates and Numbering entries).
- Exchange rates are cached for 24 hours with invalidation on write; the rate resolver degrades gracefully on cache warehouses without tag support; webhook deliveries are rate-limited per endpoint; settings multi-writes are transactional; the approval-gate pipeline type-hints its Phase 7 interface seam; the Redis connection map is formalised (cache/queue/session/reverb) and provisioned by the installer.
Fixed
- Security & scoping — live-filter visibility enforced on show/clone with an assignable-role guard on shared views; encrypted settings secrets masked in API responses and Livewire wire state with credential pages behind
settings.manage; corrupt-2FA recovery unblocked;/prototypesgated behind auth + admin + non-production; the warehouse-scoping retrofit completed across repairs, stock checks, transfers, and flightcases with cross-store denial tests, plus defense-in-depth policy checks on rental-item field actions and scan sessions. - Setup — the wizard seeds full reference data and wires its tax rate into a real tax rate and rule.
- Dead code removed — the orphaned
rental_sectionscluster and the feature-module system; the fabricated "AI Recommendations" panel is gone from account pages and the catalogue item activity timeline now renders real audit data. - UAT sweep — seven browser-verified defects — draft-invoice line editing no longer flattens sibling duration-priced totals; demo seeding no longer crashes on PostgreSQL or duplicates accounts and warehouses under
--force; toast notifications survive SPA navigation, restoring guard errors, permission denials, and success feedback app-wide; a partial-invoice series can no longer drift a penny over the rental total; and invoice quick-add renders the new row immediately instead of blanking the table.
0.7.0 — Compliance & Fleet — Equipment tests, Repair, Stock Checks, Transfers & Scanning
Added
Phase 5 closes the physical loop around quote-to-cash. The platform now knows what every asset is (identifiers and labels), where it is (transfers, stocktakes, scanning), whether it is safe to send out (equipment_tests, certifications, the dispatch gate), what it truly costs to own (TCO), and what happened to it when things went wrong (repair, damage, write-off, retirement) — with a scan console to drive it all from the warehouse floor.
Every surface below follows the established DTO → Action path with Livewire/Volt UIs, policies, column registries with live filters and CSV export, Ransack-filterable industry-standard APIs, webhooks, notifications, and audit trails.
Fleet & vehicles
- Vehicles as fleet accounts — a
Vehiclemembership type on the universalaccountstable (plate, payload, volume, dimensions, and config-driven vehicle types), so fleet inherits custom fields, files, activities, store scoping, and the account API for free - Bleed guards throughout — vehicles never leak into CRM account lists, pickers, global-search lanes, or statement eligibility; the vehicles index widens only when a
account_typefilter is supplied - Fleet UI — index/show/create-edit pages with saved views, seeded example compliance custom fields (MOT, insurance, service dates), and a dedicated global-search group
- Rental vehicle assignments — relational side-records with advisory windows and list-entry roles, exposed as a nested API and a rental Vehicles tab (no conflict detection — that arrives with the Phase 8 scheduling engine)
- Account-wrapped vehicles API at
/api/v1/vehiclesreturningAccountDataunderaccount/accountskeys, plus nested rental assignment routes
Repair lifecycle → availability
- Typed dispositions — a repair holds stock (bulk quantity or a specific serialised asset) until completed via Return to Stock or Write Off, with mixed partial dispositions on one record and a full transaction history
- Availability integration — an active repair writes a positive demand (held stock consumes a unit exactly like a booking, deliberately not virtual-stock's negative supply), phase-driven by status, with per-source attribution distinguishing held from booked on every availability surface; a repair auto-restores past its window
- Serialised conflict-at-raise policy — a free asset goes into repair cleanly; a future confirmed allocation can be displaced (permission-gated, surfacing the order as short through normal shortage detection); a dispatched asset is rejected toward the return/check-in flow
- Write-off never double-counts — the stock transaction reduces held while the demand voids in the same step
- Repair UI with damage-photo attachments, inline conflict errors, and held-stock indicators riding the existing demand attribution
- Repairs API at
/api/v1/repairswith industry-standard serialisation (numeric repair types, sentinel open-ended dates, active-only default index) pluscompleteandreleaseactions
EquipmentTest & maintenance engine
- Definitions — normalised typed task rows (pass/fail, measurement with bounds, check, choice against list values, text), time and/or usage schedules with whichever-first semantics, catalogue item assignment surfaced industry-standard as a writable
assigned_equipment_test_idsarray, and an industry-standard-shapedtask_listprojection (field_type0–3) rebuilt from the relational rows - Usage readings — monotonic cumulative readings per asset and metric (regressions rejected, naming the current value), append-only corrections that re-baseline, bulk capture, and a nested API under assets
- Execution — a single
PerformEquipmentTestaction captures per-task results as snapshots that survive task deletion, derives the outcome (a failed required task fails the result; optional failures never do), takes attachments, and serialises in an industry-standard shape (task_list_results,passed/equipment_test_ataliases as both response and filter keys) - Due computation — per asset × equipment test: time-based from the last pass (never-inspected baselines from creation), usage-based against latest readings, whichever-first when both, degrading to time-only with an awaiting first reading flag when no readings exist; cached
next_equipment_test_due/equipment_test_statusrollups maintained synchronously on every mutation plus a cluster-safe daily scanner - Certification expiry — certifying equipment_tests carry a validity period; the earliest passed-result expiry feeds
next_certification_expiryand a threshold-gated expiring notification - Failure handling — failed equipment_tests on flagged definitions auto-repair through the shared raise path (origin-linked, conflict policy intact, rejections audit-logged without blocking the result); due/overdue/failed/expiring notifications are transition-gated so daily scans never re-spam
- Dispatch enforcement gate — an
equipment_test_blocktransition rule in the rental guard pipeline (after the shortage gate, before plugin validators): none/warn/prevent per definition, evaluated against the off-hire date so an asset falling due mid-hire is flagged, strictest-wins; warn proceeds only with an override permission plus a persisted acknowledgement, prevent is a hard compliance block the override cannot relax — enforced across asset, bulk, quick book-out, and flightcase/kit manifest dispatch, with "Inspect Now" deep links into the perform flow - Equipment tests API (
/api/v1/equipment_tests) and equipment_test results API (/api/v1/equipment_test_results, perform-only write path) with separateequipment_tests:*andequipment-test-results:*abilities
Asset costs, TCO & retirement
- Typed cost entries (parts, labour, external, consumables, shipping) in integer minor units with currency snapshots and base-currency conversion; TCO rollups (purchase + accumulated, per-type breakdown, catalogue item averages); the Phase 4 cost-visibility pattern applied end-to-end (whole-resource gates, stripped DTOs,
@costssurfaces, cost-gated columns) - Maintenance parts procurement — a new
maintenancepurchase-order type with a polymorphic header link to the equipment_test result or asset; receiving lands line costs on the asset through the existingRecordAssetCostaction with the PO as source, flowing straight into TCO purchase_order_documents— the compatibility read-only PO document projection (header/body/footer/stylesheetsnapshot) with index/show/pdf endpoints- Retirement & disposal — supply-side removal (retired assets drop out of stock counts, availability reads, and allocation validation — no perpetual demand), an active/future-booking guard with a permission-gated override, reinstatement, a final TCO snapshot on the record, and the serialised repair write-off now creating the retirement through the same single path
- Asset costs API — nested index/store under assets plus flat update/delete, cost-gated by
costs.view
Stock checks (stocktakes)
- Lifecycle — warehouse-scoped checks with
SC-sequences, four scope types (store / catalogue item group / location / single catalogue item) plus stock-type, tag, and tracking filters; creation snapshots baselines; counting is line-scoped (additive or absolute bulk counts, present/absent serialised, asset-number entry, unexpected-asset lines) with no check-level lock; compatibility integer status codes with an additive label - Discrepancies → adjustments — submit detects and records the discrepancy matrix without moving stock; all movement flows through resolution into the existing stock-transaction ledger via two new typed entries (
StockCheckIncrease/StockCheckDecrease) carrying the discrepancy as their polymorphic source; missing serialised assets retire as lost through the retirement path; unexpected ones relocate physically with the prior placement saved - Auto-apply and review modes — the default queues a job that resolves every discrepancy and completes the check; review mode parks for per-row resolve/dismiss with auto-completion when the last discrepancy clears
- Compensating revert — reverting an applied check writes opposite-type transactions of equal magnitude, voids check-created retirements, and undoes relocations — the ledger stays append-only and reverts net to zero
- Stocktake UI (count sheet grouped by catalogue item group, mode-aware submit messaging, discrepancy grid, revert with warnings) and the complete compatibility API — nested item counts with
/items,/counts, and/stock_check_itemsaliases, CSV export, and GET|POST action routes (submit,revert,recalculate)
Warehouse transfers
- First-class transfers materialising the Phase 4 shortage-resolver intent as a real entity —
ST-sequences, origin/destination with either-store visibility (dispatch gated to origin access, receive to destination), per-line requested/sent/received quantities, integer industry-standard status codes - Stock moves exclusively through the existing transaction path (
TransferOut/TransferIn; serialised assets also move store), with compensating transactions on in-transit cancel and no transfer-specific availability code - Line-level partial receives through a single seam shared with scanning; the shortage resolver now creates a real draft transfer while keeping its external contract unchanged
- Warehouse transfers API at
/api/v1/warehouse_transferswithdispatch,receive, and line-receive routes
Scanning abstraction layer
- Schema + resolution engine — unique scannable identifiers with polymorphic targets (assets, flightcases, catalogue_items), append-only scan events guarded against mutation, and a registry-driven
ResolutionEnginewith contextual narrowing returning typed outcomes (resolved / unknown / ambiguous / wrong-context / duplicate) rather than exceptions - Identifier management + labels — atomic per-rule sequence generation (single and bulk), assign/replace/deactivate with history, and label printing through the Document Template Engine: a label document type on custom page dimensions rendering dependency-free Code128 SVG barcodes, printed singly or in batches through the normal PDF pipeline
- Input adapters + API + sessions — manual, keyboard-wedge (prefix/suffix/terminator normalisation), and camera-QR adapters; session lifecycle with target binding, expected-set snapshots, and hourly stale-session expiry; a Sanctum-gated scan API where every attempt is recorded regardless of outcome
- Context handlers — check-out (scan allocates-if-needed and dispatches through the real actions — every guard gate applies, refusals surface as scan outcomes with human reasons), check-in (returns including container verify flows), stocktake (feeds the same count seam as manual entry), transfer (line receipts at the destination), lookup (contextless read-only entity cards with live status, current rental, equipment_test due state, and deep links), plus prep (due/overdue equipment_tests with a perform deep link) and sub-hire receiving (against a virtual stock intake through the existing receive action)
- Scan console — an operator page with a data-driven context picker (new handlers appear without console changes), an always-focused wedge input, a live outcome feed with blocked-reason rows, expected-set progress, a bulk quantity prompt writing one authoritative count, lookup cards, and session summaries — with scan-mode deep links from the dispatch/return and count-sheet surfaces, and a session-history admin
- Plugin contracts —
RfidReaderContract(plugins implement only the reader; the core batch processor dedupes, resolves, dispatches, and records) andLabelPrinterContract(consumes rendered labels, typed non-throwing results), proven by fake-implementation conformance tests - Scanning API — read-only
scannable_identifiersindex plus session CRUD, scan, and events endpoints underscanning:*abilities
Kit-return damage & flightcase overrun
- A per-asset damage flag on the flightcase return/verify surface raises a repair through the same shared origin seam (the conflict policy correctly rejects still-packed components until unpacked)
- A scheduled scan flags still-packed sub-hired contents past their return deadline — notification-only with marker-based anti-spam, mirroring the virtual-stock overrun pattern
API
- New REST surfaces under
/api/v1for Vehicles, Repairs, Equipment tests, Equipment test results, Usage Readings, Asset Costs, Stock Checks, Warehouse Transfers, Scanning, and Purchase Order Documents — each with Sanctum abilities, Ransack filtering, includes where relevant, and auto-generated OpenAPI (Scramble) - Webhook catalogue extended for vehicle assignments, repairs, equipment_tests and results, asset costs, stock checks, warehouse transfers, and purchase order documents
Changed
- Rental dispatch pipeline gains an
equipment_test_blockgate after the shortage gate without weakening any existing gate; forrental.dispatchthe active rule sequence is dispatch_shortage → equipment_test_block RecordAssetCostexposes a polymorphicsourceseam and an optional authorisation bypass so the maintenance PO receive path can land parts costs as a system actionCreateAssetTransactionaccepts an optional polymorphic source and an authorisation flag for internal, already-gated movements (repair write-off, stock-check adjustment, transfer)- The shortage warehouse-transfer resolver now creates a real draft
WarehouseTransferwhile keeping itsResolutionResultcontract unchanged - Date and time inputs across production forms now use the shared signals date/time components instead of native browser inputs (12 forms plus the shared custom-field editor)
Fixed
- Boolean-false equipment_test answers are recorded and evaluated correctly rather than being treated as unanswered
- Fleet vehicle show page renders its tabs without error for vehicles that have no assignments or nullable spec fields
- Supplier cost "provided" flags distinguish an explicitly supplied zero cost from an omitted value on purchase-order and intake surfaces
A live UAT walk of every Phase 5 surface closed the phase with fifteen further fixes, the most notable being:
- Date custom fields no longer block entity edit saves — the shared custom-field editor round-trips date values correctly on every entity form
- Repair release recalculates availability — releasing a repair (and the virtual-stock and rental-item resolvers sharing the same path) fires demand events so availability snapshots update instead of going stale
- Volt pages resolve actions through the container, fixing 500s on actions with constructor dependencies
- Maintenance notification defaults no longer target a non-existent role — seven notification types silently delivered to nobody; a data migration repairs existing installs
- Typed barcode scans no longer error on PostgreSQL — non-UUID scan terms are guarded before uuid-column comparisons (a SQLite-lane blind spot, now covered by a PostgreSQL-group regression test)
- Datatable CSV exports emit display values (names, labels, statuses) instead of raw foreign-key ids across all list surfaces
- Permission asymmetry for cost recording (create-only warehouse users can record costs with amounts hidden), purchase-order document numbering, standard datatable toolbars and audit timelines on the new list surfaces, warehouse-transfer dispatch guard messaging, and human-readable transfer list columns
A close-out verification round (lane repairs, an unreachable-code audit, and a live walk of the flightcase flows on real PostgreSQL) added:
- Kit dispatch no longer violates the availability exclusion constraint on PostgreSQL — standing flightcase demands are suspended while a kit is out on hire (flightcase-status-aware phases, constraint-safe ordering) and resume on return; previously every kit dispatch with packed serialised components failed on PostgreSQL and silently double-counted availability on SQLite. Regression-proven against real PostgreSQL
- Equipment test certificates are now reachable — a Generate Certificate action on eligible results and
equipment_test_resultdocumentables on the generate API (the P5 certificate engine had shipped with no trigger) - In-app notification bodies are contextual for flightcase overrun, stock-check, and repair events instead of repeating the notification type's description
- The edit-line modal no longer silently flags manual price overrides — the price field pre-fills only for genuinely overridden lines, with a rate placeholder and a Manual badge otherwise
- A signals time-only picker completes the date/time component family (scheduling settings and the custom-field Time type migrated); the line-items editor's date fields moved onto the shared datepicker via parent-bindable Alpine support; the page-header module tag became a library component across 25 pages
- Purchase-order schema-API registration for the industry-standard columns, a shared list-entry resolution concern replacing six duplicated services, removal of verified-dead code (including an unregistered vehicle policy), and thirteen near-miss classes brought to full coverage
Notes
- Phase 5 deliberately adds no new event-sourced aggregates — repairs, equipment_tests, stock checks, and transfers are plain relational per the phase's binding decisions; one dead event class was removed
- Availability integrity is preserved: repair holds, retirements, transfers, and stock-check adjustments move availability exclusively through the existing demand/transaction machinery — no engine-specific availability code was added
- Single code path: every stock movement and every repair raise, from any origin (manual, failed equipment_test, kit-return damage, stocktake write-off, scan), flows through the same shared action
- Money remains stored in integer minor units and serialised as decimal strings in API responses
- This tag continues the pre-release changelog begun at v0.6.0; it will be published as a GitHub Release after the Phase 5.5 consolidation checkpoint
Testing & Quality
- 10,754 passing tests (32,791 assertions) across three lanes — the parallel SQLite suite (10,519), the sequential
env-writinggroup (97), and the real-PostgreSQL suite (138) - Coverage: 95.82% lines (53,837/56,183), 93.46% methods (6,698/7,167), 90.53% classes (1,635/1,806) — class coverage crosses the 90% bar for the first time this phase and holds it through close-out
- Static analysis: PHPStan level 6 passes with zero errors across the full project (app + tests, no ignores); Laravel Pint clean
- Two dedicated coverage-improvement rounds (~700 behaviour-named tests) plus a close-out top-up cleared the class-coverage bar deferred from Phase 4
0.6.0 — Billing & Financial — Quote-to-Cash, Tax, Documents, Notifications, Sub-Hire & Flightcases
Licensing change
- Signals is now licensed under AGPL-3.0-only, effective v0.5.2 and all later versions (releases prior to v0.5.2, including v0.5.0/v0.5.1, remain available under MIT per the
NOTICEfile). This release ships aNOTICEfile (copyright, commercial licensing contact, trade marks) andCLA.md(Contributor License Agreement — licence grant, not assignment) for external contributors - A platform documentation page for Licensing explains AGPL obligations, commercial licensing enquiries, the CLA, and trade mark restrictions
Added
Event-sourced financial aggregates
- Invoices, payments, credit notes, purchase orders, and virtual stock intakes are Verbs event streams with projected read models, matching the Phase-3 Rental aggregate pattern —
InvoiceGenerated,InvoiceIssued,PaymentRecorded,CreditNoteIssued,PurchaseOrderCreated,VirtualStockIntakeConfirmed, and related lifecycle events - Concurrency-safe document numbering via
document_sequenceswith warehouse-scoped sequences, collision guards, and entity-number generators for accounts and payments; rental numbering converges onto the same sequence infrastructure - Human-readable download filenames on every document download path; entity numbers exposed in search, API DTOs, webhooks, and billing documents
Quote-to-cash
- Checkout — generate an invoice from a rental with a frozen tax snapshot at conversion time;
Idempotency-Keysupport on checkout, payments, and refunds - Invoice issue and lifecycle — draft → issued → paid/overdue states with issue, void, and write-off actions; partial-quantity invoicing for billing only part of an order
- Deposit and pro-forma invoices with partial-invoice tax policy; running balance tracking across payments and credits
- Payments and refunds — record payments against invoices, issue refunds, and generate receipt documents; invoice margin reporting behind
costs.view - Credit notes — full and partial credit with negated tax mirror; credit-note PDFs via the Document Engine
- Customer billing emails — invoice, receipt, and statement delivery through the Notification Engine with merge-field templates
- Finance navigation — full-page Invoices, Credit Notes, and Payments list/detail/create flows under a Finance menu section, gated by finance permissions; bare
/credit-notesand/paymentsURLs redirect with 301s to the canonical routes - Invoicing API — REST surfaces under
/api/v1/invoices,/credit_notes, and/paymentswith Sanctum abilities, Ransack filtering, includes, and OpenAPI schemas
Four-stage pricing pipeline
- Stage 1 — price categories threaded through the Rate Engine with configurable tiers (
P8,P4C21) - Stage 2 — discount rules with a JSONB criteria engine (
CriteriaMatcher), auto-apply, and Verbs event sourcing (P5–P7) - Stage 3 — surcharges with per-surcharge tax types; surcharge line snapshots survive source deletion (
P9,P4C3) - Stage 4 — quantity-break/volume and bundle pricing (
P10) - Deal price — flat-headline and proportional modes with Recalculate, clear flows, and plugin hooks; mutually exclusive with price lock on rentals (
P11,P4C9) - Rounding strategy — half-up and banker's rounding via
PricingSettings(P1)
EU/UK tax engine
DocumentTaxBreakdownextraction and aTaxResolverseam for all financial documents (P2)- Tax zones and tax categories with zone-aware resolution through
DocumentTaxBreakdown(P12–P13) - Four buyer types — business, individual, government, and charity — with reachable seeded zone rules; reverse charge with legal text, zero-rated, exempt, and outside-scope treatments
- Catalogue item tax-category dimension live end-to-end on line items and documents (
P4C2) - Async VIES VAT validation feeding reverse-charge eligibility, with 30-day revalidation and scheduled expiry checks (
P14,P4C10) - Finance settings and tax-point policy with tax field-registry exposure; reverse-charge label setting and tax configuration audit trail (
P15,P4C11) - Fresh installs now compute VAT correctly via named-rate fallback and default tax types
Document template engine
- Document Engine foundations — DomPDF dependency, schema, store branding, and seeded system templates (
P3,P19) - Sandboxed Blade compiler with an allowlist, render pipeline, PDF drivers, and repeating headers/footers on every page (
P18,P4C7) - Field-registry browser and document resolver layer for template field insertion (
P16) - Concurrency-safe document numbering sequences with store scoping (
P17) - Three-panel Livewire template editor — CodeMirror 6 source, live preview, version diff/restore, and export/import (
P20,P4C18,P4C30) - Document API and public token sharing — PIN protection, expiry, and QR codes on shared links (
P21,P4C19); frozen regeneration fromrendered_datasnapshots - Rental Documents tab for attaching and previewing documents on a rental
- Document events, audit trail, and webhooks — including
pdf_generated; plugin seams and resolvers for extensibility (P22,P4C17) - Admin numbering page with live preview (
P4-N4); sandbox hardening against stored XSS on public HTML document responses and IDOR in preview rendering
Notification & communication engine
- Channel provider registry — email, database, broadcast, and a plugin seam for additional channels (
P23) - Three-layer resolution and dispatch pipeline with
communication_logpersistence (P24) - Event→notification wiring with replay guard to prevent duplicate sends on projection replay (
P25) - Merge-field renderer and communication templates with versioning (
P26) - Customer communication path with opt-out/unsubscribe and configurable TTL plus effective prune safety window (
P27,P4C22) - Notification settings UI, notification bell (real-time via Reverb), account notification preferences, and communication log admin view (
P28,P29) - Communication templates and communication-log API; notification preferences and channel-providers settings API (
P4C13,P4C14)
Sub-hire & procurement
- Supplier costs — catalogue item supplier cost reference data (
P40) - Event-sourced purchase orders — full PO aggregate with receipt workflow (
P41) - Virtual stock intakes feeding the availability engine as signed supply (
P42,R1) - Sub-hire shortage resolver with batch cross-rental consolidation (
P43) - PO receipt → intake confirm → shortage clear loop with purchase-axis tax (
P44) - Cost apportionment — five strategies through the cost event stream, with date-change recalculation (
P45,P4C16) - Return-to-supplier workflow with intake P&L surfacing and per-rental P&L (
P46) - Shortage transition API, escalation/overrun jobs, and partial dispatch support (
P47) - Serialised virtual-stock assets for tracked sub-hire inventory (
P4C15) - Field-level cost visibility on POs and intakes —
costs.viewgates cost fields, not whole resources (P4C5,P4C12)
Barcoded flightcase lifecycle
- Flightcase config, templates, and policy validation (
P48) - Flightcase CRUD, temporary flightcases, and pack/unpack events (
P49) - Seal/unseal with compliance manifest (
P50) - Manual dissolve with warn-and-resolve packing and transfer (
P51) - Dissolve-on-dispatch transport mode (
P52) - Kit dispatch, kit return, and check-in modes (
P53) - Repack-on-return with UI trigger, transfer hooks, and editable scan mode (
P54,P55.1) - Flightcase warehouse UI with plugin and webhook seams (
P55) - Cross-engine integration pass validating pricing, tax, documents, notifications, sub-hire, and flightcases end to end (
P56)
Cross-cutting
- Live filters for Phase-4 entities — saved list configs on invoices, credit notes, payments, purchase orders, and related resources (
P4C8) - Global Flux confirm modal replacing native
wire:confirmdialogs across the UI (P4-CL1) - Dashboard KPI queries, full Horizon queue coverage, and Phase-4 demo seed substrate (
P4-CL1) - Webhook parity scanner and temp-barcode namespace guard (
P4C23)
API
- New REST surfaces under
/api/v1for Invoices, Credit Notes, Payments, Purchase Orders, Virtual Stock Intakes, Documents, Document Templates, Document Sequences, Discount Categories & Rules, Price Categories, Surcharges, Tax Zones, Communication Templates, Communication Log, and Notification Preferences — each with Sanctum abilities, Ransack filtering, includes, and auto-generated OpenAPI (Scramble) - Webhook catalogue extended for documents, invoices, payments, credit notes, purchase orders, virtual stock, and flightcase lifecycle events
Also in this tag (v0.5.1 — documented but never published as a GitHub Release)
- API Console — summary at
/apiplus Explorer, Request Log, Usage, and Webhooks; weighted metering (#[PriceWeight]) on endpoints, outbound webhook events, and inbound sources; Redis-buffered usage rollups; inbound webhook receiver with HMAC verification;GET /api/v1/usage,/usage/export,/request_logs,/endpoints - Rental Diary rework — overlap-only timeline (Gantt mode removed), lane-packed job cards, Delivery / In-Use / Collection phase bands, and a per-catalogue item Catalogue item Bookings page
- Documentation IA overhaul — nav-based sections, landing page, and ⌘K search across titles, descriptions, headings, and body text
Changed
- Owner bypass no longer skips state-dependent policy rules — authorization checks remain consistent for all roles (
P4-CL1) - Admin sidebar always shows all sections (previously some were hidden until configured)
- Flat-deal checkout carries headline totals correctly through to issued invoices (
P4 UAT)
Fixed
- Invoice overdue tab uses the derived overdue scope rather than a stale status filter (
P4 UAT R2) - Credit-note modal partial-credit line editor and usage-metering FK resilience (
P4-CL1) - Notification bell pane refreshes on open with event-specific bodies (
P4-CL1) - Invoice payments app-allocated primary key collision (
P4C4) - Invoice comms query uses exact document matching (
P4C6) - Demo seeders survive real tax provisioning and per-store template setup
- UAT sweep — bell badge sync, offline reverse-charge demo, real checklist detections, SQLite-safe tier migration rollback
Notes
- Phase 4 closes the quote-to-cash loop: a rental converts to an invoice, gets paid, and reconciles — with EU/UK tax handled correctly, documents rendered from per-store templates, customers notified automatically, shortages resolved through supplier sub-hire, and serialised flightcases tracked through a full pack/seal/dispatch/return lifecycle
- All financial totals follow the framework's ex-tax model with tax computed finally and grouped by tax type; money is stored in integer minor units and serialised as decimal strings in API responses
- Financial POST endpoints honour
Idempotency-Keyheaders to prevent duplicate side effects on retry - This tag will not be published as a GitHub Release until after Phase 5/5.5; the repo changelog documents the work in the meantime
Testing & Quality
- 9,412 passing tests (28,329 assertions) across three lanes — the parallel SQLite suite (9,183, 10 baseline skips), the sequential
env-writinggroup (97), and the real-PostgreSQL suite (132) — all green at release - Coverage: 96.68% lines (44,290/45,809), 93.50% methods (5,526/5,910), 88.60% classes (1,352/1,526) — the remaining gap is documented genuinely-unreachable defensive code
- Static analysis: PHPStan level 6 passes with zero errors across the full project; Laravel Pint clean
- Shipped after a full pre-release review across correctness, simplification, and maintainability lenses, with all high- and medium-severity findings remediated
0.5.1 — Planning Interface Polish — Overlap Planner, Multi-Store Availability & Catalogue item Bookings
Added
Catalogue item Bookings
- Catalogue item Bookings page at
/availability/bookings/{catalogue item}/{from}/{days}— a per-catalogue item demand grid with summary rows for available, held, and post-rent quantities; one row per opportunity for bulk demand and one row per serialised asset; Delivery / In-Use / Collection day-phase colours on every demand cell; row labels link back to the parent opportunity
Shared Services
App\Services\Opportunities\OpportunityPresentation— single source of truth for opportunity phase band colours (Delivery / In-Use / Collection), status swatches, icons, labels, customer Collecting/Returning badges, and legend assembly; shared by the Job Planner and availability booking gridsApp\Services\Planning\PlanningStoreFilter— multi-store name↔id resolution for planning views (Signals tag-input store filter; empty selection = all in-default-query stores)AvailabilityServicecross-store aggregation —getCalendarForStores,productTotalStockForStores, andgetGanttForStoressum availability and stock across selected stores for the By Group grid and Catalogue item Bookings drill-down
Demo Data
PlannerDemoSeederandAvailabilityDemoSeeder— local demo datasets for the Job Planner and availability grids, including asset allocations
API Console
- Full-page API Console at
/api(new API sidebar section) with a summary dashboard and navigation into Explorer, Request Log, Usage, and Webhooks — gated byapi.accessand per-area permissions (api.explorer.use,api.logs.view,api.usage.view,webhooks.manage) - Explorer at
/api/explorer— interactive request builder backed byApiEndpointCatalogue; select resource, endpoint, query params, and token; executes viaExecuteExplorerRequestusing a short-lived ephemeral Sanctum token that is deleted immediately after each call - Request Log at
/api/logs— unified browse acrossapi,webhook_sent, andwebhook_receivedchannels with date/status/token/method filters, expandable detail rows, and async CSV export - Usage dashboard at
/api/usage—24h/7d/30d/ custom windows, channel filter, breakdowns by day/token/endpoint/resource/status, and async CSV export - Webhooks at
/api/webhooks— outbound subscription CRUD with delivery logs plus inbound source management (slug, secret, signature header/algo, expected events, per-source price weight)
Metering & logging
- API request logging via
LogApiRequestmiddleware on all/api/v1/*routes — captures route, token, timing, status, bytes, and optional redacted bodies intorequest_logs - Weighted metering with integer price weights 0–4; weight 0 = logged in detail and request counts but zero weighted units (audit-only, no quota consumption)
- Three metering channels —
api(REST),webhook_sent(outbound deliveries),webhook_received(inbound receiver) - Daily rollups in
usage_dailywith dedupe keys; Redis-buffered writes flushed byapi:flush-logs(every minute); detail rows pruned byapi:prune-logs(default 90-day retention) - Price weight sources —
#[PriceWeight]attribute on API controllers (PriceWeightResolver); per-event weights inWebhookEventRegistryfor outbound webhooks;price_weightDB column oninbound_webhook_sourcesfor inbound traffic
Inbound webhooks
- Public receiver at
POST /webhooks/receive/{slug}withVerifyInboundWebhookSignaturemiddleware — HMAC verification using per-source secret, algorithm, and header name;200+InboundWebhookReceivedon success,401on bad signature,404for unknown/inactive slugs inbound_webhook_logstable and Request Log integration on thewebhook_receivedchannel
Outbound webhook metering
DeliverWebhookrecordsprice_weight/weighted_unitson first delivery attempt and incrementswebhook_sentdaily rollups usingWebhookEventRegistryweights
API surfaces
GET /api/v1/usageandGET /api/v1/usage/export— aggregated usage with channel/dimension/window filters (api-usage:read)GET /api/v1/request_logs— paginated detail log with Ransack filters (api-log:read)GET /api/v1/endpoints— endpoint catalogue with abilities and price weights (system:read); all meta endpoints declared at weight 0
Registries & catalogue
AbilityRegistry— canonical Sanctum ability definitions with labels and groupsWebhookEventRegistry— extended with per-event price weights alongside the existing allow-listApiEndpointCatalogue— introspected route metadata for Explorer, endpoints API, and request log enrichment
Documentation
- Root
CHANGELOG.md(Keep a Changelog format) and refreshed platform docs for Job Planner and Equipment Availability - Platform page for API Console
- API reference for Usage & Metering
- Updated Webhooks API with inbound receiver and metering weights
- Architecture plan:
framework-plans/api-usage-metering.md
Docs site
- Documentation landing page at /docs — hero plus a card grid per section with icons, descriptions, and key page links (replaces the redirect to the Introduction page)
- ⌘K / Ctrl+K search across all documentation — matches page titles, descriptions, headings, and body text with context snippets; available from the docs header on every page
- Placeholder pages for Workflows and Plugin SDK under Development
Changed
Docs site
- Navigation restructured around the app's areas — top-level sections are now Getting Started, CRM, Job Planning, Resources, Finance, Operations, and Development, with the Changelog at the bottom; page URLs are unchanged
- Collapsible sidebar sections with grouped sub-headings, per-level indentation, and persisted open/closed state; the API reference now sits under Development as a nested sub-section
- SSO and Magic-Link Login pages merged into Authentication & Settings; the old URLs redirect permanently
Job Planner
- Overlap-only timeline — Gantt-per-row mode and the layout toggle removed; jobs are lane-packed into a horizontal day-column grid that fills its panel edge-to-edge
- Per-day job-count heatmap row above the lanes (colour intensity by load) and weekend shading on day columns
- Sticky chrome — control bar, day headers, and heatmap row stay pinned while scrolling
- Lane-packed job cards showing number — subject, date/time range, venue address, status line, opportunity tags, and Customer Collecting/Returning badges; the whole card links to the opportunity
- Card colours and labels driven by
OpportunityStatus; Delivery / In-Use / Collection phase bands snapped to day columns viaOpportunityPresentation - Default view = 1 month, with both orders and quotes included
Equipment Availability (By Group)
- Tightened grid so 30 days fit on load; softer sans-serif table font for long planning sessions
- Multi-store selection via Signals tag-input (default = all stores; availability summed across the selection)
- Signals datepicker; filter toggles for Shortages only, Warnings only, and Include quotes (switches)
- Loading skeletons on first paint; planning filter row aligned with the Job Planner (shared store tag-input, datepicker, and toggle styling)
Fixed
CI & Tooling
- Claude Code Review GitHub Action — restored
anthropics/claude-code-action@v1after a beta→v1 regression (PR #43) - Coverage scripts — purge stale
.phpcoverage dumps before eachcomposer test:coveragerun so merged reports stay accurate
0.5.0 — Rentals, Availability Engine, Shortage Resolution & Quote Versioning
Added
Rentals
- The rental — the event-sourced quote/order document at the centre of a rental job — with a two-axis model: a state (Draft → Quote → Order) and a per-state status (Quote/Reserved, Order/On Hire, and so on). Reads hit a fast read-projection; every write is recorded as an event so the full history is preserved and replayable
- Full-page Rentals list at
/rentals(under a new Job Planning navigation section, gatedrentals.access) with Active / Archived / All filtering, state and status chips with counts, column toggling, export, multi-select bulk archive/restore, row actions, and saved live filters (All / Drafts / Quotations / Orders) - Rental detail page with tabs for items, costs, versions, shortages, activities, custom fields, and files; a header carrying the state and status badges, breadcrumbs, an Edit action, and a split-button of the legal state-transition actions
- State-transition actions — Convert to Quotation, Convert to Order, Reinstate, Reopen, Revert to Quotation, Revert to Draft, Lock / Unlock pricing, and Change Status — each offered only when legal for the current state and status (the page asks the same
available_actionsengine the API exposes), with raced or illegal moves surfaced as clear messages rather than errors - Create / edit form mirroring the API path: title, account, store, currency, hire and charge dates, reference, tags, tax-inclusive flag, and custom fields, all flowing through the same
CreateRental/UpdateRentalactions and DTOs as the API; rental numbering follows the configurable Rental Settings format - FX and tax locks — a rental snapshots its exchange rate and tax treatment when committed so later rate or tax-rule changes don't silently alter an agreed deal; an Unlock Rates action (gated
rentals.unlock_rates) releases the locks to re-rate - Asset allocation, dispatch, and return for orders — serialised catalogue_items allocate to specific assets with batch allocate / prepare / book-out / check-in actions; bulk catalogue_items dispatch and return by quantity, driving the order's statuses (Active → Dispatched → On Hire → Returned → Checked → Complete) with automatic status promotion
Line-Item Editor
- A fast, local-first line-item editor on the Items tab — edits apply instantly in the browser and sync to the server in the background, so typing, dragging, and adding lines never wait on a round-trip — with live ex-tax totals (line, group, and grand) recomputed through the rate and tax engines on every change
- Two-tier catalogue item search — an instant client-side index plus a debounced server fallback (PostgreSQL
pg_trgmtrigram matching) that marks newer, server-only catalogue hits with a "new" badge — and a quick-add bar that parses a quantity and term together (6 spiider), with full keyboard navigation - Nestable groups — catalogue item lines, group rows (named sub-sections with their own subtotal), and free-text note lines all live in one drag-sortable tree; drag any row to reorder, nest a line under a group, or nest groups within groups, and add lines, groups, or text inline. Ungrouped lines fall back to grouping by their catalogue-item-group tree
- Deal price and price lock — set a single negotiated deal total that caps the headline charge while keeping the component breakdown, or lock the pricing to freeze every line's rate, quantity, discount, days, and removal against further edits; the two are mutually exclusive
- Per-line edits — quantity (with rippling accessory quantities), manual price override, discount, line-level dates, optional flag, catalogue item substitution, description and warehouse notes, merge-duplicates, and removal
- Accessory sub-rows — real, persisted line items nested beneath their principal catalogue item, priced through the rate engine and generating their own availability demand, whose quantities ripple from the parent line by their configured ratio — and per-line availability status chips (available / reserved / booked-out)
Availability Engine
- Demand-based availability —
available = total stock − total demandedper catalogue item, warehouse, and time slot, at a configurable resolution (daily, half-daily, or hourly), backed by PostgreSQL range types, a GiST index, and an exclusion constraint - Full-page Equipment Availability view at
/availability(Job Planning section, gatedavailability.view) with three view modes: a By catalogue item group grid (the default), a catalogue items-by-days Calendar reading a pre-calculated daily summary, and a per-catalogue item Timeline (Gantt) decomposing each booking into its prep, on-hire, and turnaround zones - By catalogue item group grid — select a catalogue item group and date window (7, 14, 28, 30, or 60 days) to see every catalogue item's daily availability as colour-coded cells (green/amber/red/cyan for available/warning/shortage/booked) with available and held quantities; filterable by shortages-only or warnings-only; rows link directly to the per-catalogue item timeline
- Warehouse, date-range, catalogue item, and view state persisted in the URL; a shortage count for the visible range; and a caveat surfaced for kit/composed catalogue items, whose availability is read-time and so do not appear in the daily-summary grids
Shortages
- Live shortage detection — the gap between what a rental's lines need and what is free for their catalogue_items, store, and dates; shortages are computed on every read and never stored, while resolutions and acknowledgements are persisted
- A Shortages tab (gated
shortages.view; resolving needsshortages.resolve) showing each short line's requested, available, shortfall, and remaining-shortfall figures, with critical shortages flagged on confirmed orders - A Block / Warn / Allow confirmation gate on Convert to Order driven by the warehouse's shortage policy, with the
shortages.ignorepermission relaxing the gate one level and recording an acknowledgement (a frozen snapshot of the shortage state) when proceeding past a warning - Six non-purchase-order resolvers — Reallocate, Substitute, Transfer, Date shift, Partial, and Waitlist — each contributing concrete options per short line, with self-contained resolvers (Partial, Date shift, Waitlist) taking effect immediately and the rest recording a pending intent for follow-up
- A persisted resolution lifecycle (Confirm → Start → Fulfill, plus Cancel and Fail with reasons), with only the legal transitions offered and the warehouse's dispatch policy surfaced for fulfilment
Quote Versioning
- A Versions tab maintaining multiple quote versions per rental — revisions (successive iterations that supersede their parent) and alternatives (parallel options) — shown as a version tree with the active version marked and each node showing its type, status, net total, and author
- Per-version lifecycle actions (Create, Activate, Send, Accept, Decline, Rename, Delete) offered only when legal, with activation confirm-gated because it swaps the reserved demand to the newly active version
- A version diff — select any two versions to see the added, removed, and changed lines with the net total delta; on Convert to Order the accepted (or active) version becomes the order and the rest are superseded
Dashboard & Navigation
- A new Job Planning navigation section (header mega-menu and sidebar) hosting Rentals, Equipment Availability, and the Rental Diary, gated
rentals.access - A dashboard pipeline + shortage widget and a New Rental quick action (gated
rentals.create) - A Rental Diary at
/rental-diary— a visual Gantt and overlap timeline of quotes and orders across a configurable 1-week, 2-week, or monthly window, with store, type (quotes/orders), and free-text filters and a colour key by workflow status
Real-Time
- The Laravel Echo / Reverb client bundle is wired, so rental availability badges, the Equipment Availability calendar and Gantt, and the Shortages panel all update live (via
availability.rental.{id},availability.store.{warehouseId}, and per-catalogue item channels) as bookings, returns, stock movements, and resolutions occur
API
- New REST surfaces under
/api/v1for Rentals (CRUD, lifecycle transitions, line items, costs, assets, allocation/dispatch, deal-price override, quote versioning, available-actions engine, involved_contacts, restore, clone, reinstate, reopen, revert-to-quotation, revert-to-draft, unlock-locks), Availability (point and range reads, available-assets, calendar grid, Gantt, store-wide shortage sweep), Shortages (detection, shortage gate pre-check, resolver listing, resolution, resolution lifecycle transitions, resolutions index, acknowledgement), and Flightcases — each with Sanctum abilities, Ransack filtering, includes, and OpenAPI schemas
Documentation
- Platform documentation pages for Rentals (the lifecycle, line-item editor, quote versioning, state transitions, and FX/tax locks), Equipment Availability (the calendar grid, demand-bar Gantt, and resolution setting), and Shortages (detection, the Block/Warn/Allow gate, the six resolvers, and the resolution lifecycle)
Notes
- Rentals are one of the three soft-deleted entities; the list offers Active / Archived / All with archive and restore, and there is no merge (rentals are not a duplicate-prone entity)
- All rental totals follow the framework's ex-tax model — line and group totals are net, and tax is computed finally, grouped by tax type
- Catalogue item lines, group sub-sections, and free-text lines are all event-sourced rows in a single nested line-item tree (a unified model — there is no separate sections table); each row's order and nesting are part of the rental's event history and survive replay
- Availability on PostgreSQL uses native range types, a GiST index, and an exclusion constraint; the calendar grid reads a pre-calculated daily summary and so excludes kit/composed catalogue_items, whose availability is derived at read time
Testing & Quality
- 6,654 passing tests (19,529 assertions) across three lanes — the parallel SQLite suite (6,470, 8 skipped), the sequential
env-writinggroup (97), and the real-PostgreSQL suite (87) — all green at release - Coverage: 96.64% lines, 95.31% methods, 91.24% classes (the remaining gap is documented genuinely-unreachable defensive code — concurrent-delete race guards, schema-impossible states, exhaustive-match defaults)
- Static analysis: PHPStan level 6 passes with zero errors across the full project (~1,700 files); Laravel Pint clean
- Shipped after a full pre-release review across correctness, simplification, and maintainability lenses, with all high- and medium-severity findings remediated
0.4.2 — Activities Calendar, iCal Feeds, SSO & Magic-Link Login
Added
Calendar
- Full-page Calendar at
/calendarthat visualises scheduled activities, gated on the existingactivities.accesspermission — no new entity, table, or permission - Three views — Day (staff columns against a vertical hour-axis), Week (seven day-columns against an hour-axis, coloured by owner; the default view), and Month (a seven-column grid of day-cells with event chips)
- Pixel-precise hour-axis placement in Day and Week views; the visible hour window auto-expands to include any event outside working hours so nothing is clipped
- All-day handling computed from the activity's times (starts at
00:00with no end or ending at23:59/next midnight), rendered in a band above the grid and as month chips - Pickable start date defaulting to today and looking forwards, with Today / Previous / Next navigation that steps by the active view
- Owner filter restricting the calendar to selected staff users, with deterministic per-user colours shared across columns, blocks, chips, and avatars
- Owner and participant avatars on each event (owner first, then participants), showing each user's photo where available and coloured initials otherwise
- Unscheduled tray listing activities with no start time, each opening its detail modal so it can be scheduled
- View, date, and filter state persisted in the URL so a calendar link reopens exactly as shared
Modals & Realtime
- Add modal opened from an empty slot (pre-filled owner and start time) or a month day-cell (pre-filled date), reusing the existing activity form
- Detail modal showing an activity's subject, type, status, owner, time, and location, with Complete, Edit, and Delete actions and a link to the full
/activities/{id}page - Edit modal using the same form as create, reached from the detail modal
- All writes flow through the existing
CreateActivity/UpdateActivity/CompleteActivity/DeleteActivityactions — same validation, authorisation, audit logging, andactivity.*webhooks — and the calendar refreshes instantly and locally after every change
iCal Feeds
- Secure global iCal feed at
/calendar/feed.ics(every scheduled activity) and per-user feeds at/calendar/feed/{user}.ics(a single owner's activities) - Feeds secured with Laravel signed URLs (no expiry); a missing or tampered signature returns
403, and the feeds serve without an authenticated session - Feed window covers activities from one year ago with no forward bound; activities without a start time are excluded
- Hand-rolled RFC 5545 output (no new Composer dependency) — one
VEVENTper activity withUID,DTSTAMP,DTSTART/DTENDin UTC,SUMMARY,LOCATION,DESCRIPTION,STATUS,TRANSP, andORGANIZER, with proper text escaping and 75-octet line folding; all-day events emit;VALUE=DATE - Copyable subscribe URLs surfaced in a feed modal on the calendar page (global and own feed for every user; full per-user list for administrators)
Settings
- New Settings → Calendar page at
/settings/calendarshowing the current user's personal feed: a subscribe URL with a copy button, a Download .ics button, and Google/Apple/Outlook subscription help; administrators also see the global feed URL
Navigation
- Calendar opened from the top navigation bar's calendar icon (gated on
activities.access), with Activities relocated into the CRM menu; a "Go to Calendar" command-palette navigation entry is also available
Documentation
- Platform documentation page for the Calendar (views, navigation, owner filter, modals, unscheduled tray, the settings page, and the week-start / working-hours / weekend settings that affect it)
- API documentation page for the calendar feed endpoints, covering the signed-URL security model and its global-only revocation tradeoff
Single Sign-On (SSO)
- SSO login via Google and Microsoft 365 (Azure
commontenant) powered by Laravel Socialite — staff can sign in with their Google or Microsoft account without a Signals password - Auto-link by verified email: on first SSO login the provider's verified email is matched to an existing, active Signals user and the identity is linked automatically; unknown or unverified emails are denied and the user must be invited before using SSO
- Subsequent logins match by stored provider identity (robust to later email changes in Signals)
- Self-hosted credential configuration in Settings → Integrations → Single Sign-On: per-provider enable toggle, Client ID, and Client Secret (stored encrypted at rest; write-only — not re-displayed after save), with the correct callback URL shown for copy-paste into the Google Cloud Console or Azure portal
- Signals Cloud mode: credential fields are hidden in the Integrations UI; only enable/disable toggles are shown (credentials are managed centrally by Signals Cloud)
- Per-role SSO enforcement in Settings → Security → SSO Enforcement: admins can require specific roles to use SSO; password login for enforced roles is blocked with an inline guidance message and SSO buttons; the Owner role is always exempt (break-glass — cannot be selected)
- 2FA still applies: SSO replaces only the password step; users with 2FA enabled are redirected to the existing one-time-code challenge after a successful OAuth callback
- Allowed email domains allow-list (Settings → Integrations → Single Sign-On): when set, only users whose IdP email domain is on the list may sign in or auto-link — enforced on both the auto-link and existing-link paths — strongly recommended with Microsoft's multitenant
commonsetting to prevent cross-tenant account takeover; an empty list permits any domain - Client secrets are now write-only end-to-end: they are never loaded back into the Integrations form, never rendered to the browser, and a blank secret field on save keeps the existing stored secret (enter a new value to rotate)
- Routes:
GET /auth/google/redirect,GET /auth/google/callback,GET /auth/microsoft/redirect,GET /auth/microsoft/callback; unknown providers return404 oauth_identitiestable andOAuthIdentitymodel recording the(provider, provider_id, user_id, email)link
Authentication — Password Manager & Accessibility
- One-time-code 2FA input now auto-submits when six digits are entered, removing the need to press Enter or click a button
/.well-known/change-passwordwell-known URL pointing to the profile password-change page, so browsers and password managers can locate the change-password flow automatically- Invitation acceptance page includes a username field pre-filled with the invited email, giving password managers the anchor they need to save the credential against the right account on first use
- Login and password forms now carry canonical
autocomplete="username"/autocomplete="current-password"/autocomplete="new-password"attributes throughout the auth flow for consistent password-manager integration
Documentation
- Platform documentation page for Single Sign-On — covers enabling Google and Microsoft (self-hosted and Signals Cloud), callback URL registration, auto-link matching, per-role enforcement, and 2FA interaction
Magic-Link Login
- Passwordless login for existing users via a single-use link emailed to their address — an opt-in alternative to password login, enabled in Settings → Security → Magic-Link Login (off by default)
- Links are valid for 15 minutes and can only be consumed once; requesting a new link invalidates any prior unconsumed link for that account
- 2FA is not bypassed — users with 2FA enabled (or enforced) are redirected to the existing one-time-code challenge after the link is consumed
- Anti-enumeration: Signals always returns the same neutral response ("if an account exists, we've emailed a link") regardless of whether the email is registered, so no information about account existence is leaked; the email is dispatched asynchronously so response timing cannot be used to infer an account's existence
- Blocked for SSO-enforced roles at both the request step and the consume step, so a policy change after issue invalidates the outstanding link; the Owner role is always exempt
- Tokens are stored as a SHA-256 hash only — the plaintext travels only in the email and is compared with
hash_equals; request and consume endpoints are rate-limited per email and per IP - Platform documentation page for Magic-Link Login — covers the login flow, enabling the feature, anti-enumeration behaviour, SSO interaction, and the security model
Changed
Activities
- Activity type is now a user-editable list of values — the seeded "Activity Type" list (Task, Call, Fax, Email, Meeting, Note, Letter), manageable in Settings → List Names — replacing the previous fixed enum. The change applies across the Activities pages, the Calendar create/edit/detail modals, and the API, where
type_idnow references alist_valuesid and responses includeactivity_type_name - Activity participants now resolve to the linked user's name and avatar (with photo where available) on the activity page, in the calendar, and in the owner/participant pickers; the activity page lists the owner first followed by participants
- Activities API operations now publish full OpenAPI request/response schemas — the paginated list envelope, the single-resource envelope, and the create/update request bodies
Notes
- The calendar reuses the existing Activities write path and
activities.*permissions; no new permissions, webhook events, or database tables were introduced - Feed URLs are not individually revocable — rotating the application's
APP_KEYinvalidates all existing feed URLs at once
0.4.1 — Member Merge, Anonymise & Docs Overhaul
Added
Accounts — Merge & Anonymise
MergeMemberaction — transfers relationships, contact details, custom field values, and memberships from a secondary member to a primary member of the same type, then archives the secondaryAnonymiseMemberaction — erases a member's personally identifiable information (name, description, icon, emails, phones, addresses, links); irreversible; users cannot anonymise their own recordPOST /api/v1/members/{account}/mergeAPI endpoint — requiresmembers:writeability andmembers.deletepermission; returns the updated primary member under thememberkey; 422 on self-merge or membership-type mismatchPOST /api/v1/members/{account}/anonymiseAPI endpoint — same auth requirements; 422 if the authenticated user targets their own recordmember.mergedandmember.anonymisedwebhook events — both registered inWebhookService::EVENTSand dispatched by their respective actions
Navigation & UI Permission Gating
- CRM mega menu in the application header is now gated behind
members.access— hidden entirely for users without access - Command palette "Members" navigation entry and "New Member" create entry gated behind
members.view/members.createpermissions respectively - Dashboard "Add Member" quick action is now a live link gated behind
members.create
Removed
- Admin Modules page (
/admin/settings/modules) — the page's toggle cards did not actually enable or disable functionality. Module selection now lives solely in setup feature profiles (FeatureProfile::modules()); storedsettings('modules.*')values are unaffected
Documentation
docs/api/webhooks.md— Events table expanded from 7 to 39 events, grouped by domain (Members, Products, Stock, Activities, Rate Definitions, Tax, Users, Roles, Settings)docs/platform/members.md— updated bulk actions (archive + conditional merge), row actions (View/Edit/Archive/Restore), archive filter chips, column toggle/export, merge modal, and anonymise; detail page tabs corrected from 7 to 8 (Overview, Information, Contacts, Activities, Opportunities, Movements, Invoices, Files)docs/getting-started/seeders.md— expanded from 4 to 16 seeders; added CountrySeeder, ListOfValuesSeeder, CurrencySeeder, TaxClassSeeder, TaxRateSeeder, RevenueGroupSeeder, CostGroupSeeder, ProductGroupSeeder, EmailTemplateSeeder, NotificationTypeSeeder, ViewSeeder, RateDefinitionPresetSeeder, ProductSeeder, and ActivitySeeder; corrected DatabaseSeeder run order; addedsignals:seed-demo/signals:clear-demodemo data commands; updated idempotency notesdocs/api/members.md— documentedPOST /mergeandPOST /anonymiseendpoints including request body, response shape, and 422 error cases
0.4.0 — Rate Engine
Added
Rate Engine
- Composable rate engine — a rate definition combines a calculation strategy, a base period, and optional modifiers to describe how a charge is calculated over a rental window
- Three calculation strategies — Period-based (charge per chargeable unit), Fixed (flat charge), and Hybrid (fixed initial charge plus per-unit charge thereafter)
- Five base periods — half-hourly, hourly, daily, weekly, and monthly, each converting elapsed time into chargeable units
- Multiplier modifier — tiered duration multipliers that scale the unit price as a rental lengthens, with the final tier inheriting forward
- Factor modifier — quantity ranges that scale the per-unit subtotal, with an open-ended final range
- Rate breakdown value objects — lossless integer-minor-unit arithmetic via Brick Money, rounded once at assembly, with structured line items and applied-modifier records
- Time options for period and hybrid strategies — clock vs business-hours day type, business hours, rental days per week, leeway minutes, and first/last day cutoffs
- 11 rate definition presets replicating industry-standard rate engine types (Daily Rate, Daily Multiplier and Factor, Hourly Rate, Hourly Multiplier and Factor, Half Hourly Rate, Weekly Rate, Monthly Rate, Monthly Multiplier and Factor, Fixed Rate, Fixed Rate and Factor, Fixed Rate and Subs Days)
- Presets seeded on install and re-runnable from the Database Seeders admin panel
- Rate resolver — resolves the highest-priority catalogue item rate by store, transaction type, date validity, and priority, with per-catalogue item tagged caching invalidated on write
RateTransactionTypeenum (rental, sale, service) for catalogue item rates, distinct from the int-backed stock transaction type
Config Schema System
- Reusable
App\Support\ConfigSchemasubsystem for dynamic, schema-driven configuration forms — Field hierarchy (Text, Number, Decimal, Toggle, Select, Time), GroupField, RepeaterField, Schema, and Section - Conditional field visibility evaluated server-side, validation rule generation for visible fields, and sanitisation that strips hidden and disabled-modifier values
Persistence
rate_definitionstable — strategy, base period, JSONB enabled modifiers and config, preset slug, and self-referential clone trackingproduct_ratestable — integer minor-unit price, transaction type, currency, optional warehouse scope, validity dates, and priorityRateDefinitionandProductRatemodels with factories, schema definitions, and aCatalogue item.rates()relationship
Actions & DTOs
- Seven rate actions — Create/Update/Delete/Duplicate RateDefinition and Create/Update/Delete ProductRate — each authorising via gate, wrapping writes in a transaction, firing audit events, and dispatching webhooks
- Rate DTOs for input validation and API serialisation, including
RateBreakdownData(decimal-string money, structured line items) - Config validation on create/update against the composed schema; disabled-modifier configs stripped on update
- Non-blocking catalogue item rate overlap detection surfaced as a warning
API
- Rate definition endpoints — full CRUD plus duplicate, with Ransack filtering by strategy, base period, and preset
- Nested catalogue item rate endpoints under
catalogue items/{catalogue item}/rates, with anoverlapping_rate_idswarning in the response meta POST catalogue items/{catalogue item}/calculate_rate— returns a rate breakdown, or a zero-priced breakdown withmeta.resolved: falsewhen no rate is configured- Rate engine metadata endpoints —
rate_engine/strategies,/modifiers,/presets, and/schemafor external form builders rates:read/rates:writeSanctum abilities and therates.*permission group
Admin & Catalogue UI
- Rate Definitions admin panel under Admin → Pricing — list of presets and custom definitions with in-use counts, schema-driven create/edit form with preset picker and From Scratch option, and duplicate support
- Server-driven config form — choosing a strategy constrains base periods, enabling a modifier reveals its tier/range table, and fields show/hide based on other values
- Config-schema Blade field partials styled to Signals conventions, including bordered repeater row cards with add/remove/reorder controls
- Catalogue item Rates tab on the catalogue item detail page with a dedicated rate assignment form, modal-confirmed removal, and an overlap warning banner
- New Pricing group in the admin sidebar, landing grid, and group switcher
Documentation
- Platform documentation page for Rate Definitions
- API documentation page for rate definitions, catalogue item rates, calculation, and metadata, including the industry-standard rate engine mapping table
Changed
AppServiceProviderregisters the rate engine registry as a singleton with the three strategies and two modifiersPermissionSeederadds therates.*permission group, assigned to the Admin and Operations Manager rolesCompleteSetupandDatabaseSeederseed the rate definition presets on install
0.3.0 — Products, Activities & Inventory
Added
Products Module
- Catalogue item model with
ProductTypeenum (Rental, Sale, Service, Loss & Damage),StockMethodenum (Bulk, Serialised), andHasSchemaintegration - Products list page with type filter chips (Rental/Sale/Service), archive filter (Active/Archived/All), per-type counts, and data table with column sorting, filtering, and bulk selection
- Catalogue item detail page — 3-column layout with description, quick stats, catalogue item group, tax & revenue (left), catalogue item details, pricing, activity timeline placeholder (center), catalogue item image, key attributes, tags (right)
- Catalogue item create and edit forms — 2-column layout with basic info, identification, stock, pricing, tax & revenue, options, and custom fields panel
- Catalogue item merge — select two catalogue items, side-by-side comparison modal, migrates stock levels, accessories, attachments, and custom fields to primary; archives secondary
- Catalogue item icon upload on show and edit pages
- Catalogue item tabs — Overview, Stock, Accessories, Custom Fields, Activities, Files
- Catalogue item API endpoints — full CRUD with Ransack filtering, custom view support, and industry-standard response shape
- Catalogue item policies for view, create, edit, and delete permissions
- Catalogue item column registry for custom views with 11 columns and 6 defaults
- Custom views for products — All Products, Rental Products, Sale Products, Active Products, Inactive Products
catalogue item groups
- catalogue item group model with hierarchical structure and catalogue item count
- catalogue item groups list page with data table, search, and catalogue item count column
- catalogue item group detail page — sidebar with group details, main area with catalogue items-in-group data table
- catalogue item group create and edit forms
- catalogue item group API endpoints — full CRUD with Ransack filtering
- catalogue item group policy for view, create, edit, and delete permissions
- Delete catalogue item group action with audit trail and webhook dispatch
- Navigation — mega menu link, command palette commands
Stock Levels
- Stock level model with
StockCategoryenum (Bulk, Serialised) andHasSchemaintegration - Stock levels list page with data table and search
- Stock level detail page — 2-column layout with details, quantities (with available calculation), dates, and transactions panel
- Stock level tab on catalogue item pages — scoped data table showing stock for the catalogue item
- Stock level API endpoints — full CRUD with Ransack filtering and custom view support
- Stock level policies and column registry
- Custom views — All Stock Levels, Serialised Stock, Bulk Stock
Stock Transactions
- Stock transaction model with
TransactionTypeenum — 11 types: Opening Balance, Increase, Decrease, Buy, Find, Write Off, Sell, Return, Make, Transfer Out, Transfer In - Signed quantity calculation via
quantitySign()— negative for reductions (Decrease, WriteOff, Sell, TransferOut), positive for additions quantity_moveaccessor on model — signed quantity for display- Manual transaction creation restricted to Buy, Find, Write Off, Sell, Make (system types like Opening, Transfer are API-only)
- Inline transaction form on stock level show page — type selector, quantity, date, description
- Automatic
quantity_heldupdate on stock level when transactions are created - Stock transaction API endpoints — industry-standard nested routes (
catalogue items/{catalogue item}/stock_levels/{stock_level}/stock_transactions) with index, show, and store - industry-standard response shape with
transaction_type,transaction_type_name,quantity,quantity_move,manualflags
Activities (CRM)
- Activity model with polymorphic
regardingrelationship (Member, Catalogue item, StockLevel),owned_byuser, andparticipantsmany-to-many with members ActivityTypeenum — Task (1001), Call (1002), Fax (1003), Email (1004), Meeting (1005), Note (1006), Letter (1007) — mapped to industry-standardtype_idinteger codesActivityStatusenum — Scheduled (2001), Completed (2002), Cancelled (2003), Held (2004)ActivityPriorityenum — Low (0), Normal (1), High (2)TimeStatusenum — Free (0), Busy (1) for calendar blocking- Activities list page with type filter chips (Task/Call/Meeting/Email/Note), status filter chips (Scheduled/Completed/Cancelled/Held), per-type and per-status counts
- Activity detail page with badges (type, status, priority), details panel, regarding link, participants list, quick actions (Complete, Edit, Delete)
- Activity create and edit forms — 2-column layout with basic info, classification, schedule (left), assignment and entity picker (right)
- Member activities tab — scoped data table with "New Activity" button pre-filling regarding_type=Member
- Catalogue item activities tab — scoped data table with "New Activity" button pre-filling regarding_type=Catalogue item
- Stock level activities tab — scoped data table with "New Activity" button pre-filling regarding_type=StockLevel
- Complete activity action — sets status to Completed,
completed=true, fires audit event - Activity API endpoints — full CRUD plus
POST /activities/{id}/complete, Ransack filtering, custom view support - industry-standard API responses —
type_id,status_id,activity_type_name,activity_status_name,time_status_name,participantsarray, nestedregardingandownerobjects - Activity permissions — access, view, create, edit, delete, complete — assigned to Admin, Operations Manager, Sales, and Read Only roles
- Activity column registry for custom views with 11 columns and 7 defaults
- Custom views — All Activities, Scheduled Activities, Completed Activities
- compatibility type mapping via
Activity::resolveRegardingType()andActivity::shortRegardingType()— stores full class names in DB, exposes short names (Member, Catalogue item, StockLevel) in API responses
Accessories
- Accessory model linking products to other products with quantity
- Accessories tab on catalogue item pages
- Accessories API endpoints — index, store, destroy nested under products
Navigation & Search
- Activities mega menu link under CRM > Engagement with active state
- catalogue item groups mega menu link under Resources with active state
- Command palette — Activities, catalogue item groups, Stock Levels navigation commands; New Activity, New Catalogue item, New catalogue item group create commands
- Global search — activities searchable by subject (permission-gated), results displayed in command palette with calendar icon
- Search results for catalogue items, stock levels, and catalogue item groups in command palette
Schema & Services
- Schema definitions registered on 14 Phase 1 models (Address, Country, Currency, ExchangeRate, Email, Phone, Link, Attachment, User, ActionLog, Webhook, CustomView, TaxRate, TaxRule)
- Two-tier caching on SchemaRegistry — L1 in-memory + L2 Redis with tag-based invalidation
- Schema Discovery API —
GET /api/v1/schemaandGET /api/v1/schema/{model}for introspecting field metadata - VisibilityRuleEvaluator — 11 operators (equals, not_equals, contains, in, gt, lt, etc.) with AND logic
- AutoNumberService — atomic increment, preview, and reset for custom field auto-numbering
- Formatter::money() and moneyDecimal() — brick/money string-based formatting without float conversion
- EnforceSessionTimeout middleware — settings-driven idle session expiry
Security
- SecuritySettings enforcement for login rate limiting (max_login_attempts, lockout_duration)
- FileService hardened — S3 path changed to entity-organised structure, scan_status default changed to pending, Storage::put() failure checks added
- Icon thumbnail size corrected from 400px to 150px
Changed
- Member activities tab replaced placeholder with scoped data table and action buttons
- Custom view description column added to custom_views table
- Attachment scan_status default changed from
cleantopending - AppServiceProvider cleanup — removed unused Relation import after morph map evaluation
- Search controller expanded to cover catalogue items, stock levels, catalogue item groups, and activities alongside members
- Command palette expanded from 22 to 30+ static commands with catalogue item, activity, and catalogue item group entries
Quality
- 227 new/modified files, 17,200+ lines of code added
- 130+ new tests across enums, models, policies, actions, API controllers, column registries, Livewire components, and search
- PHPStan level 6 — zero errors across all new code
- Pint formatting enforced on all changes
- Documentation pages for Activities (platform + API)
0.2.0 — People & Places
Added
Accounts Module
- Members module — universal entity for contacts, organisations, venues, and users
- Members list page with search, type filter chips, column sorting, column filtering, bulk selection, and archive/restore
- Member detail page — CRM-style 3-column layout with left sidebar (avatar, quick actions, key contacts, account details), center content (stat cards with sparklines, AI recommendations, activity timeline), and right sidebar (customer health score, health factors)
- Member create and edit forms — 2-column layout with membership type, status, description, locale, currency, tax class, and conditional org/contact sections
- Contact detail management — addresses, emails, phones, and links with primary flags and type classification
- Member relationships — link contacts to organisations with relationship type labels
- Member archive and restore — replace delete with soft-archive, restore from archived view, archive filter chips (Active/Archived/All)
- Member merge — select two same-type members, side-by-side comparison modal, migrates all polymorphic relations (addresses, emails, phones, links, attachments), relationships, custom fields, and memberships to primary; archives secondary
- Member profile icon in page header — visible on all member pages, white box with border and shadow, initials fallback
- Phone country code —
country_codecolumn on phones table, flag emoji picker with searchable country dropdown and dial code display, international format on information page - Searchable country combobox on address form — replaces plain
<select>withx-signals.comboboxfor 246 countries - what3words geocoding on address form — forward lookup (three words to coordinates), Nominatim geocode from address fields, interactive Leaflet.js map with draggable marker
- Command palette with live member search — triggered by
/orCmd+K, 22 static commands plus live search with type badges and status indicators - Keyboard shortcuts on member pages —
eto edit,nto open New dropdown - Member API endpoints — full CRUD for members, addresses, emails, phones, links, and relationships
- industry-standard member API responses — active flag, membership object, type-specific fields, icon object, child/parent members
- Auto-create User-type member on user creation via InviteUser action
- BackfillUserMembers command for existing users
Custom Fields
- Custom field groups — create, edit, reorder, and delete groups that organise custom fields
- Custom fields admin — 16 field types (Text, TextArea, Integer, Decimal, Boolean, Date, DateTime, Time, Select, MultiSelect, URL, Email, Phone, Colour, Currency, Percentage, RichText) with validation rules, default values, and list-backed dropdowns
- Custom field values on member detail pages, grouped by field group
- Custom field enforcement — default values applied on creation, is_required validated before persistence, is_searchable and is_active enforced in queries
- Custom field multi-value support for multi-select fields
- Auto-number sequences for custom field auto-numbering
- Default
custom_fieldsin member API responses — industry-standard, no explicit?include=customFieldValuesrequired - Custom field API endpoints — CRUD for groups and definitions
Custom Views
- Custom views system — saved list configs with columns, filters, sort order, and visibility levels (personal, shared, role-restricted)
- View builder Livewire component — create and edit custom views with column picker, filter builder, and sort configuration
- Column registry — schema-driven column definitions for DataTable, custom view validation, and API field selection
- DataTable custom views integration — view selector dropdown, filter/sort application from saved views
- Custom views API endpoints — full CRUD
Multi-Currency
- Currencies table (ISO 4217) with CurrencyService for conversion
- Exchange rates with effective dates — create, update, delete via admin and API
- Currency and exchange rate API endpoints
- Currency seeder with ISO 4217 data
File Attachments
- Polymorphic
attachmentstable with S3 signed URLs and virus scanning support - File service — S3/public disk abstraction with signed URL generation, icon upload, and thumbnail creation
- Icon upload Livewire component
- File upload modal on member pages
- Attachment API endpoints
Reference Data & Lists
- Lists (reference data) — create and manage named lists with hierarchical values, system/non-system flags, and active/inactive status
- Built-in system lists seeded on install: AddressType, EmailType, PhoneType, LinkType
- Additional list categories: Lawful Basis Type, Location Type, Rating, Invoice Term, Locale, Currency
- Countries admin page — browse, search, and toggle active status
- List API endpoints — CRUD for list names and list values
- Countries API endpoints — read-only index and show
Tax
- TaxCalculator service — resolves tax rules by organisation + catalogue item tax class matrix with fallback to defaults, bcmath precision, currency-aware decimal formatting
- Tax classes — catalogue item and organisation tax classifications with default designation
- Tax rate and tax rule API endpoints — full CRUD with Ransack filtering
- Tax rate and tax rule admin pages — create, edit, delete with priority ordering
- Tax class API endpoints — CRUD for catalogue item and organisation tax classes
Authorization & Security
- Model policies for 10 domain models — Member, Store, CustomField, CustomFieldGroup, ListName, OrganisationTaxClass, ProductTaxClass, Webhook, ActionLog, EmailTemplate
- RoleLevel enum — hierarchical role ordering for authorization comparisons
- Policy traits —
AuthorizesByPermissionandChecksStoreAccessextracted from 8 policies - Store scoping via
StoreScopeglobal scope andHasStoreScopingtrait — request-scoped via Context facade, Octane-safe - Cost visibility trait —
HasCostVisibilitywith fail-closed design, explicit user parameter for queue contexts - Blade permission directives —
@area,@action,@costsfor template-level authorization
Schema & Query Engine
- Field Registry and Schema Engine —
HasSchemacontract,SchemaBuilder,SchemaRegistryfor unified field metadata across core, computed, and custom fields - Ransack filter enhancements —
matches(regex),start,end,present,blankpredicates; relationship filtering; custom field EAV filtering viacf.prefix; per-token rate limits - Reusable
applyIncludes()in FiltersQueries trait with$defaultIncludesand$allowedIncludesproperties
Admin & Settings
- Admin Integrations settings page — encrypted API key storage for what3words, Google Maps, and future third-party services
- Feature profiles — Full, Lite, Warehouse, Services presets for module configuration
- Setup wizard improvements — infrastructure checks, branding step, module selection, feature profile presets
- Admin seeder management page — run seeders from the admin panel
- Getting started checklist on dashboard — tracks setup completion progress
Infrastructure
- Reusable DataTable Livewire component — configurable columns, sorting, filtering, pagination, row selection with shift-click range, bulk actions, and event-driven refresh
- CustomFieldCopier service — copies custom field values between entities when field name + type match
- Permission registry
validate()method for reusable permission validation - Action log export job — async CSV export with date validation, failure notifications, and retry configuration
- Social promo pages — LinkedIn (1200x627) and Instagram (1080x1080) format
Quality
- PHPStan upgraded from level 5 to level 6 — 168 generic type annotation fixes, zero errors across 618 files
- 7 new migrations: currencies, exchange rates, attachments, custom views, custom view roles, user view preferences, phone country code
- 100+ new tests across actions, services, Livewire components, DTOs, and pre-existing coverage gaps
- Documentation pages for Members, Custom Fields, Lists, Tax Classes, Countries, Currencies, Exchange Rates, Attachments, and Custom Views
Changed
- Member show page redesigned as 3-column CRM-style layout with stat cards, AI recommendations, and activity timeline
- Member form redesigned as 2-column layout with conditional org/contact sections and list-value dropdowns
- Members list bulk action changed from delete to archive with confirmation modal
- Row actions changed from delete to archive/restore based on member state
- Admin sidebar expanded with Data section, Tax section, and Integrations under Preferences
- Demo data seeder updated with sample members, contact details, custom fields, lists, and tax classes
- RansackFilter uses grammar-wrapped column quoting for SQL safety
- CustomFieldSerializer uses batch loading for ListOfValues fields (eliminates N+1 queries)
- ExportActionLog validates date filters and notifies users on failure
- AnonymiseMember uses
refresh()instead offresh()for null safety - PermissionRegistry centralises permission validation (removed duplication from CreateRole/UpdateRole)
- UpdateMemberData membership_type changed from
?stringto?MembershipTypeenum - MergeMember action wraps all operations in DB::transaction with orphan cleanup
- ArchiveMember action wrapped in DB::transaction for atomic state changes
- What3WordsService adds HTTP timeouts (10s) and logging on all failure paths
- MergeModal catches exceptions with user-friendly flash messages instead of raw 500 errors
Fixed
- SQL injection vector in RansackFilter
matchespredicate — column names now safely quoted via query grammar - Relation filter column names validated against
[a-z_][a-z0-9_]*pattern to prevent injection - ILIKE wildcard characters escaped in SearchController to prevent pattern injection
- TaxResult decimal accessors now respect per-currency minor unit exponents (JPY, BHD, KWD, etc.)
- TaxCalculator uses pure bcmath chain — eliminated float intermediate that could cause precision loss
- StoreScope replaced static mutable state with request-scoped Context facade (Octane-safe)
- HasCostVisibility throws LogicException when
$costColumnsproperty is missing (fail-closed instead of fail-open) - BackfillUserMembers wrapped in DB::transaction with per-user error handling
- Gate::authorize('members.view') permission check added to SearchController
- RestoreMember guards against restoring non-deleted members
- Migration logs warnings for unresolvable ListOfValues during type code realignment
0.1.0 — Initial Alpha
Added
- Infrastructure install wizard (
signals:install) with interactive configuration for PostgreSQL, Redis, S3, and Reverb - Web-based setup wizard for company details, stores, feature profiles, branding, and admin account creation
- CLI setup command (
signals:setup) as terminal alternative to web wizard - Status command (
signals:status) for checking infrastructure connection health - Livewire authentication system with login, forgot password, email verification, and profile settings
- Two-factor authentication (TOTP) with recovery codes, manageable from Settings → Profile
- Composite dashboard with branded design system and KPI cards
- Documentation system with markdown rendering, full-text search, and three-column layout
- API documentation auto-generated via Scramble at
/docs/api - Demo data seeding (
signals:seed-demo) for evaluation and testing - Landing page with technical blueprint aesthetic
- Non-interactive mode for CI/CD deployments
- System admin panel with settings management (Company, Stores, Branding, Modules)
- User management with invite, deactivate, reactivate, password reset, and ownership transfer
- User edit page with role assignment and authorization checks
- Role management with system roles (Admin, Manager, Operator, Viewer) and custom role creation
- Role edit page with multi-user assignment
- Permissions reference page showing all registered permissions grouped by domain
- Security settings page for password policies, session timeouts, login lockout, and 2FA enforcement
- Email settings page for SMTP configuration with connection testing
- Database seeders admin page showing seeder status with run controls
- Permission and role seeders integrated into default
DatabaseSeeder - Branding colours (primary and accent) applied across the UI via CSS custom properties
- Invitation system with signed URLs and accept-invitation flow
- REST API (v1) with Sanctum bearer token authentication and scoped abilities (
resource:action) - API token management — create, list, and revoke personal access tokens with granular ability scoping
- Ransack-compatible query filtering engine with 18 predicates (
_eq,_cont,_lt,_in, etc.) and sort support - API endpoints for users, roles, settings, action logs, and system health
- Webhook system — register URLs with event subscriptions, HMAC-SHA256 signed delivery, exponential backoff retry
- Webhook management API — create, update, delete, toggle, and view delivery logs
- Automatic webhook dispatch on user, role, and settings changes
- Auto-disable webhooks after 18 consecutive delivery failures
- ForceJsonResponse middleware ensuring all API responses return JSON
- EnsureActiveUser middleware blocking deactivated users from API access
- Rate limiting on API routes (60 requests/minute per user)
- API documentation pages for authentication, webhooks, and overview
Changed
- Moved
symfony/yamlandlivewire/voltto production dependencies for documentation rendering - Updated documentation URL to
docs.signals.rent - Header navigation bar and mobile sidebar use brand primary colour from settings
- Search input and focus states in header use brand colour tokens instead of hardcoded values
- Auth views updated to use signals component library (
s-*classes) - Component library expanded with 17 new components and
s-prefix convention