2026-05-06 18:33:15 +02:00
/ * *
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
* External EOI upload - for EOIs signed outside Documenso ( paper signing ,
2026-05-06 18:33:15 +02:00
* different e - sign vendor , signed in person , etc ) .
*
* Creates BOTH the document row AND the signed - file record in one shot ,
* then advances the interest stage . Distinct from the existing
* uploadSignedManually flow which augments a document row that was
* already created via the Documenso pathway .
* /
2026-05-21 19:34:19 +02:00
import { and , eq , inArray } from 'drizzle-orm' ;
2026-05-06 18:33:15 +02:00
import { db } from '@/lib/db' ;
import { interests } from '@/lib/db/schema/interests' ;
2026-05-21 17:34:59 +02:00
import { documents , documentEvents , documentSigners , files } from '@/lib/db/schema/documents' ;
2026-05-06 18:33:15 +02:00
import { ports } from '@/lib/db/schema/ports' ;
import { env } from '@/lib/env' ;
import { buildStoragePath } from '@/lib/minio' ;
import { getStorageBackend } from '@/lib/storage' ;
import { createAuditLog , type AuditMeta } from '@/lib/audit' ;
2026-05-21 19:34:19 +02:00
import { CodedError , ConflictError , NotFoundError , ValidationError } from '@/lib/errors' ;
2026-05-06 18:33:15 +02:00
import { emitToRoom } from '@/lib/socket/server' ;
2026-05-21 17:34:59 +02:00
/** A single signatory on an externally-signed document. */
export interface ExternalSignatory {
name : string ;
email : string ;
/ * * D r i v e s a u t o - f i l l o n t h e d i a l o g + d o w n s t r e a m " E m a i l c o p y " r e c i p i e n t
* list . ` cc ` recipients aren ' t signers but show up on send - out lists . * /
role : 'client' | 'developer' | 'rep' | 'witness' | 'cc' ;
}
2026-05-06 18:33:15 +02:00
export interface ExternalEoiInput {
interestId : string ;
portId : string ;
/** PDF bytes. */
fileData : { buffer : Buffer ; originalName : string ; mimeType : string ; size : number } ;
/** Free-text title for the document row. Defaults to "External EOI - <date>". */
title? : string ;
/** When the signing actually happened (the date on the paper / contract). */
signedAt? : Date ;
2026-05-21 17:34:59 +02:00
/ * *
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
* Structured signatory list - preferred over the legacy ` signerNames `
2026-05-21 17:34:59 +02:00
* CSV . When present , the service inserts one ` document_signers ` row
* per non - CC entry pre - stamped ` status='signed' ` so the
* "X / Y signed" badge renders correctly downstream .
* /
signatories? : ExternalSignatory [ ] ;
/** Legacy CSV form kept for backwards compat with older callers. */
2026-05-06 18:33:15 +02:00
signerNames? : string [ ] ;
/** Free-text note (e.g. "signed in person at boat show"). */
notes? : string ;
meta : AuditMeta ;
}
export async function uploadExternallySignedEoi ( input : ExternalEoiInput ) {
const { interestId , portId , fileData , meta } = input ;
if ( fileData . size <= 0 ) throw new ValidationError ( 'Empty file' ) ;
if ( fileData . size > 25 * 1024 * 1024 ) {
throw new ValidationError ( 'File too large (max 25 MB)' ) ;
}
if (
fileData . mimeType !== 'application/pdf' &&
! fileData . originalName . toLowerCase ( ) . endsWith ( '.pdf' )
) {
throw new ValidationError ( 'Only PDF uploads are accepted for signed EOIs' ) ;
}
const interest = await db . query . interests . findFirst ( {
where : eq ( interests . id , interestId ) ,
} ) ;
if ( ! interest || interest . portId !== portId ) throw new NotFoundError ( 'Interest' ) ;
const port = await db . query . ports . findFirst ( { where : eq ( ports . id , portId ) } ) ;
if ( ! port ) throw new NotFoundError ( 'Port' ) ;
const documentId = crypto . randomUUID ( ) ;
const fileId = crypto . randomUUID ( ) ;
const storagePath = buildStoragePath ( port . slug , 'eoi-signed' , documentId , fileId , 'pdf' ) ;
2026-05-06 22:11:00 +02:00
// Upload to storage FIRST so we have a stable key for the DB rows,
// then commit all four DB writes in one transaction. If the tx fails
// the storage object becomes orphaned (S3 isn't transactional) but
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
// the DB stays clean - orphan reaper handles those.
2026-05-06 18:33:15 +02:00
await (
await getStorageBackend ( )
) . put ( storagePath , fileData . buffer , {
contentType : 'application/pdf' ,
sizeBytes : fileData.size ,
} ) ;
const title =
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
input . title ? ? ` External EOI - ${ ( input . signedAt ? ? new Date ( ) ) . toISOString ( ) . slice ( 0 , 10 ) } ` ;
2026-05-06 18:33:15 +02:00
2026-05-06 22:11:00 +02:00
const result = await db . transaction ( async ( tx ) = > {
const [ fileRecord ] = await tx
. insert ( files )
. values ( {
portId ,
clientId : interest.clientId ,
filename : fileData.originalName ,
originalName : fileData.originalName ,
mimeType : 'application/pdf' ,
sizeBytes : String ( fileData . size ) ,
storagePath ,
storageBucket : env.MINIO_BUCKET ,
category : 'eoi' ,
uploadedBy : meta.userId ,
} )
. returning ( ) ;
if ( ! fileRecord ) {
throw new CodedError ( 'INSERT_RETURNING_EMPTY' , {
internalMessage : 'External EOI file insert returned no row' ,
} ) ;
}
const [ doc ] = await tx
. insert ( documents )
. values ( {
id : documentId ,
portId ,
interestId ,
clientId : interest.clientId ,
yachtId : interest.yachtId ,
documentType : 'eoi' ,
title ,
status : 'completed' ,
isManualUpload : true ,
signedFileId : fileRecord.id ,
notes : input.notes ? ? null ,
createdBy : meta.userId ,
} )
. returning ( ) ;
if ( ! doc ) {
throw new CodedError ( 'INSERT_RETURNING_EMPTY' , {
internalMessage : 'External EOI document insert returned no row' ,
} ) ;
}
2026-05-21 17:34:59 +02:00
// Backfill document_signers rows for the structured signatory list
// so the document-detail "X / Y signed" badge counts correctly. CC
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
// recipients aren't signatories - they're recipients of the email
2026-05-21 17:34:59 +02:00
// copy and don't show up in the signed-count denominator.
const signedAtMoment = input . signedAt ? ? new Date ( ) ;
const signerRows = ( input . signatories ? ? [ ] )
. filter ( ( s ) = > s . role !== 'cc' )
. map ( ( s , i ) = > ( {
documentId : doc.id ,
signerName : s.name ,
signerEmail : s.email ,
signerRole : s.role ,
signingOrder : i + 1 ,
status : 'signed' as const ,
signedAt : signedAtMoment ,
} ) ) ;
if ( signerRows . length > 0 ) {
await tx . insert ( documentSigners ) . values ( signerRows ) ;
}
2026-05-06 22:11:00 +02:00
await tx . insert ( documentEvents ) . values ( {
documentId : doc.id ,
eventType : 'completed' ,
eventData : {
isManualUpload : true ,
external : true ,
2026-05-21 17:34:59 +02:00
signerNames : input.signerNames ? ? ( input . signatories ? ? [ ] ) . map ( ( s ) = > s . name ) ,
signatories : input.signatories ? ? null ,
signedAt : signedAtMoment.toISOString ( ) ,
2026-05-06 22:11:00 +02:00
fileId : fileRecord.id ,
} ,
2026-05-06 18:33:15 +02:00
} ) ;
2026-05-21 17:01:35 +02:00
// Two concerns to keep separate:
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
// 1. Document metadata - always write `dateEoiSigned` + `eoiStatus`
2026-05-21 17:01:35 +02:00
// from the upload. Even if the rep already advanced the stage
// manually, the paper signing event needs a recorded date so
// downstream surfaces (SkipAheadBanner, milestone strip, EOI
// merge fields) reflect reality. Honour an existing
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
// dateEoiSigned (don't overwrite if already set - covers the
2026-05-21 17:01:35 +02:00
// case where the rep is uploading evidence for an event whose
// date was already backfilled).
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
// 2. Stage advance - only when the deal hasn't reached eoi_signed
2026-05-21 17:01:35 +02:00
// yet. Bypasses canTransitionStage because the operator just
// brought concrete proof.
const shouldAdvanceStage =
2026-05-06 22:11:00 +02:00
interest . pipelineStage === 'open' ||
interest . pipelineStage === 'details_sent' ||
interest . pipelineStage === 'in_communication' ||
2026-05-21 17:01:35 +02:00
interest . pipelineStage === 'eoi_sent' ;
await tx
. update ( interests )
. set ( {
dateEoiSigned : interest.dateEoiSigned ? ? input . signedAt ? ? new Date ( ) ,
eoiStatus : 'signed' ,
. . . ( shouldAdvanceStage ? { pipelineStage : 'eoi_signed' as const } : { } ) ,
updatedAt : new Date ( ) ,
} )
. where ( eq ( interests . id , interestId ) ) ;
2026-05-06 22:11:00 +02:00
2026-05-21 17:01:35 +02:00
return {
documentId : doc.id ,
fileId : fileRecord.id ,
stageChanged : shouldAdvanceStage ,
newStage : shouldAdvanceStage ? ( 'eoi_signed' as const ) : interest . pipelineStage ,
} ;
2026-05-06 18:33:15 +02:00
} ) ;
2026-05-21 17:01:35 +02:00
const { documentId : docId , fileId : fId , stageChanged , newStage } = result ;
2026-05-06 18:33:15 +02:00
void createAuditLog ( {
portId ,
userId : meta.userId ,
action : 'create' ,
entityType : 'document' ,
2026-05-06 22:11:00 +02:00
entityId : docId ,
2026-05-06 18:33:15 +02:00
metadata : {
kind : 'external_eoi_upload' ,
interestId ,
title ,
signerNames : input.signerNames ? ? [ ] ,
signedAt : ( input . signedAt ? ? new Date ( ) ) . toISOString ( ) ,
fileSizeBytes : fileData.size ,
} ,
ipAddress : meta.ipAddress ,
userAgent : meta.userAgent ,
} ) ;
2026-05-06 22:11:00 +02:00
emitToRoom ( ` port: ${ portId } ` , 'document:completed' , { documentId : docId } ) ;
2026-05-06 18:33:15 +02:00
2026-05-21 17:01:35 +02:00
// Berth rules engine: a manually-uploaded external EOI is still an
// EOI-signed event for the rules that watch this trigger (e.g.
// auto-mark the primary berth Under Offer). Fire via dynamic import
// to dodge the circular dep between berth-rules-engine and the
// interest services.
try {
const { evaluateRule } = await import ( '@/lib/services/berth-rules-engine' ) ;
await evaluateRule ( 'eoi_signed' , interestId , portId , meta ) ;
} catch {
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
// Swallow - rules engine failures should never block the upload
2026-05-21 17:01:35 +02:00
// that the rep has already completed end-to-end. The orphan-reaper
// path doesn't apply; a missed rule evaluation is a soft failure.
}
return { documentId : docId , fileId : fId , stageChanged , newStage } ;
2026-05-06 18:33:15 +02:00
}
2026-05-21 19:34:19 +02:00
// ─── Edit metadata on a previously-uploaded external EOI ─────────────────────
/** Signatory row in an edit payload. `id` distinguishes update vs insert. */
export interface ExternalEoiSignatoryEdit {
/** Present on existing rows; omitted to insert a new signer. */
id? : string ;
name : string ;
email : string ;
role : 'client' | 'developer' | 'rep' | 'witness' | 'cc' ;
}
export interface ExternalEoiMetadataPatch {
documentId : string ;
portId : string ;
/** Undefined = leave unchanged; null is not allowed (title is NOT NULL). */
title? : string ;
/** Undefined = leave unchanged; null clears `interests.dateEoiSigned`. */
signedAt? : Date | null ;
/** Undefined = leave unchanged; '' clears the column. */
notes? : string ;
/ * *
* Full replacement set when present . Rows with an id are UPDATEd ; rows
* without are INSERTed ; existing rows whose id isn ' t in the array are
* DELETEd . CC entries are stored but excluded from the X / Y signed
* count ( status stays signed = signedAt to match the upload - time
* pre - stamp ) .
* /
signatories? : ExternalEoiSignatoryEdit [ ] ;
meta : AuditMeta ;
}
/ * *
* Update title / notes / signed - date / signatories on a previously - uploaded
* external EOI . Refuses to touch Documenso - managed documents because their
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
* signer rows are the vendor ' s source of truth - any edit would drift from
2026-05-21 19:34:19 +02:00
* the upstream envelope .
*
* Mirrors the upload service ' s invariants :
* - ` signedAt ` updates BOTH ` documents ` events and ` interests.dateEoiSigned `
* ( and the per - signer ` signedAt ` stamp for the structured signatories ) .
* - Document_signers writes are full - replacement when ` signatories ` is
* present ( insert / update / delete by id - presence ) . Same shape as the
* upload - time insert : CC entries persisted but not counted as signers .
*
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
* Stage advance is NOT re - evaluated - that fires once at upload and shouldn ' t
2026-05-21 19:34:19 +02:00
* be reversed by a metadata edit . If the rep needs to roll a stage back ,
* they do it through the stage - change UI directly .
* /
export async function updateExternalEoiMetadata ( input : ExternalEoiMetadataPatch ) {
const { documentId , portId , meta } = input ;
const document = await db . query . documents . findFirst ( {
where : and ( eq ( documents . id , documentId ) , eq ( documents . portId , portId ) ) ,
} ) ;
if ( ! document ) throw new NotFoundError ( 'Document' ) ;
if ( ! document . isManualUpload ) {
throw new ConflictError (
'Only manually-uploaded documents can have their metadata edited. Documenso-managed documents inherit their signers and signing date from the upstream envelope.' ,
) ;
}
if ( document . documentType !== 'eoi' ) {
// The form only knows EOI shape today; widen later when other doc
// types grow their own external-upload pathways.
throw new ConflictError (
'Metadata edit is currently supported only for EOI documents. Open a ticket if you need it for contracts or reservations.' ,
) ;
}
// Capture before-state for the audit log.
const beforeSigners = await db . query . documentSigners . findMany ( {
where : eq ( documentSigners . documentId , documentId ) ,
} ) ;
const result = await db . transaction ( async ( tx ) = > {
// 1) Patch the document row itself.
const docPatch : Partial < typeof documents. $ inferInsert > = { updatedAt : new Date ( ) } ;
if ( input . title !== undefined ) docPatch . title = input . title ;
if ( input . notes !== undefined ) docPatch . notes = input . notes === '' ? null : input . notes ;
await tx . update ( documents ) . set ( docPatch ) . where ( eq ( documents . id , documentId ) ) ;
// 2) Sync the interest's `dateEoiSigned` when signedAt is being
// edited. Honour the upload-side rule: a metadata edit IS the
// canonical source for the date, so we overwrite even when the
// column already has a value (the rep is presumably fixing it).
if ( input . signedAt !== undefined && document . interestId ) {
await tx
. update ( interests )
. set ( {
dateEoiSigned : input.signedAt ,
updatedAt : new Date ( ) ,
} )
. where ( eq ( interests . id , document . interestId ) ) ;
}
// 3) Replace the signatories list when provided. Mirror the upload
// invariants: status='signed' on every non-CC row (the doc has
// already been signed externally), signedAt stamped from the
// edit's effective signing date.
if ( input . signatories !== undefined ) {
const signedAtMoment =
input . signedAt !== undefined
? ( input . signedAt ? ? new Date ( ) )
: ( beforeSigners [ 0 ] ? . signedAt ? ? new Date ( ) ) ;
const submittedIds = new Set ( input . signatories . filter ( ( s ) = > s . id ) . map ( ( s ) = > s . id ! ) ) ;
const existingIds = beforeSigners . map ( ( s ) = > s . id ) ;
const toDelete = existingIds . filter ( ( id ) = > ! submittedIds . has ( id ) ) ;
if ( toDelete . length > 0 ) {
await tx . delete ( documentSigners ) . where ( inArray ( documentSigners . id , toDelete ) ) ;
}
for ( let i = 0 ; i < input . signatories . length ; i ++ ) {
const s = input . signatories [ i ] ! ;
const isSigner = s . role !== 'cc' ;
if ( s . id && existingIds . includes ( s . id ) ) {
await tx
. update ( documentSigners )
. set ( {
signerName : s.name ,
signerEmail : s.email ,
signerRole : s.role ,
signingOrder : i + 1 ,
status : isSigner ? 'signed' : 'pending' ,
signedAt : isSigner ? signedAtMoment : null ,
} )
. where ( eq ( documentSigners . id , s . id ) ) ;
} else {
await tx . insert ( documentSigners ) . values ( {
documentId ,
signerName : s.name ,
signerEmail : s.email ,
signerRole : s.role ,
signingOrder : i + 1 ,
status : isSigner ? 'signed' : 'pending' ,
signedAt : isSigner ? signedAtMoment : null ,
} ) ;
}
}
}
// 4) Trail in document_events so the activity timeline reflects the
// edit alongside the original 'completed' row.
await tx . insert ( documentEvents ) . values ( {
documentId ,
eventType : 'metadata_updated' ,
eventData : {
editedBy : meta.userId ,
fields : {
title : input.title !== undefined ,
signedAt : input.signedAt !== undefined ,
notes : input.notes !== undefined ,
signatories : input.signatories !== undefined ,
} ,
} ,
} ) ;
return { documentId } ;
} ) ;
void createAuditLog ( {
portId ,
userId : meta.userId ,
action : 'update' ,
entityType : 'document' ,
entityId : documentId ,
metadata : {
kind : 'external_eoi_metadata_edit' ,
fieldsChanged : {
title : input.title !== undefined ,
signedAt : input.signedAt !== undefined ,
notes : input.notes !== undefined ,
signatories : input.signatories !== undefined ,
} ,
} ,
ipAddress : meta.ipAddress ,
userAgent : meta.userAgent ,
} ) ;
emitToRoom ( ` port: ${ portId } ` , 'document:updated' , { documentId : result.documentId } ) ;
return { documentId : result.documentId } ;
}