Skip to content

Document Lifecycle — Improvement & Growth Plan

Package: capell-app/document-lifecycle · Kind: package · Tier: premium · Product group: Capell Operations · Bundle: operations · Status: Draft

Document Lifecycle is an admin/database package that turns “controlled documents” (terms, privacy, policy, operational docs) into evidence-grade records: a registry (Document), immutable versioned publications with SHA-256 content hashes (DocumentPublication), and acceptance records that extend the existing legal_acceptances table (DocumentAcceptance). Behaviour lives entirely in Actions (RegisterDocumentAction, PublishDocumentAction, PublishDocumentFromPublishingRevisionAction, ResolveLatestDocumentPublicationAction, ComputeDocumentContentHashAction, RecordDocumentAcceptanceAction, plus status/health helpers); the admin surface (DocumentResource + two read-only relation managers) can register documents, publish manual versions, archive/restore records, and record authenticated-admin acceptances. A PublishingRevision::created listener auto-publishes when a registered document’s underlying model is published in Publishing Studio. Requires capell-app/core, capell-app/admin, capell-app/publishing-studio; supports capell-app/customer-portal via a portal self-service feed (DocumentLifecyclePortalSelfServiceItemProvider). Current marketplace summary: “Version-pinned acceptance evidence for controlled documents in Capell: register documents, publish hashed versions from admin or Publishing Studio, and record who accepted which version and when.” Screenshots: capell.json now declares the extension card plus 4 committed admin PNG captures from docs/screenshots.json.

  • Done/Shipped: model factories are real. DocumentFactory, DocumentPublicationFactory, and DocumentAcceptanceFactory exist under database/factories, model factory() methods resolve them, and coverage exercises state helpers such as draft, active, archived, metadata, document, fromRevision, acceptor, subject, and forPublication. — database/factories/*, tests/Feature/DocumentLifecycleFactoryTest.php
  • Done/Shipped: health check has a real diagnostic report. DocumentLifecycleHealthCheck::report() delegates to BuildDocumentLifecycleHealthReportAction, which verifies the three required tables, legal_acceptances extension columns, protected audit-table registration, morph map aliases, and the Publishing Studio revision listener. Coverage locks both the passing report and a missing-table failure. — src/Health/DocumentLifecycleHealthCheck.php, src/Actions/BuildDocumentLifecycleHealthReportAction.php, tests/Feature/DocumentLifecycleHealthReportTest.php
  • Done/Shipped: document status labels and badge colours are enum-driven. DocumentStatusEnum implements Filament HasLabel and HasColor, DocumentResource uses the enum for Select options and badge colours, and coverage verifies translated labels plus gray/success/warning colours. — src/Enums/DocumentStatusEnum.php, src/Filament/Resources/Documents/DocumentResource.php, tests/Feature/DocumentLifecycleStatusActionTest.php
  • Done/Shipped: acceptance evidence is request-context-pure. RecordDocumentAcceptanceAction accepts explicit ipAddress and userAgent parameters and hashes those values independently of the live request; when callers omit them, the Action does not silently read request state. — src/Actions/RecordDocumentAcceptanceAction.php, tests/Feature/RecordDocumentAcceptanceTest.php
  • Done/Shipped: legal_acceptances rollback only reverses package-owned additions. The migration now tracks whether it added the publication/hash columns and package-named indexes, guards drops with Schema::hasIndex(...), and preserves legacy-owned columns/indexes with matching or differently named indexes. — database/migrations/2026_05_10_190868_03_extend_legal_acceptances_for_document_lifecycle.php, tests/Feature/ExtendLegalAcceptancesMigrationTest.php
  • Done/Shipped: DocumentAcceptance mass-assignment is explicit. The evidence table model now declares a field-level $fillable list instead of $guarded = [], matching the sibling models and keeping writes constrained even outside Actions. — src/Models/DocumentAcceptance.php
  • Index Document.documentable for reverse lookups used by the listener. PublishDocumentFromPublishingRevisionAction::documentForRevision() queries whereIn('documentable_type', ...)->whereIn('documentable_id', ...). The migration indexes documentable via nullableMorphs(...idx) (good), but the hot path also ->first() with no ordering — fine, yet confirm the composite type+id morph index is actually hit (it is, via the generated index). No change needed if verified; otherwise add index(['documentable_type','documentable_id']). — database/migrations/2026_05_10_190868_01_create_document_lifecycle_documents_table.php:22S
  • Document the auto-publish coupling. The PublishingRevision::created global listener fires PublishDocumentFromPublishingRevisionAction on every revision row app-wide and early-returns when no registered Document matches. That’s correct but invisible; one extra query per revision create. Note it in docs/overview.md and consider a config flag to disable, for hosts that don’t use auto-publish. — src/Providers/DocumentLifecycleServiceProvider.php:146-153S

Mapped to capabilities[]: document-lifecycle, document-lifecycle-admin, document-lifecycle-acceptance, document-lifecycle-customer-portal-document-feed.

  • Admin lifecycle actions (closed for the feasible Now slice). The admin now uses Action-backed create, publish-version, archive, restore, and record-authenticated-admin-acceptance flows, all gated through the existing DocumentPolicy update/create checks and backed by package Actions rather than inline lifecycle writes. Void acceptance, bulk acceptance operations, and richer acceptor targeting remain part of the export/re-acceptance work below rather than this completed row. — ties to document-lifecycle-admin.
  • Done/Shipped: retention / expiry policy. Controlled documents now have review_due_at and expires_at timestamps, the admin resource exposes both dates, and the package registers/schedules capell:document-lifecycle:archive-expired daily through ArchiveExpiredDocumentsAction so active expired documents automatically move to Archived. Portal reminder surfacing remains optional future UX polish.
  • Done/Shipped: re-acceptance enforcement and outstanding report foundation. RequiresDocumentReAcceptanceAction answers whether a subject is missing the latest publication/hash, and the acceptances relation manager now exports an outstanding-acceptances CSV grouped by the latest known subject acceptance. Reminder scheduling remains Later depth.
  • Done/Shipped: acceptance evidence CSV export. BuildDocumentAcceptanceEvidenceCsvAction exports a document acceptance ledger as CSV, and the acceptances relation manager exposes an Export evidence CSV header action with an optional publication/version selector. Bulk publish/archive and PDF certificates remain separate Later depth.
  • Done/Shipped: signed JSON acceptance certificate. BuildDocumentAcceptanceCertificateAction bundles the acceptance, document, publication hash, acceptor/subject, timestamp, IP/user-agent hashes, legal bundle values, and metadata into a JSON payload signed with an HMAC-SHA256 signature derived from the host app key. The acceptances relation manager exposes a row-level Download signed JSON action for exportable proof bundles. PDF certificates remain optional future polish rather than a remaining roadmap requirement.
  • Done/Shipped: public frontend surface is reconciled. The package manifest intentionally declares only admin and console surfaces; the Customer Portal self-service feed remains an authenticated supported integration, not package-owned anonymous public output. Public consent widgets can still be built by host apps using package Actions.
  • Done/Shipped: diagnostics health report (see §2). DocumentLifecycleHealthCheck::report() now exposes a real install/registration report for Diagnostics consumers.
  • Done/Shipped: per-version diff / content snapshot. New publications now store a normalized content_snapshot and content_snapshot_format in publication metadata, including Publishing Studio after_payload snapshots. BuildDocumentPublicationDiffAction exports a JSON diff against the previous publication, and the publications relation manager exposes a Download diff JSON row action. Existing historical publications without snapshots honestly report snapshot_available: false in the export.
  • Done/Shipped: manifest frontend surface is reconciled. surfaces is now ["admin","console"], providers.frontend is empty, and overview docs state that the Customer Portal feed is authenticated self-service data rather than package-owned public frontend output. — capell.json, docs/overview.md, tests/Feature/DocumentLifecycleManifestTest.php
  • Done/Shipped: critical health check asserts install and wiring state. DocumentLifecycleHealthCheck::report() returns a typed report for table, column, protected-table, morph-map, and revision-listener wiring; package tests cover success and failure cases. — src/Health/DocumentLifecycleHealthCheck.php, src/Actions/BuildDocumentLifecycleHealthReportAction.php, tests/Feature/DocumentLifecycleHealthReportTest.php
  • Done/Shipped: factories resolve from models. database/factories contains factories for all three models and tests prove Document::factory(), DocumentPublication::factory(), and DocumentAcceptance::factory() resolve correctly. — composer.json, database/factories/*, tests/Feature/DocumentLifecycleFactoryTest.php
  • Done/Shipped: off-request acceptance evidence can be explicit. Queue/console callers can pass ipAddress and userAgent; the Action no longer reaches into the current request behind the caller’s back. — src/Actions/RecordDocumentAcceptanceAction.php, tests/Feature/RecordDocumentAcceptanceTest.php
  • Done/Shipped: asymmetric migration rollback is fixed. The rollback path now guards and ownership-checks package-added columns/indexes, including legacy pre-existing table cases. — migrations/...03_extend_legal_acceptances...php, tests/Feature/ExtendLegalAcceptancesMigrationTest.php
  • PHP constraint drift. composer.json requires php: ^8.3 while the Capell baseline is PHP 8.4. Harmless (broader floor) but inconsistent with siblings; confirm intentional. — composer.json
  • Done/Shipped: portal feed is scoped to the current portal account. The Customer Portal self-service test creates a second acceptance for another portal account id and verifies only the current account’s document item is returned. The feed still exposes acceptance/publication ids to the owner of the record, which is acceptable for authenticated account history. — src/Support/CustomerPortal/DocumentLifecyclePortalSelfServiceItemProvider.php, tests/Feature/CustomerPortalDocumentFeedTest.php
  • Done/Shipped: admin query budget is defended. The admin index now has regression coverage that renders ListDocuments with publication counts and asserts select queries stay within performance.adminQueryBudget. — capell.json (performance), tests/Feature/DocumentLifecycleAdminTest.php
  • Done/Shipped: locale coverage extends beyond English. resources/lang/es/navigation.php mirrors the package navigation/action/message keys so admin and portal strings have a second bundled locale. Portal feed and admin strings remain translation-keyed.
  • Test gaps (covered vs not): Covered — publish/hash/immutability (DocumentPublicationTest), revision listener + documentable repointing, acceptance recording + config fallback and explicit IP/UA hashing (RecordDocumentAcceptanceTest), portal feed scoping + manifest (CustomerPortalDocumentFeedTest), admin visibility/forbidden/permitted/query budget (DocumentLifecycleAdminTest), schema + legacy-table adoption + protected-table/morph registration (DocumentLifecycleSchemaTest), nav + manifest contribution, health report behavior, factory usage, migration rollback, and enum labels/colours. Not covered — bulk evidence export, re-acceptance reporting, retention/expiry command execution, signed evidence JSON download, and content snapshot/diff export.

Critique. The manifest summary and composer description are identical-ish and feature-list-flat (“registry, publication metadata, hashes, and acceptance evidence”). They state what is stored, not what the buyer gets (defensible compliance evidence). Neither mentions the actual differentiator: tamper-evident, version-pinned proof of which document version a user accepted, wired automatically to Publishing Studio. Composer description is also truncated vs the manifest (drops “for Capell CMS”).

Improved 1-sentence summary:

Tamper-evident proof of which document version was published and who accepted it — controlled documents, immutable hashed versions, and a court-ready acceptance ledger for Capell.

Improved 3–4 sentence description:

Document Lifecycle turns terms, privacy, and policy documents into auditable records: every publication is version-pinned and content-hashed, and every acceptance captures the exact version, hash, actor, and timestamp. It hooks into Publishing Studio so a published revision automatically becomes an immutable document publication — no custom audit layer required. Acceptance evidence surfaces in the admin and (with Customer Portal) in each account’s self-service history, and the tables are registered as protected so the audit trail can’t be casually deleted. Built for compliance-sensitive Capell sites that must answer “which version did this user agree to, and when?”

Screenshot/media status. Manifest declares the extension card plus 4 committed admin captures from docs/screenshots.json (index, edit, publications RM, acceptances RM). Remaining optional media depth is a video/GIF of the auto-publish-on-revision flow, which is the most compelling demo.

Pricing / tier / bundle positioning. premium in the operations bundle, Capell Operations group — appropriate; compliance/governance is a willingness-to-pay feature. Strengthen by cross-selling on dependencies: hard-requires publishing-studio (anchor the “auto-publish” story to it), supports customer-portal (the acceptance-history feed is a portal upsell). Natural Extension-Suite framing: an “Operations / Compliance” suite = Document Lifecycle + Publishing Studio + Diagnostics + Password Policy (already the README “Best Used With” set). The retention/reminder/e-signature features in §3 justify the premium tier and a possible add-on tier.

Differentiators / value props / target buyer. Differentiator: content-hash immutability + automatic version pinning from Publishing Studio + protected audit tables — most CMS “terms acceptance” is an unversioned boolean. Target buyer: ops/compliance/legal owners of regulated or contract-heavy Capell sites (memberships, healthcare, finance, B2B SaaS portals) who need defensible “who accepted what version when” evidence.

Keywords/tags (8–12): document-lifecycle, compliance, audit-trail, terms-of-service, consent-management, content-hash, versioning, legal, governance, acceptance-evidence, customer-portal, publishing.

ItemBucketEffortImpactSection ref
Ship model factories; fix/remove database/factories autoloadDoneSHigh§2, §4
Real health-check report() Action + health testDoneMHigh§2, §3, §4
Reconcile frontend surface: drop it or ship safe consent componentDoneS→LHigh§3, §4
Capture 4 admin screenshots; sync marketplace.screenshots[] to manifestDoneSHigh§1, §5 — closed 2026-06-06: four Capell runner PNG captures are committed and promoted into marketplace media.
Make RecordDocumentAcceptanceAction request-context-pure (ip/UA params)DoneSMed§2, §4
Fix asymmetric legal_acceptances migration down() + rollback testDoneSMed§2, §4
Rewrite manifest summary + composer descriptionDoneSMed§5 — 2026-06-04: replaced court-ready/immutable phrasing with shipped admin, Publishing Studio, and acceptance-evidence wording; manifest test now locks composer/marketplace copy.
Admin lifecycle Actions (Publish / Archive / Record acceptance buttons)DoneMHigh§3
Done/Shipped: Bulk export of acceptance evidence (CSV) per document/version. Evidence: BuildDocumentAcceptanceEvidenceCsvAction, acceptances relation manager header action, and document-lifecycle-evidence-export.DoneMHigh§3
Done/Shipped: Re-acceptance enforcement + outstanding-acceptances report. Evidence: RequiresDocumentReAcceptanceAction, BuildOutstandingDocumentAcceptancesCsvAction, relation manager CSV action, and document-lifecycle-reacceptance-report.DoneMHigh§3
Portal-feed cross-account isolation negative testDoneSMed§4
DocumentStatusEnumHasLabels + badge coloursDoneSMed§2, §4
Retention / expiry + review-due scheduled transitionDoneMHigh§3 — 2026-06-06: added review/expiry timestamps, admin fields/table columns, ArchiveExpiredDocumentsAction, a registered command, and a daily schedule.
Signed acceptance certificate (PDF/JSON proof bundle)DoneLHigh§3 — 2026-06-06: shipped HMAC-signed JSON proof bundles through BuildDocumentAcceptanceCertificateAction and a row-level admin download action. PDF certificate rendering remains optional polish.
Per-version content snapshot + diff viewDoneLMed§3 — 2026-06-06: new publications store normalized snapshots in metadata, and admins can download JSON diffs between publication versions.
Locale coverage beyond en; admin query-budget regression testDoneSLow§4 — 2026-06-06: added bundled Spanish translations and a manifest-budgeted admin index query regression test.

Row movement in the 2026-06-04 follow-up: Admin lifecycle Actions (Publish / Archive / Record acceptance buttons) and Fix asymmetric legal_acceptances migration down() + rollback test moved to Done. The remaining screenshot captures, exports, re-acceptance, retention/expiry, signed evidence, and content-diff rows stay outside the closed Now slice.