Objective
- Introduce a company- and/or template-level control over when signed agreements are marked stale (
isStale), so users like SE Region can keep agreements current for report lock without treating charge/service-only edits as invalidating a signature. - Primary focus: a selective inspection triggers mode — stale checks (or stale outcomes) apply when template/legal-relevant inspection facts change likely includes property/address, people, datetime, and always template body/version changes), but not for edits that are only charge/service line items.
- Lower priority for this task: an additional mode where staleness tracks template version only (inspection edits never drive stale via the worker). Worth documenting as a follow-up if the same setting enum leaves room, but not the main deliverable.
- Preserve today’s default for companies/templates that do not opt in (no surprise behavior change).
Background
- SE (and similar operators) write agreements so that service selections are not meant to void the deal; today the product still marks agreements outdated whenever regenerated resolved text would differ from
signedContentHash, which commonly happens after charges change because stale checks run from inspection and charge streams. - There is no “SE region” branch in code today — behavior is global for Attik-created jobs (
jobCreationType === 'attik'in the stale worker). The ask is a configurable policy, not a hard-coded region exception. - How it works now (symptoms):
queueAgreementStaleCheckdebounces ~30s thenagreementStaleWorkerrecomputes merged content and flipsisStalewhen the hash diverges. Report unlock usesagreementLockSync: only non-stale active agreements count as signed for lock purposes, so all active rows stale leaves reports locked even if something is still “signed” on paper. - Transcript case: agreement signed → services changed → replacement signed → prior agreement archived → client/report access still wrong. That can align with stale-only lock math or timing; fixing charge-driven staleness for opted-in templates may reduce incidence, but any remaining archive/replace edge should be validated with repro data after policy work.
- Original context and team thread: Message from ryan — Slack.
Scope
Backend
- Stale queue sources: charge lifecycle in
src/events/streamHandlers/chargeStream.ts(insert/update/delete paths that callqueueAgreementStaleCheck), and inspection updates insrc/events/streamHandlers/inspectionStream.ts(e.g. datetime reschedule,charges/charges.*, people, address/property — the latter two are already gated so they do not double-queue whenhasChargesChangeis true). Policy work will need a clear rule for which paths still enqueue a stale check under an opted-in template/company. - Stale computation:
src/events/bullmq/agreementStaleWorker.ts(queueAgreementStaleCheck,processStaleCheck, merge +signedContentHash/computeSignedContentHashflow) and helpers such asmergeAgreementBlocksToContentandresolveAgreementTextForStaleCheckimported there — today the worker compares full resolved text; selective behavior may mean skipping the job, no-oping when the only pending change class is charge-only, or narrowing what invalidates, depending on the approach the implementer chooses. - Persistence: agreement shape and staleness fields live in
src/models/agreementSchema.ts(isStale,staleAt,signedContentHash, template link via_agreementTemplateId). Template documents usesrc/models/agreementTemplateSchema.ts(timestamps exist for versioning-style signals; no dedicated “version” field beyond normal doc evolution — decision needed on how to represent policy and template identity). - Lock and inspection sync:
src/util/functions/agreements/agreementLockSync.tsencodes the non-stale-only signed rule used bysrc/util/functions/inspection/checkReportAccess.tsand bysrc/events/streamHandlers/agreementStream.tswhen syncinginspection.agreement.signed/present. If product changes when rows count as stale vs signed, keep portal and API behavior consistent with those rules. - Tests: existing coverage touches this area in
tests/unit/agreementLockSync.test.tsandtests/integration/lockReport.test.ts— extend or add cases when behavior flags change.
Frontend
- Portal report lock logic is intended to mirror backend:
attik-frontend/src/util/functions/report/computeAgreementSignedForReportLock.ts(see cross-repo note inagreementLockSync.ts). Any change to what counts as signed for lock must stay aligned with backend and be regression-tested in the portal paths that consume the same semantics.
Mobile
- No specific files were audited for this story; confirm if any mobile client duplicates agreement lock rules before shipping.
References
- Message from ryan — Slack
- Backend stale pipeline:
src/events/bullmq/agreementStaleWorker.ts,src/events/streamHandlers/inspectionStream.ts,src/events/streamHandlers/chargeStream.ts - Lock math:
src/util/functions/agreements/agreementLockSync.ts,src/util/functions/inspection/checkReportAccess.ts,src/events/streamHandlers/agreementStream.ts - Portal parity:
attik-frontend/src/util/functions/report/computeAgreementSignedForReportLock.ts