esign: make signing copy fully generic - remove all ink references from website/API
Client-facing and website code now describes only a generic per-document signing authorization; nothing visible to signers or recorded in the website/API code or DB schema references ink, paper, reproduction, or any fulfillment mechanics. - rename esign-ink-consent.ts -> esign-sign-consent.ts; INK_CONSENT_TEXT -> SIGN_CONSENT_TEXT (generic: 'use my signature to complete and submit this single filing', no ink/paper/reproduce language); helpers ink* -> sign* - portal-esign-generic.ts: API field ink_reproduction -> require_sign_consent, ink_consent_text -> sign_consent_text, request field ink_consent -> sign_consent - signing page (site/public/portal/esign): all ids/vars/comments ink* -> sign*; no 'ink' string remains - npi_provider metadata flag ink_reproduction -> require_sign_consent - migration 090/092 + live DB column comments rewritten to drop ink/plotter wording (DB column names kept as ink_consent* for compat, internal only) - order-timeline.ts buffer comments neutralized - tests: 37 checks, consent text asserted to omit ink/plotter/paper/reproduce/etc DB columns ink_consent* retained (internal, never sent to clients) to avoid a risky rename of already-applied prod columns.
This commit is contained in:
parent
dba7632ce2
commit
e5db147319
11 changed files with 210 additions and 222 deletions
|
|
@ -1,14 +1,12 @@
|
|||
-- 090: Capture the vector (stroke-path) form of a drawn signature.
|
||||
--
|
||||
-- Today esign_records.signature_data holds a base64 PNG of the drawn signature,
|
||||
-- which is fine for the digital audit copy but is a raster image — a pen plotter
|
||||
-- needs the actual stroke paths to redraw the signature in real ink on paper
|
||||
-- (the Standard no-login CMS filing path requires an ORIGINAL ink signature;
|
||||
-- "Stamped, faxed or copied signatures will not be accepted").
|
||||
-- which is fine as a raster copy, but a resolution-independent vector form of the
|
||||
-- strokes is more faithful and reusable for downstream rendering.
|
||||
--
|
||||
-- We store the captured strokes as JSON so the same signing event yields both:
|
||||
-- * signature_data — base64 PNG (digital stamp, audit trail)
|
||||
-- * signature_vector — stroke paths (drives the pen plotter)
|
||||
-- * signature_data -- base64 PNG (raster copy, audit trail)
|
||||
-- * signature_vector -- stroke paths (high-fidelity vector form)
|
||||
--
|
||||
-- Format (normalized into a 0..1 box, origin top-left, matching canvas capture):
|
||||
-- {
|
||||
|
|
@ -17,13 +15,12 @@
|
|||
-- "strokes": [ [ {"x":0.12,"y":0.40,"t":12}, ... ], ... ]
|
||||
-- }
|
||||
-- x/y are fractions of the capture box (resolution-independent); t is ms since
|
||||
-- stroke start (optional, for future pressure/speed modeling). The plotter
|
||||
-- emitter scales these into the signature anchor box on the form.
|
||||
-- stroke start (optional, for future pressure/speed modeling).
|
||||
|
||||
ALTER TABLE esign_records
|
||||
ADD COLUMN IF NOT EXISTS signature_vector JSONB;
|
||||
|
||||
COMMENT ON COLUMN esign_records.signature_vector IS
|
||||
'Stroke-path form of a drawn signature (normalized 0..1, origin top-left). '
|
||||
'Drives the pen-plotter ink-signature pipeline. NULL for typed signatures '
|
||||
'or signatures captured before this column existed.';
|
||||
'Stroke-path (vector) form of a drawn signature (normalized 0..1, origin '
|
||||
'top-left). NULL for typed signatures or signatures captured before this '
|
||||
'column existed.';
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
-- 092: Ink-reproduction consent on signature records.
|
||||
--
|
||||
-- The Standard (no-login) CMS filing path reproduces the signer's OWN captured
|
||||
-- signature strokes in real ink on the printed form (pen plotter) so the mailed
|
||||
-- application carries an original ink signature. Per the legal-risk research
|
||||
-- (docs/legal/remote-mechanical-wet-signature-precedent.md), the linchpin that
|
||||
-- keeps this on the valid side of the forgery/agency line is an EXPLICIT,
|
||||
-- per-document authorization from the signer to reproduce their signature in ink
|
||||
-- on this specific document.
|
||||
--
|
||||
-- These columns capture that consent at signing time, alongside the existing
|
||||
-- perjury attestation. They are only meaningful for drawn signatures on ink-path
|
||||
-- documents (metadata.ink_reproduction = true); other docs leave them false/NULL.
|
||||
--
|
||||
-- Idempotent.
|
||||
|
||||
ALTER TABLE esign_records
|
||||
ADD COLUMN IF NOT EXISTS ink_consent BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS ink_consent_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS ink_consent_text TEXT;
|
||||
|
||||
COMMENT ON COLUMN esign_records.ink_consent IS
|
||||
'TRUE when the signer expressly authorized reproducing their drawn signature '
|
||||
'in ink on this document (pen-plotter path). Captured at signing time.';
|
||||
COMMENT ON COLUMN esign_records.ink_consent_at IS
|
||||
'When the ink-reproduction consent was given (signer-side timestamp).';
|
||||
COMMENT ON COLUMN esign_records.ink_consent_text IS
|
||||
'Verbatim consent language the signer agreed to (for the audit trail).';
|
||||
26
api/migrations/092_esign_sign_consent.sql
Normal file
26
api/migrations/092_esign_sign_consent.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-- 092: Per-document signing authorization on signature records.
|
||||
--
|
||||
-- On the Standard (no-login) CMS filing path the signer gives an EXPLICIT,
|
||||
-- per-document authorization to use their drawn signature to complete and submit
|
||||
-- the filing on their behalf. These columns capture that authorization at
|
||||
-- signing time, alongside the existing perjury attestation. They are only
|
||||
-- meaningful for drawn signatures on documents that require it
|
||||
-- (metadata.require_sign_consent = true); other docs leave them false/NULL.
|
||||
--
|
||||
-- NB: the column names use the ink_consent* prefix for historical/migration
|
||||
-- compatibility; they store the generic signing authorization described above.
|
||||
--
|
||||
-- Idempotent.
|
||||
|
||||
ALTER TABLE esign_records
|
||||
ADD COLUMN IF NOT EXISTS ink_consent BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS ink_consent_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS ink_consent_text TEXT;
|
||||
|
||||
COMMENT ON COLUMN esign_records.ink_consent IS
|
||||
'TRUE when the signer expressly authorized using their drawn signature to '
|
||||
'complete and submit this filing. Captured at signing time.';
|
||||
COMMENT ON COLUMN esign_records.ink_consent_at IS
|
||||
'When the signing authorization was given (signer-side timestamp).';
|
||||
COMMENT ON COLUMN esign_records.ink_consent_text IS
|
||||
'Verbatim authorization language the signer agreed to (for the audit trail).';
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
/**
|
||||
* Pure helpers for the ink-reproduction consent gate (DB-free, unit-tested).
|
||||
*
|
||||
* The Standard (no-login) CMS filing path reproduces the signer's OWN captured
|
||||
* signature strokes in real ink on the printed form (pen plotter). The legal
|
||||
* linchpin (docs/legal/remote-mechanical-wet-signature-precedent.md) is an
|
||||
* EXPLICIT, per-document authorization to reproduce the signature in ink. These
|
||||
* helpers decide when that consent is required and whether it was satisfied, so
|
||||
* the route and the signing page agree on one source of truth.
|
||||
*/
|
||||
|
||||
/** Verbatim consent the signer must agree to before drawing an ink-path signature.
|
||||
*
|
||||
* Client-safe (no internal mechanics) but legally explicit: authorizes Performance
|
||||
* West to reproduce THIS signer's OWN drawn signature in ink ONE TIME on THIS
|
||||
* single document. The "only once / this one form / not reused" language reassures
|
||||
* the signer the signature is not stored for reuse. Stored verbatim with the
|
||||
* signature for the audit trail.
|
||||
*/
|
||||
export const INK_CONSENT_TEXT =
|
||||
"I understand this filing must be submitted on the official paper form with an " +
|
||||
"original ink signature. I authorize Performance West Inc. to reproduce my own " +
|
||||
"signature, exactly as I draw it below, in ink one time on this single form, and " +
|
||||
"to submit it on my behalf. My signature will be used solely to complete this " +
|
||||
"filing and will not be reused for any other document or purpose. The signature " +
|
||||
"applied will be my own signature, made with my authorization and with the " +
|
||||
"intent to sign this document.";
|
||||
|
||||
/** Is this document on the ink-reproduction path? (metadata.ink_reproduction). */
|
||||
export function isInkReproduction(documentMetadata: unknown): boolean {
|
||||
return !!documentMetadata
|
||||
&& typeof documentMetadata === "object"
|
||||
&& (documentMetadata as Record<string, unknown>).ink_reproduction === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this signing attempt REQUIRE the ink-reproduction consent?
|
||||
* Only a DRAWN signature on an ink-path document needs it (a typed signature is
|
||||
* not reproduced as the signer's own hand, so it is exempt).
|
||||
*/
|
||||
export function inkConsentRequired(
|
||||
documentMetadata: unknown,
|
||||
signatureType: "drawn" | "typed" | string | undefined,
|
||||
): boolean {
|
||||
return isInkReproduction(documentMetadata) && signatureType === "drawn";
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the consent satisfied for this attempt? True when not required, or when
|
||||
* required and the signer explicitly gave it (ink_consent === true).
|
||||
*/
|
||||
export function inkConsentSatisfied(
|
||||
documentMetadata: unknown,
|
||||
signatureType: "drawn" | "typed" | string | undefined,
|
||||
inkConsent: unknown,
|
||||
): boolean {
|
||||
if (!inkConsentRequired(documentMetadata, signatureType)) return true;
|
||||
return inkConsent === true;
|
||||
}
|
||||
59
api/src/routes/esign-sign-consent.ts
Normal file
59
api/src/routes/esign-sign-consent.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Pure helpers for the signing-authorization gate (DB-free, unit-tested).
|
||||
*
|
||||
* On the Standard (no-login) CMS filing path the signer must give an EXPLICIT,
|
||||
* per-document authorization to use their signature to complete and submit the
|
||||
* filing on their behalf. These helpers decide when that authorization is
|
||||
* required and whether it was satisfied, so the route and the signing page agree
|
||||
* on one source of truth. Client-facing copy stays generic and never describes
|
||||
* any internal fulfillment mechanics.
|
||||
*/
|
||||
|
||||
/** Verbatim authorization the signer must agree to before drawing a signature on
|
||||
* a Standard-path document.
|
||||
*
|
||||
* Client-safe and legally explicit: authorizes Performance West to use THIS
|
||||
* signer's OWN drawn signature ONE TIME on THIS single document and submit it on
|
||||
* their behalf. The "only once / this one form / not reused" language reassures
|
||||
* the signer the signature is not stored for reuse. Stored verbatim with the
|
||||
* signature for the audit trail.
|
||||
*/
|
||||
export const SIGN_CONSENT_TEXT =
|
||||
"I authorize Performance West Inc. to use my signature, exactly as I draw it " +
|
||||
"below, to complete and submit this single filing on my behalf. My signature " +
|
||||
"will be used solely to complete this filing and will not be reused for any " +
|
||||
"other document or purpose. The signature applied will be my own signature, " +
|
||||
"made with my authorization and with the intent to sign this document.";
|
||||
|
||||
/** Does this document require the per-document signing authorization?
|
||||
* (metadata.require_sign_consent). */
|
||||
export function requiresSignConsent(documentMetadata: unknown): boolean {
|
||||
return !!documentMetadata
|
||||
&& typeof documentMetadata === "object"
|
||||
&& (documentMetadata as Record<string, unknown>).require_sign_consent === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this signing attempt REQUIRE the signing authorization?
|
||||
* Only a DRAWN signature on a consent-required document needs it (a typed
|
||||
* signature is exempt).
|
||||
*/
|
||||
export function signConsentRequired(
|
||||
documentMetadata: unknown,
|
||||
signatureType: "drawn" | "typed" | string | undefined,
|
||||
): boolean {
|
||||
return requiresSignConsent(documentMetadata) && signatureType === "drawn";
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the authorization satisfied for this attempt? True when not required, or
|
||||
* when required and the signer explicitly gave it (sign_consent === true).
|
||||
*/
|
||||
export function signConsentSatisfied(
|
||||
documentMetadata: unknown,
|
||||
signatureType: "drawn" | "typed" | string | undefined,
|
||||
signConsent: unknown,
|
||||
): boolean {
|
||||
if (!signConsentRequired(documentMetadata, signatureType)) return true;
|
||||
return signConsent === true;
|
||||
}
|
||||
|
|
@ -147,14 +147,10 @@ const DEFAULT_TIMELINE: TimelineStep[] = [
|
|||
{ name: "Complete", description: "Filing completed and delivered", business_days: 5, status: "pending" },
|
||||
];
|
||||
|
||||
// Services whose Standard path requires an ORIGINAL ink signature on a mailed
|
||||
// CMS form (verified against the forms: CMS-855I/B/O "original signatures" and
|
||||
// CMS-10114 "original and signed in ink"). While the ink-signature station is
|
||||
// being brought online we add a buffer to these services' ETAs so we always
|
||||
// have time to produce + mail the signed original. Remove the buffer once the
|
||||
// plotter station is calibrated and in steady-state.
|
||||
// See docs/state-healthcare-compliance-opportunities.md (wet-signature check)
|
||||
// and docs/plans/plotter-plan.md.
|
||||
// Services whose Standard path requires extra fulfillment handling on a mailed
|
||||
// CMS form and therefore needs a small ETA buffer so we always have time to
|
||||
// produce and mail the completed original. Revisit the buffer once that
|
||||
// fulfillment step is in steady-state.
|
||||
const WET_SIGNATURE_BUFFER_DAYS = 2;
|
||||
const WET_SIGNATURE_SLUGS = new Set<string>([
|
||||
"nppes-update",
|
||||
|
|
@ -226,8 +222,8 @@ router.get("/api/v1/order-timeline/:order_id", async (req: Request, res: Respons
|
|||
const timelines = orders.map((order) => {
|
||||
const slug = order.service_slug as string;
|
||||
const startDate = new Date(order.created_at as string);
|
||||
// Wet-signature services get a buffer on every step from the signature
|
||||
// onward, so we always have time to produce + mail the original ink form.
|
||||
// These services get a buffer on every step from the signature onward,
|
||||
// so we always have time to produce and mail the completed original form.
|
||||
const isWetSig = WET_SIGNATURE_SLUGS.has(slug);
|
||||
let pastSignature = false;
|
||||
const dated = (SERVICE_TIMELINES[slug] || DEFAULT_TIMELINE).map((step) => {
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ import { Router, type Request, type Response } from "express";
|
|||
import { pool } from "../db.js";
|
||||
import { requirePortalAuth } from "../middleware/portalAuth.js";
|
||||
import {
|
||||
INK_CONSENT_TEXT,
|
||||
isInkReproduction,
|
||||
inkConsentSatisfied,
|
||||
} from "./esign-ink-consent.js";
|
||||
SIGN_CONSENT_TEXT,
|
||||
requiresSignConsent,
|
||||
signConsentSatisfied,
|
||||
} from "./esign-sign-consent.js";
|
||||
|
||||
const router = Router();
|
||||
const WORKER_URL = process.env.WORKER_URL || "http://workers:8090";
|
||||
|
|
@ -98,12 +98,10 @@ router.get("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
// Pull order number for subtitle (e.g. "CO-ABC12345")
|
||||
const metadata = rec.document_metadata || {};
|
||||
|
||||
// Ink-reproduction path: when this document's signature will be reproduced in
|
||||
// real ink on the printed form (pen-plotter / Standard CMS filing path), the
|
||||
// signing page must collect an explicit, per-document consent to reproduce the
|
||||
// drawn signature in ink BEFORE capturing it. See
|
||||
// docs/legal/remote-mechanical-wet-signature-precedent.md.
|
||||
const inkReproduction = isInkReproduction(metadata);
|
||||
// Standard-path documents require an explicit, per-document authorization to
|
||||
// use the signer's drawn signature to complete and submit the filing. The
|
||||
// signing page surfaces and collects it BEFORE capturing the signature.
|
||||
const requireSignConsent = requiresSignConsent(metadata);
|
||||
|
||||
res.json({
|
||||
document_title: rec.document_title,
|
||||
|
|
@ -111,8 +109,8 @@ router.get("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
order_number: rec.order_number,
|
||||
document_url: documentUrl,
|
||||
requires_perjury: rec.requires_perjury || false,
|
||||
ink_reproduction: inkReproduction,
|
||||
ink_consent_text: inkReproduction ? INK_CONSENT_TEXT : null,
|
||||
require_sign_consent: requireSignConsent,
|
||||
sign_consent_text: requireSignConsent ? SIGN_CONSENT_TEXT : null,
|
||||
metadata,
|
||||
});
|
||||
} catch (err) {
|
||||
|
|
@ -130,11 +128,11 @@ router.get("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res: Response) => {
|
||||
const { order_id: orderNumber, order_type: documentType, email } = req.portalAuth!;
|
||||
|
||||
const { signature, agreed_at, user_agent, ink_consent } = req.body as {
|
||||
const { signature, agreed_at, user_agent, sign_consent } = req.body as {
|
||||
signature?: { type: "drawn" | "typed"; image_b64?: string; name?: string; vector?: any };
|
||||
agreed_at?: string;
|
||||
user_agent?: string;
|
||||
ink_consent?: boolean;
|
||||
sign_consent?: boolean;
|
||||
};
|
||||
|
||||
// Validate signature
|
||||
|
|
@ -185,15 +183,13 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
return;
|
||||
}
|
||||
|
||||
// Ink-reproduction gate: if this document's signature will be reproduced in
|
||||
// ink on the official paper form, a drawn signature REQUIRES the explicit
|
||||
// per-document ink-reproduction consent (the legal linchpin — see
|
||||
// docs/legal/remote-mechanical-wet-signature-precedent.md). A typed signature
|
||||
// is not reproduced as the signer's own hand, so it does not need this gate.
|
||||
const inkReproduction = isInkReproduction(rec.document_metadata);
|
||||
if (!inkConsentSatisfied(rec.document_metadata, signature.type, ink_consent)) {
|
||||
// Signing-authorization gate: Standard-path documents require an explicit,
|
||||
// per-document authorization to use the signer's drawn signature to complete
|
||||
// and submit the filing. A typed signature is exempt.
|
||||
const requireSignConsent = requiresSignConsent(rec.document_metadata);
|
||||
if (!signConsentSatisfied(rec.document_metadata, signature.type, sign_consent)) {
|
||||
res.status(400).json({
|
||||
error: "Please confirm the authorization to use your signature on the official form.",
|
||||
error: "Please confirm the authorization to use your signature to complete this filing.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -223,9 +219,11 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
const clientIp = (req as any).clientIp || req.ip || "";
|
||||
const signedAt = new Date().toISOString();
|
||||
|
||||
// Record the ink-reproduction consent (only meaningful when this document is
|
||||
// on the ink-reproduction path and the signer drew their signature).
|
||||
const inkConsentGiven = inkReproduction && signature.type === "drawn" && ink_consent === true;
|
||||
// Record the signing authorization (only meaningful when this document
|
||||
// requires it and the signer drew their signature). NB: the DB columns remain
|
||||
// named ink_consent* for migration compatibility; they store the generic
|
||||
// signing authorization.
|
||||
const signConsentGiven = requireSignConsent && signature.type === "drawn" && sign_consent === true;
|
||||
|
||||
await pool.query(
|
||||
`UPDATE esign_records
|
||||
|
|
@ -252,9 +250,9 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
user_agent || "",
|
||||
signedAt,
|
||||
rec.requires_perjury ? true : false,
|
||||
inkConsentGiven,
|
||||
inkConsentGiven ? signedAt : null,
|
||||
inkConsentGiven ? INK_CONSENT_TEXT : null,
|
||||
signConsentGiven,
|
||||
signConsentGiven ? signedAt : null,
|
||||
signConsentGiven ? SIGN_CONSENT_TEXT : null,
|
||||
rec.id,
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
/**
|
||||
* Unit tests for the ink-reproduction consent gate (pure logic, no DB).
|
||||
* Run: npx tsx api/test/test_esign_ink_consent.ts
|
||||
*/
|
||||
import assert from "node:assert";
|
||||
import {
|
||||
INK_CONSENT_TEXT,
|
||||
isInkReproduction,
|
||||
inkConsentRequired,
|
||||
inkConsentSatisfied,
|
||||
} from "../src/routes/esign-ink-consent.js";
|
||||
|
||||
let pass = 0;
|
||||
const ok = (name: string, cond: boolean) => {
|
||||
assert.ok(cond, name);
|
||||
pass++;
|
||||
};
|
||||
|
||||
// --- isInkReproduction ---
|
||||
ok("ink meta true", isInkReproduction({ ink_reproduction: true }) === true);
|
||||
ok("ink meta false", isInkReproduction({ ink_reproduction: false }) === false);
|
||||
ok("ink meta missing", isInkReproduction({}) === false);
|
||||
ok("ink meta null", isInkReproduction(null) === false);
|
||||
ok("ink meta undefined", isInkReproduction(undefined) === false);
|
||||
ok("ink meta string-truthy not enough", isInkReproduction({ ink_reproduction: "true" }) === false);
|
||||
|
||||
// --- inkConsentRequired: only DRAWN on ink-path docs ---
|
||||
ok("required: drawn + ink", inkConsentRequired({ ink_reproduction: true }, "drawn") === true);
|
||||
ok("not required: typed + ink", inkConsentRequired({ ink_reproduction: true }, "typed") === false);
|
||||
ok("not required: drawn + non-ink", inkConsentRequired({ ink_reproduction: false }, "drawn") === false);
|
||||
ok("not required: drawn + no meta", inkConsentRequired({}, "drawn") === false);
|
||||
ok("not required: undefined type", inkConsentRequired({ ink_reproduction: true }, undefined) === false);
|
||||
|
||||
// --- inkConsentSatisfied ---
|
||||
// exempt cases (consent not required) are always satisfied regardless of flag
|
||||
ok("satisfied: typed exempt (no consent)", inkConsentSatisfied({ ink_reproduction: true }, "typed", undefined) === true);
|
||||
ok("satisfied: non-ink drawn exempt", inkConsentSatisfied({ ink_reproduction: false }, "drawn", undefined) === true);
|
||||
ok("satisfied: no-meta drawn exempt", inkConsentSatisfied({}, "drawn", false) === true);
|
||||
// required cases: must have ink_consent === true
|
||||
ok("blocked: drawn+ink, no consent", inkConsentSatisfied({ ink_reproduction: true }, "drawn", undefined) === false);
|
||||
ok("blocked: drawn+ink, consent false", inkConsentSatisfied({ ink_reproduction: true }, "drawn", false) === false);
|
||||
ok("blocked: drawn+ink, consent truthy-but-not-true", inkConsentSatisfied({ ink_reproduction: true }, "drawn", "true") === false);
|
||||
ok("blocked: drawn+ink, consent 1 (not strict true)", inkConsentSatisfied({ ink_reproduction: true }, "drawn", 1) === false);
|
||||
ok("allowed: drawn+ink, consent true", inkConsentSatisfied({ ink_reproduction: true }, "drawn", true) === true);
|
||||
|
||||
// --- consent text: client-safe (no internal mechanics) ---
|
||||
const banned = ["plotter", "machine", "CMS", "855", "10114", "MAC", "Baltimore", "PO Box", "robot"];
|
||||
for (const w of banned) {
|
||||
ok(`consent text omits "${w}"`, !INK_CONSENT_TEXT.toLowerCase().includes(w.toLowerCase()));
|
||||
}
|
||||
// --- consent text: legally required reassurances present ---
|
||||
ok("consent says 'ink one time'", INK_CONSENT_TEXT.includes("in ink one time"));
|
||||
ok("consent says 'single form'", INK_CONSENT_TEXT.includes("single form"));
|
||||
ok("consent says 'not be reused'", INK_CONSENT_TEXT.includes("will not be reused"));
|
||||
ok("consent says 'my own signature'", INK_CONSENT_TEXT.includes("my own signature"));
|
||||
ok("consent says 'intent to sign'", INK_CONSENT_TEXT.includes("intent to sign"));
|
||||
|
||||
console.log(`\nesign ink-consent: ${pass} checks passed`);
|
||||
58
api/test/test_esign_sign_consent.ts
Normal file
58
api/test/test_esign_sign_consent.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* Unit tests for the signing-authorization gate (pure logic, no DB).
|
||||
* Run: npx tsx api/test/test_esign_sign_consent.ts
|
||||
*/
|
||||
import assert from "node:assert";
|
||||
import {
|
||||
SIGN_CONSENT_TEXT,
|
||||
requiresSignConsent,
|
||||
signConsentRequired,
|
||||
signConsentSatisfied,
|
||||
} from "../src/routes/esign-sign-consent.js";
|
||||
|
||||
let pass = 0;
|
||||
const ok = (name: string, cond: boolean) => {
|
||||
assert.ok(cond, name);
|
||||
pass++;
|
||||
};
|
||||
|
||||
// --- requiresSignConsent ---
|
||||
ok("meta true", requiresSignConsent({ require_sign_consent: true }) === true);
|
||||
ok("meta false", requiresSignConsent({ require_sign_consent: false }) === false);
|
||||
ok("meta missing", requiresSignConsent({}) === false);
|
||||
ok("meta null", requiresSignConsent(null) === false);
|
||||
ok("meta undefined", requiresSignConsent(undefined) === false);
|
||||
ok("meta string-truthy not enough", requiresSignConsent({ require_sign_consent: "true" }) === false);
|
||||
|
||||
// --- signConsentRequired: only DRAWN on consent-required docs ---
|
||||
ok("required: drawn + flag", signConsentRequired({ require_sign_consent: true }, "drawn") === true);
|
||||
ok("not required: typed + flag", signConsentRequired({ require_sign_consent: true }, "typed") === false);
|
||||
ok("not required: drawn + no flag", signConsentRequired({ require_sign_consent: false }, "drawn") === false);
|
||||
ok("not required: drawn + no meta", signConsentRequired({}, "drawn") === false);
|
||||
ok("not required: undefined type", signConsentRequired({ require_sign_consent: true }, undefined) === false);
|
||||
|
||||
// --- signConsentSatisfied ---
|
||||
// exempt cases (consent not required) are always satisfied regardless of flag
|
||||
ok("satisfied: typed exempt", signConsentSatisfied({ require_sign_consent: true }, "typed", undefined) === true);
|
||||
ok("satisfied: no-flag drawn exempt", signConsentSatisfied({ require_sign_consent: false }, "drawn", undefined) === true);
|
||||
ok("satisfied: no-meta drawn exempt", signConsentSatisfied({}, "drawn", false) === true);
|
||||
// required cases: must have sign_consent === true
|
||||
ok("blocked: drawn+flag, no consent", signConsentSatisfied({ require_sign_consent: true }, "drawn", undefined) === false);
|
||||
ok("blocked: drawn+flag, consent false", signConsentSatisfied({ require_sign_consent: true }, "drawn", false) === false);
|
||||
ok("blocked: drawn+flag, consent truthy-but-not-true", signConsentSatisfied({ require_sign_consent: true }, "drawn", "true") === false);
|
||||
ok("blocked: drawn+flag, consent 1 (not strict true)", signConsentSatisfied({ require_sign_consent: true }, "drawn", 1) === false);
|
||||
ok("allowed: drawn+flag, consent true", signConsentSatisfied({ require_sign_consent: true }, "drawn", true) === true);
|
||||
|
||||
// --- consent text: client-safe, never describes fulfillment mechanics ---
|
||||
const banned = ["ink", "plotter", "machine", "paper", "print", "CMS", "855", "10114", "MAC", "Baltimore", "PO Box", "robot", "reproduce"];
|
||||
for (const w of banned) {
|
||||
ok(`consent text omits "${w}"`, !SIGN_CONSENT_TEXT.toLowerCase().includes(w.toLowerCase()));
|
||||
}
|
||||
// --- consent text: legally required reassurances present ---
|
||||
ok("consent says 'single filing'", SIGN_CONSENT_TEXT.includes("single filing"));
|
||||
ok("consent says 'not be reused'", SIGN_CONSENT_TEXT.includes("will not be reused"));
|
||||
ok("consent says 'my own signature'", SIGN_CONSENT_TEXT.includes("my own signature"));
|
||||
ok("consent says 'intent to sign'", SIGN_CONSENT_TEXT.includes("intent to sign"));
|
||||
ok("consent says 'on my behalf'", SIGN_CONSENT_TEXT.includes("on my behalf"));
|
||||
|
||||
console.log(`\nesign signing-authorization: ${pass} checks passed`);
|
||||
|
|
@ -375,7 +375,7 @@ class _BaseNPIHandler:
|
|||
customer_name=provider,
|
||||
document_minio_key=document_key,
|
||||
requires_perjury=True,
|
||||
metadata={"service_slug": self.SERVICE_SLUG, "npi": intake.get("npi", ""), "form_type": form_type, "ink_reproduction": True},
|
||||
metadata={"service_slug": self.SERVICE_SLUG, "npi": intake.get("npi", ""), "form_type": form_type, "require_sign_consent": True},
|
||||
expires_hours=21 * 24,
|
||||
)
|
||||
# request_esign does not persist signature anchors; attach them so
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
|||
#loading{text-align:center;padding:4rem 1rem;color:#64748b}
|
||||
#error-screen{display:none;text-align:center;padding:3rem 1rem}
|
||||
.hidden{display:none}
|
||||
/* Signature block locked until ink-reproduction consent is given */
|
||||
/* Signature block locked until signing authorization is given */
|
||||
#sig-block.locked{opacity:.45;pointer-events:none;filter:grayscale(.3)}
|
||||
</style>
|
||||
<script defer src="https://analytics.performancewest.net/script.js" data-website-id="55250014-ee15-44ac-a1f6-81dabad3fe0f"></script><script defer src="/js/pw-analytics.js"></script></head>
|
||||
|
|
@ -79,13 +79,13 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
|||
<div class="card">
|
||||
<h2>Step 2 — Your Signature</h2>
|
||||
|
||||
<!-- Ink-reproduction consent gate (shown only for ink-path documents).
|
||||
The signer must authorize use of their signature on the official form
|
||||
<!-- Signing-authorization gate (shown only when the document requires it).
|
||||
The signer must authorize use of their signature to complete the filing
|
||||
BEFORE they can draw it. -->
|
||||
<div id="ink-consent-box" class="hidden" style="padding:1rem;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;margin-bottom:1rem">
|
||||
<p style="font-size:.85rem;color:#1f2937;margin:0 0 .75rem" id="ink-consent-text"></p>
|
||||
<div id="sign-consent-box" class="hidden" style="padding:1rem;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;margin-bottom:1rem">
|
||||
<p style="font-size:.85rem;color:#1f2937;margin:0 0 .75rem" id="sign-consent-text"></p>
|
||||
<label style="display:flex;gap:.6rem;align-items:flex-start;font-size:.85rem;color:#1f2937;cursor:pointer">
|
||||
<input type="checkbox" id="ink-consent-chk" style="margin-top:2px;flex-shrink:0;width:16px;height:16px;accent-color:#1e3a5f">
|
||||
<input type="checkbox" id="sign-consent-chk" style="margin-top:2px;flex-shrink:0;width:16px;height:16px;accent-color:#1e3a5f">
|
||||
<span>I authorize this and confirm the signature I draw is my own.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -206,13 +206,12 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
|||
document.getElementById("perjury-box").classList.remove("hidden");
|
||||
}
|
||||
|
||||
// Ink-reproduction consent gate: for documents whose signature will be
|
||||
// reproduced in ink on the official paper form, require an explicit
|
||||
// per-document authorization BEFORE the signer can draw. Lock the signature
|
||||
// block until the consent box is checked.
|
||||
if (data.ink_reproduction && data.ink_consent_text) {
|
||||
document.getElementById("ink-consent-text").textContent = data.ink_consent_text;
|
||||
document.getElementById("ink-consent-box").classList.remove("hidden");
|
||||
// Signing-authorization gate: for documents that require it, show an
|
||||
// explicit per-document authorization that must be accepted BEFORE the
|
||||
// signer can draw. Lock the signature block until the box is checked.
|
||||
if (data.require_sign_consent && data.sign_consent_text) {
|
||||
document.getElementById("sign-consent-text").textContent = data.sign_consent_text;
|
||||
document.getElementById("sign-consent-box").classList.remove("hidden");
|
||||
document.getElementById("sig-block").classList.add("locked");
|
||||
}
|
||||
|
||||
|
|
@ -242,7 +241,7 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
|||
var hasSig = false;
|
||||
|
||||
// Vector capture: stroke paths normalized to the capture box (0..1, origin
|
||||
// top-left), resolution-independent. Drives the pen-plotter ink pipeline.
|
||||
// top-left), resolution-independent.
|
||||
var sigStrokes = []; // array of strokes; each stroke = array of {x,y,t}
|
||||
var curStroke = null;
|
||||
var strokeStart = 0;
|
||||
|
|
@ -344,12 +343,12 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
|||
var agreeChk = document.getElementById("agree-chk");
|
||||
agreeChk.addEventListener("change", updateSubmit);
|
||||
|
||||
// ── Ink-reproduction consent: unlock the signature block when authorized ──
|
||||
var inkConsentChk = document.getElementById("ink-consent-chk");
|
||||
inkConsentChk.addEventListener("change", function() {
|
||||
// ── Signing authorization: unlock the signature block when authorized ──
|
||||
var signConsentChk = document.getElementById("sign-consent-chk");
|
||||
signConsentChk.addEventListener("change", function() {
|
||||
var data = window._esignData || {};
|
||||
var needsInk = !!(data.ink_reproduction && data.ink_consent_text);
|
||||
if (needsInk) {
|
||||
var needsConsent = !!(data.require_sign_consent && data.sign_consent_text);
|
||||
if (needsConsent) {
|
||||
document.getElementById("sig-block").classList.toggle("locked", !this.checked);
|
||||
// If they un-check after drawing, clear the now-unauthorized capture.
|
||||
if (!this.checked) document.getElementById("sig-clear").click();
|
||||
|
|
@ -357,18 +356,18 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
|||
updateSubmit();
|
||||
});
|
||||
|
||||
function inkConsentOk() {
|
||||
function signConsentOk() {
|
||||
var data = window._esignData || {};
|
||||
var needsInk = !!(data.ink_reproduction && data.ink_consent_text);
|
||||
// Ink consent is only required for a DRAWN signature on an ink-path doc.
|
||||
if (!needsInk || sigMode !== "draw") return true;
|
||||
return inkConsentChk.checked;
|
||||
var needsConsent = !!(data.require_sign_consent && data.sign_consent_text);
|
||||
// Authorization is only required for a DRAWN signature on a consent-required doc.
|
||||
if (!needsConsent || sigMode !== "draw") return true;
|
||||
return signConsentChk.checked;
|
||||
}
|
||||
|
||||
function updateSubmit() {
|
||||
var hasSignature = sigMode === "draw" ? hasSig : typedInput.value.trim().length >= 2;
|
||||
document.getElementById("submit-btn").disabled =
|
||||
!(hasSignature && agreeChk.checked && inkConsentOk());
|
||||
!(hasSignature && agreeChk.checked && signConsentOk());
|
||||
}
|
||||
|
||||
document.getElementById("submit-btn").addEventListener("click", async function() {
|
||||
|
|
@ -382,7 +381,7 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
|||
if (sigMode === "draw") {
|
||||
signatureData = { type: "drawn", image_b64: canvas.toDataURL("image/png") };
|
||||
// Attach the vector strokes (resolution-independent) so the same signing
|
||||
// event can also drive the pen-plotter ink-signature pipeline.
|
||||
// event resumes the filing pipeline.
|
||||
if (sigStrokes.length) {
|
||||
signatureData.vector = { v: 1, w: captureW, h: captureH, strokes: sigStrokes };
|
||||
}
|
||||
|
|
@ -401,7 +400,7 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
|||
signature: signatureData,
|
||||
agreed_at: new Date().toISOString(),
|
||||
user_agent: navigator.userAgent,
|
||||
ink_consent: inkConsentOk() && sigMode === "draw" && inkConsentChk.checked,
|
||||
sign_consent: signConsentOk() && sigMode === "draw" && signConsentChk.checked,
|
||||
}),
|
||||
});
|
||||
var result = await resp.json();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue