Skip to content

feat(#1466 P3b): compliance-grade insurance resolution evidence#1472

Merged
Systemsaholic merged 1 commit into
mainfrom
feature/issue-1466-p3b-insurance-evidence
Jul 5, 2026
Merged

feat(#1466 P3b): compliance-grade insurance resolution evidence#1472
Systemsaholic merged 1 commit into
mainfrom
feature/issue-1466-p3b-insurance-evidence

Conversation

@Systemsaholic

@Systemsaholic Systemsaholic commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Automation Gate epic #1466 — slice P3b (compliance-grade resolution evidence)

refs #1466. Advances #837 P2 (documentation) + P3 (declinations). Captures how and by whom a traveler's insurance resolution was recorded, closing the "rubber-stamp" gap where declined/has_own_insurance/selected_package could be set with no consent record or offered-coverage snapshot. Additive/forward-only; ships live on merge. Does not rebuild #837 catalog/picker (shipped). STRICT error mode (compliance never best-effort); the money boundary is untouched.

Migration (20260705120000_1466_insurance_resolution_evidence)

  • New enum insurance_resolution_source (advisor_manual | client_esign).
  • 4 additive columns on trip_traveler_insurance: resolution_source (NOT NULL DEFAULT advisor_manual), consent_method varchar(50), form_token_id uuid FK→form_tokens (ON DELETE SET NULL), offered_coverages_snapshot jsonb.
  • The ~93 existing prod rows keep the default with null evidence — a documented pre-P3b audit gap, NOT retro-enforced (flag to Al). Registered in _journal.json (monotonic; migrations:validate-journal passes).

Backend

  • insurance.servicevalidateStatusRequirements split: field checks stay; consent is a dedicated assertConsentMethod. A resolved status recorded via the advisor path requires a valid consent_method (400 otherwise); pending is exempt. create/updateTravelerInsurance force resolution_source='advisor_manual' server-side and strip all evidence columns from the DTO (mirrors the activity_id-strip — spoofed resolution_source/consent_method can't forge provenance), and freeze offered_coverages_snapshot from active packages. A client_esign record is preserved (no consent demanded, evidence intact) for evidence-neutral edits (notes/premium/policy #); a material change (status or a status-defining field — package, external policy/provider/coverage, decline reason) is an advisor override → requires consent, flips to advisor_manual, unlinks the token, refreshes the snapshot. formatTravelerInsurance surfaces all 4 fields.
  • forms.service handleInsuranceWaiverSubmission — stamps resolution_source='client_esign', form_token_id, offered_coverages_snapshot, and clears stale advisor consent_method on every write (insert+update × purchase+decline) inside the existing atomic-claim tx. No new send.
  • insurance-offer-snapshot.mapper — extracted verbatim from queueProposalEmails' contextData snapshot (§7 colocated spec) so the advisor and client-waiver paths record an identical offer shape.
  • insurance.queries declines report surfaces resolution_source + consent_method.

Frontend (trip-insurance.tsx)

  • TravelerInsuranceDialog + AssignPackageDialog: required "Confirmed via" selector shown for a resolved status on a non-e-sign record — and, mirroring the server rule, also for a material override of an e-sign record (so the override isn't silently 400'd). Save gated until chosen; threaded into the assign API calls.
  • Status table + dialog: "View signed waiver" link when form_token_id is present (reuses GET /trips/:id/forms/:formId/pdf — no new endpoint).

Shared-types

InsuranceResolutionSource, AdvisorConsentMethod (+ADVISOR_CONSENT_METHODS = verbal_phone/email_confirmation/in_person_signature, excludes client-self-service), InsuranceOfferSnapshotItem; 4 read-DTO fields + consentMethod on Create/Update.

Tests (all foreground, green)

  • insurance-offer-snapshot.mapper.spec (field mapping + nullable coalesce).
  • insurance.service.evidence.spec (13): consent required per resolved status + exempt for pending; invalid consent rejected; resolution_source always advisor_manual even if spoofed; snapshot frozen; client_esign preserve vs material override (incl. package A→B reassignment); neutral-PATCH consent-injection stripped.
  • forms.service.spec extended: client_esign + form_token_id + snapshot + stale-consent-cleared on both insert and update branches inside the tx.
  • api + admin typecheck clean; money mirror/link/commission paths untouched.

Review

Codex (gpt-5.5, xhigh) — reviewed iteratively: r1 BLOCK (e-sign evidence preserved across overrides; stale advisor consent on client update) → fixed; r2 BLOCK (body-mutable consent_method on neutral PATCH) → fixed; APPROVE on ca801212 (final HEAD).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Trip insurance now tracks how consent was captured and shows signed waiver links when available.
    • Advisors can record required consent when resolving traveler insurance statuses.
  • Bug Fixes

    • Improved handling of insurance updates so existing consent evidence is preserved when changes don’t affect the resolution.
    • Insurance decline reporting now includes consent and resolution source details for clearer review.

Advances #837 P2 (documentation) + P3 (declinations). Captures HOW and by WHOM
a traveler's insurance resolution was recorded, closing the "rubber-stamp"
gap where declined/has_own/selected could be set with no consent or offered-
coverage record. Additive/forward-only; ships live on merge.

Migration (20260705120000): new enum insurance_resolution_source
('advisor_manual','client_esign') + 4 additive columns on trip_traveler_insurance
(resolution_source NOT NULL DEFAULT 'advisor_manual', consent_method,
form_token_id FK→form_tokens ON DELETE SET NULL, offered_coverages_snapshot jsonb).
The ~93 existing prod rows keep the default with null evidence — a documented
pre-P3b audit gap, NOT retro-enforced (flag to Al).

Backend:
- insurance.service: validateStatusRequirements now REQUIRES a valid consent_method
  when an advisor records a resolved status (400 otherwise); pending is exempt.
  create/updateTravelerInsurance FORCE resolution_source='advisor_manual'
  server-side (mirrors the activity_id-strip; spoofed values stripped) and freeze
  offered_coverages_snapshot from active packages. A client_esign record is
  preserved (no consent demanded, evidence not clobbered) when the advisor edits
  without changing the resolved status; an override flips it to advisor_manual with
  fresh consent and unlinks the token. formatTravelerInsurance surfaces all 4 fields.
- forms.service handleInsuranceWaiverSubmission: stamps resolution_source='client_esign',
  form_token_id, offered_coverages_snapshot on every write (insert+update, both
  actions) inside the existing atomic-claim tx. No new send.
- Shared insurance-offer-snapshot.mapper (extracted verbatim from
  queueProposalEmails' contextData snapshot, §7 colocated spec) so the advisor and
  waiver paths record an identical offer shape.
- insurance.queries declines report surfaces resolution_source + consent_method.

Frontend (trip-insurance.tsx):
- TravelerInsuranceDialog + AssignPackageDialog: required "Confirmed via" selector
  shown for a resolved status on a non-e-sign record; Save gated until chosen.
- Status table + dialog: "View signed waiver" link when form_token_id is present
  (reuses GET /trips/:id/forms/:formId/pdf — no new endpoint).

Shared-types: InsuranceResolutionSource, AdvisorConsentMethod (+ADVISOR_CONSENT_METHODS,
excludes client-self-service), InsuranceOfferSnapshotItem; 4 read-DTO fields +
consentMethod on Create/Update.

STRICT error mode (compliance never best-effort). Money boundary untouched
(commissionTotalCents/activityId/mirror/link paths unchanged). No TripsService touch.

Tests (all foreground): mapper spec; service evidence spec (consent required per
resolved status + exempt for pending; resolution_source always advisor_manual even
if spoofed; snapshot frozen; client_esign preserve vs override); forms spec extended
(client_esign+form_token_id+snapshot on both branches inside the tx).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tailfire-client Ready Ready Preview, Comment Jul 5, 2026 2:33pm
tailfire-ota Ready Ready Preview, Comment Jul 5, 2026 2:33pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds compliance-grade resolution evidence to trip traveler insurance. A new migration and schema introduce resolution source, consent method, form token, and offer snapshot columns. API services validate/store evidence on create, update, and client e-sign waiver submission; reporting queries expose the fields; admin UI captures consent and shows signed-waiver links.

Changes

Insurance Resolution Evidence

Layer / File(s) Summary
Schema, migration, and shared types
packages/database/src/migrations/20260705120000_1466_insurance_resolution_evidence.sql, packages/database/src/schema/insurance.schema.ts, packages/database/src/index.ts, packages/database/src/migrations/meta/_journal.json, packages/shared-types/src/api/insurance.types.ts
Adds insurance_resolution_source enum and new evidence columns (resolution_source, consent_method, form_token_id, offered_coverages_snapshot) to trip_traveler_insurance, exports new types, and extends DTOs with matching fields.
Offer snapshot mapper
apps/api/src/trips/insurance-offer-snapshot.mapper.ts, insurance-offer-snapshot.mapper.spec.ts
New mapPackagesToOfferSnapshot function converts packages into snapshot items, coalescing nullable defaults, with unit tests.
InsuranceService evidence enforcement
apps/api/src/trips/insurance.service.ts, insurance.service.evidence.spec.ts
Validates required consent for resolved statuses, computes offer snapshots, detects material resolution changes, preserves/overrides client_esign evidence, and returns evidence fields in responses.
Client e-sign waiver stamping
apps/api/src/forms/forms.service.ts, forms.service.spec.ts, apps/api/src/trips/insurance-automation.service.ts
Stamps client_esign evidence on insert/update paths during waiver submission and switches token context packages to the centralized mapper.
Reporting evidence exposure
apps/api/src/reporting/queries/insurance.queries.ts
Declines report query selects and maps resolutionSource/consentMethod.
Admin UI consent capture
apps/admin/src/app/trips/[id]/_components/trip-insurance.tsx
Adds consent method selectors, evidence-aware validation, and signed-waiver links for both per-traveler and batch-assignment flows.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Traveler
  participant FormsService
  participant Database
  participant InsuranceService
  participant AdminUI

  Traveler->>FormsService: submit signed waiver (formTokenId)
  FormsService->>FormsService: build esignEvidence (resolutionSource=client_esign, offeredCoveragesSnapshot, consentMethod=null)
  FormsService->>Database: insert/update trip_traveler_insurance with esignEvidence
  Database-->>FormsService: confirm write

  AdminUI->>InsuranceService: update traveler status (advisor override)
  InsuranceService->>InsuranceService: detect material change, require consentMethod
  InsuranceService->>Database: write resolutionSource=advisor_manual, consentMethod, clear formTokenId
  Database-->>InsuranceService: confirm write
  InsuranceService-->>AdminUI: return updated DTO with evidence fields
Loading

Possibly related PRs

  • Systemsaholic/tailfire#577: Both PRs modify the same trip traveler insurance create/update flow in insurance.service.ts and the admin trip-insurance.tsx UI, adding different persisted fields for resolved/selected_package updates.
  • Systemsaholic/tailfire#850: Both PRs modify the same admin UI package assignment dialog code paths in trip-insurance.tsx.

Suggested labels: enhancement, area:api

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed but does not follow the required template or include the TripsService surface checkbox/checklist. Rewrite it to use the repository template: add a ## Summary section, complete the TripsService surface checkbox, and include the checklist items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change to insurance resolution evidence.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-1466-p3b-insurance-evidence

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/admin/src/app/trips/[id]/_components/trip-insurance.tsx

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: ESLint configuration in --config is invalid:

  • Property "" is the wrong type (expected object but got "import config from \"@tailfire/config/eslint/flat-next\";\nexport default config;").

    at ConfigValidator.validateConfigSchema (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2177:19)
    at ConfigArrayFactory._normalizeConfigData (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:19)
    at ConfigArrayFactory._loadConfigData (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2984:21)
    at ConfigArrayFactory.loadFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2850:40)
    at createCLIConfigArray (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3660:35)
    at new CascadingConfigArrayFactory (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3735:29)
    at new CLIEngine (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli-engine/cli-engine.js:617:36)
    at new ESLint (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/eslint/eslint.js:430:27)
    at Object.execute (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli.js:410:24)
    at async main (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/bin/eslint.js:152:22)

apps/api/src/forms/__tests__/forms.service.spec.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: ESLint configuration in --config is invalid:

  • Property "" is the wrong type (expected object but got "import config from \"@tailfire/config/eslint/flat-node\";\nexport default config;").

    at ConfigValidator.validateConfigSchema (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2177:19)
    at ConfigArrayFactory._normalizeConfigData (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:19)
    at ConfigArrayFactory._loadConfigData (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2984:21)
    at ConfigArrayFactory.loadFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2850:40)
    at createCLIConfigArray (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3660:35)
    at new CascadingConfigArrayFactory (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3735:29)
    at new CLIEngine (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli-engine/cli-engine.js:617:36)
    at new ESLint (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/eslint/eslint.js:430:27)
    at Object.execute (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli.js:410:24)
    at async main (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/bin/eslint.js:152:22)

apps/api/src/forms/forms.service.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: ESLint configuration in --config is invalid:

  • Property "" is the wrong type (expected object but got "import config from \"@tailfire/config/eslint/flat-node\";\nexport default config;").

    at ConfigValidator.validateConfigSchema (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2177:19)
    at ConfigArrayFactory._normalizeConfigData (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:19)
    at ConfigArrayFactory._loadConfigData (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2984:21)
    at ConfigArrayFactory.loadFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2850:40)
    at createCLIConfigArray (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3660:35)
    at new CascadingConfigArrayFactory (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3735:29)
    at new CLIEngine (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli-engine/cli-engine.js:617:36)
    at new ESLint (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/eslint/eslint.js:430:27)
    at Object.execute (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli.js:410:24)
    at async main (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/bin/eslint.js:152:22)

  • 6 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Systemsaholic Systemsaholic merged commit 96f613d into main Jul 5, 2026
12 of 19 checks passed
@Systemsaholic Systemsaholic deleted the feature/issue-1466-p3b-insurance-evidence branch July 5, 2026 14:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/database/src/migrations/20260705120000_1466_insurance_resolution_evidence.sql (1)

23-23: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider indexing form_token_id.

form_token_id has ON DELETE SET NULL but no index. When a form_tokens row is deleted, Postgres must scan trip_traveler_insurance to null out matching rows; without an index this is a sequential scan. Low risk today given the small table (~93 rows per the migration comment), but worth adding proactively since this FK will be hit on every form-token cleanup/expiry going forward.

♻️ Optional index addition
 ALTER TABLE trip_traveler_insurance
   ADD COLUMN IF NOT EXISTS resolution_source insurance_resolution_source NOT NULL DEFAULT 'advisor_manual',
   ADD COLUMN IF NOT EXISTS consent_method varchar(50),
   ADD COLUMN IF NOT EXISTS form_token_id uuid REFERENCES form_tokens(id) ON DELETE SET NULL,
   ADD COLUMN IF NOT EXISTS offered_coverages_snapshot jsonb;
+
+CREATE INDEX IF NOT EXISTS idx_trip_traveler_insurance_form_token_id
+  ON trip_traveler_insurance (form_token_id)
+  WHERE form_token_id IS NOT NULL;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/database/src/migrations/20260705120000_1466_insurance_resolution_evidence.sql`
at line 23, Add an index for the new trip_traveler_insurance.form_token_id
foreign key so deletes from form_tokens don’t require a sequential scan when ON
DELETE SET NULL runs. Update the migration that adds form_token_id to also
create the appropriate index in the same change set, using the existing
migration naming style and keeping it aligned with the new column definition.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/forms/forms.service.ts`:
- Around line 571-583: The `esignEvidence` payload in `forms.service.ts` is
persisting `offeredCoveragesSnapshot` directly from `offeredPackages`, which can
carry the old partial token shape. Update the `forms.service.ts` write path to
normalize or validate the snapshot before assigning it, reusing the same shape
produced by `mapPackagesToOfferSnapshot(activePackages)` or equivalent
validation logic. Keep the fix within the client e-sign evidence construction so
every insert/update stores a fully rehydrated compliance snapshot.

In `@apps/api/src/trips/insurance.service.ts`:
- Around line 961-1020: The `updateTravelerInsurance` flow is racing because it
reads the current insurance row, derives
`preserveClientEsign`/`advisorResolutionWrite`, and then writes evidence fields
later without a transaction or row lock. Wrap the read-decision-write sequence
in a transaction and lock the target insurance record first (for example in the
`updateTravelerInsurance` method before computing `materialResolutionChange` and
`effectiveStatus`) so concurrent PATCHes cannot overwrite `resolutionSource`,
`consentMethod`, `offeredCoveragesSnapshot`, or `formTokenId` from stale state.

---

Nitpick comments:
In
`@packages/database/src/migrations/20260705120000_1466_insurance_resolution_evidence.sql`:
- Line 23: Add an index for the new trip_traveler_insurance.form_token_id
foreign key so deletes from form_tokens don’t require a sequential scan when ON
DELETE SET NULL runs. Update the migration that adds form_token_id to also
create the appropriate index in the same change set, using the existing
migration naming style and keeping it aligned with the new column definition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57f8f5a2-d23e-4d09-ac00-dddfee6a122c

📥 Commits

Reviewing files that changed from the base of the PR and between ab97890 and ca80121.

📒 Files selected for processing (14)
  • apps/admin/src/app/trips/[id]/_components/trip-insurance.tsx
  • apps/api/src/forms/__tests__/forms.service.spec.ts
  • apps/api/src/forms/forms.service.ts
  • apps/api/src/reporting/queries/insurance.queries.ts
  • apps/api/src/trips/insurance-automation.service.ts
  • apps/api/src/trips/insurance-offer-snapshot.mapper.spec.ts
  • apps/api/src/trips/insurance-offer-snapshot.mapper.ts
  • apps/api/src/trips/insurance.service.evidence.spec.ts
  • apps/api/src/trips/insurance.service.ts
  • packages/database/src/index.ts
  • packages/database/src/migrations/20260705120000_1466_insurance_resolution_evidence.sql
  • packages/database/src/migrations/meta/_journal.json
  • packages/database/src/schema/insurance.schema.ts
  • packages/shared-types/src/api/insurance.types.ts

Comment on lines +571 to +583
// refs #1466 P3b — this is a CLIENT E-SIGN resolution: stamp the
// compliance evidence on every write (insert AND update, both actions).
// form.id links the signed waiver PDF; offeredPackages freezes what the
// traveler was shown. Inside the existing atomic-claim tx, no new send.
const esignEvidence = {
resolutionSource: 'client_esign' as const,
formTokenId: form.id,
offeredCoveragesSnapshot: offeredPackages,
// Clear any stale advisor consent — this row is now resolved by the
// client's signed waiver, not an out-of-band advisor confirmation.
consentMethod: null,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the waiver submission flow and the types involved
git ls-files 'apps/api/src/forms/*' 'apps/api/src/**/insurance*' 'apps/api/src/**/forms*' 'apps/api/src/**/shared*' | sed 's#^`#FILE` #'

printf '\n--- forms.service.ts around the cited lines ---\n'
sed -n '430,620p' apps/api/src/forms/forms.service.ts

printf '\n--- search for offeredCoveragesSnapshot / InsuranceOfferSnapshotItem / contextData.packages ---\n'
rg -n "offeredCoveragesSnapshot|InsuranceOfferSnapshotItem|contextData\.packages|mapPackagesToOfferSnapshot|packages:" apps/api/src -g '!**/*.map'

printf '\n--- likely schema/type definitions ---\n'
fd -a 'insurance' apps/api/src
fd -a 'snapshot' apps/api/src
fd -a 'waiver' apps/api/src

Repository: Systemsaholic/tailfire

Length of output: 18129


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- insurance-offer-snapshot.mapper.ts ---\n'
sed -n '1,220p' apps/api/src/trips/insurance-offer-snapshot.mapper.ts

printf '\n--- insurance-automation.service.ts around token creation ---\n'
sed -n '280,360p' apps/api/src/trips/insurance-automation.service.ts

printf '\n--- forms.service.spec.ts evidence tests around snapshot payloads ---\n'
sed -n '360,440p' apps/api/src/forms/__tests__/forms.service.spec.ts

printf '\n--- insurance.service.evidence.spec.ts relevant assertions ---\n'
sed -n '1,220p' apps/api/src/trips/insurance.service.evidence.spec.ts

Repository: Systemsaholic/tailfire

Length of output: 16417


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- search snapshot contract definitions ---\n'
rg -n "offered_coverages_snapshot|offeredCoveragesSnapshot|InsuranceOfferSnapshotItem" . -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**'

printf '\n--- database/schema definitions for trip_traveler_insurance ---\n'
fd -a 'trip.*insurance|insurance.*schema|schema.*ts' apps/api packages | sed -n '1,120p'

Repository: Systemsaholic/tailfire

Length of output: 12510


Normalize offeredCoveragesSnapshot before persisting it

apps/api/src/forms/forms.service.ts:571-583 still copies form.contextData.packages directly into evidence. Newly minted tokens use mapPackagesToOfferSnapshot(activePackages), but any older token can still carry the pre-change partial shape and freeze malformed compliance data. Rehydrate or validate the snapshot before writing it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/forms/forms.service.ts` around lines 571 - 583, The
`esignEvidence` payload in `forms.service.ts` is persisting
`offeredCoveragesSnapshot` directly from `offeredPackages`, which can carry the
old partial token shape. Update the `forms.service.ts` write path to normalize
or validate the snapshot before assigning it, reusing the same shape produced by
`mapPackagesToOfferSnapshot(activePackages)` or equivalent validation logic.
Keep the fix within the client e-sign evidence construction so every
insert/update stores a fully rehydrated compliance snapshot.

Comment on lines +961 to +1020
// refs #1466 P3b — a resolution captured by the client waiver path
// (client_esign) already carries its own evidence (signature/IP/signed PDF).
// It is preserved ONLY for evidence-NEUTRAL edits (notes, premium, policy
// number). ANY change to the resolution itself — the status OR a status-defining
// field (package, external policy/provider/coverage, decline reason) — is a
// material advisor override: re-capture consent, flip to advisor_manual, unlink
// the signed waiver, and refresh the offer snapshot.
const differs = (a: unknown, b: unknown) => a !== undefined && a !== b
const materialResolutionChange =
differs(dto.status, existing.status) ||
differs(dto.selectedPackageId, existing.selectedPackageId) ||
differs(dto.externalPolicyNumber, existing.externalPolicyNumber) ||
differs(dto.externalProviderName, existing.externalProviderName) ||
differs(dto.externalCoverageDetails, existing.externalCoverageDetails) ||
differs(dto.declinedReason, existing.declinedReason)
const preserveClientEsign =
existing.resolutionSource === 'client_esign' && !materialResolutionChange
const effectiveStatus = dto.status ?? existing.status
const effectiveResolved = RESOLVED_INSURANCE_STATUSES.has(effectiveStatus)
// This write establishes an advisor-recorded resolution when we're not
// preserving an e-sign record and either the status is (re)set or a resolution
// field materially changed.
const advisorResolutionWrite =
!preserveClientEsign && (dto.status !== undefined || materialResolutionChange)

// Validate status-specific field requirements (only when status is provided,
// to preserve the existing partial-update contract).
if (dto.status !== undefined) {
this.validateStatusRequirements(dto.status, dto)
}
// Consent is required whenever this write records an advisor resolution.
if (advisorResolutionWrite && effectiveResolved) {
this.assertConsentMethod(dto)
}

const updateData: any = {
...dto,
updatedAt: new Date(),
}

// refs #1466 P3b — ALL evidence columns are SERVICE-MANAGED; strip them from the
// incoming body unconditionally (mirror the activity_id-strip pattern) so a
// spoofed { resolutionSource: 'client_esign' } can't forge provenance AND a
// neutral PATCH { consentMethod: 'bogus' } can't inject an unvalidated consent
// value. Consent is re-set ONLY inside the validated advisor-resolution block.
delete updateData.resolutionSource
delete updateData.formTokenId
delete updateData.offeredCoveragesSnapshot
delete updateData.consentMethod

if (advisorResolutionWrite) {
// Advisor is (re)recording the resolution → advisor_manual with fresh
// evidence; unlink any prior signed-waiver token (this is now the advisor's
// decision of record). dto.consentMethod was already validated above.
updateData.resolutionSource = 'advisor_manual'
updateData.consentMethod = effectiveResolved ? (dto.consentMethod ?? null) : null
updateData.offeredCoveragesSnapshot = effectiveResolved ? await this.buildOfferSnapshot(tripId) : null
updateData.formTokenId = null
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant service methods and schema definitions.
rg -n "getTravelerInsuranceById|updatePackage|FOR UPDATE|transaction\(" apps/api/src/trips -S

# Map the insurance service structure first.
ast-grep outline apps/api/src/trips/insurance.service.ts --view expanded

# Read the relevant sections around the reported lines and the updatePackage comparison point.
sed -n '920,1085p' apps/api/src/trips/insurance.service.ts
printf '\n--- updatePackage ---\n'
sed -n '1,260p' apps/api/src/trips/insurance.service.ts | sed -n '/updatePackage/,/^[[:space:]]*}/p'

# Find the read method implementation and any lock usage in the repository.
rg -n "getTravelerInsuranceById|SELECT.*FOR UPDATE|for update|lock.*tripTravelerInsurance|tripTravelerInsurance" apps/api/src -S

# Read the schema for the insurance row to understand the row identity and update targets.
fd -a ".*trips.*schema.*|.*insurance.*schema.*|.*traveler.*insurance.*" apps/api/src

Repository: Systemsaholic/tailfire

Length of output: 30607


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the exact methods involved.
sed -n '858,1068p' apps/api/src/trips/insurance.service.ts

printf '\n--- getTravelerInsuranceById ---\n'
sed -n '858,919p' apps/api/src/trips/insurance.service.ts

printf '\n--- updatePackage ---\n'
sed -n '670,798p' apps/api/src/trips/insurance.service.ts

# Inspect the evidence-focused test file for any expected concurrency or locking behavior.
wc -l apps/api/src/trips/insurance.service.evidence.spec.ts
sed -n '1,220p' apps/api/src/trips/insurance.service.evidence.spec.ts

# Check whether updateTravelerInsurance has any dedicated transactional or race-related test coverage.
rg -n "updateTravelerInsurance|client_esign|advisor_manual|FOR UPDATE|transaction\\(" apps/api/src/trips -S

Repository: Systemsaholic/tailfire

Length of output: 44622


Lock updateTravelerInsurance before deriving evidence fields
The update path reads the row, decides whether to preserve client_esign or switch to advisor_manual, then writes later with no transaction or FOR UPDATE. Concurrent PATCHes can race and overwrite resolutionSource, consentMethod, offeredCoveragesSnapshot, or formTokenId from stale state. Wrap the read + decision + write in a transaction and lock the insurance row first.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/trips/insurance.service.ts` around lines 961 - 1020, The
`updateTravelerInsurance` flow is racing because it reads the current insurance
row, derives `preserveClientEsign`/`advisorResolutionWrite`, and then writes
evidence fields later without a transaction or row lock. Wrap the
read-decision-write sequence in a transaction and lock the target insurance
record first (for example in the `updateTravelerInsurance` method before
computing `materialResolutionChange` and `effectiveStatus`) so concurrent
PATCHes cannot overwrite `resolutionSource`, `consentMethod`,
`offeredCoveragesSnapshot`, or `formTokenId` from stale state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant