feat(externally-signed): mark contract/reservation as signed without file
Step 4 second slice. Adds the "Mark as signed without file" action to
contract + reservation tabs per PRE-DEPLOY-PLAN § 1.5.14.
Service: `markExternallySigned(interestId, portId, docType, reason)`
flips the relevant doc-status column ('contract_doc_status' /
'reservation_doc_status' / 'eoi_doc_status') to 'signed', writes an
audit log entry with `metadata.type='externally_signed'` capturing
the optional reason, and fires the appropriate berth-rule trigger
(eoi_signed / contract_signed) so downstream automation (berth
status flips, notifications) treats it identically to a Documenso-
signed completion.
Route: POST /api/v1/interests/[id]/mark-externally-signed gated on
interests.edit. Validates docType against the canonical 3-value enum.
UI: <MarkExternallySignedDialog> AlertDialog with optional reason
textarea + per-docType copy. Wired into EmptyContractState and
EmptyReservationState empty-state buttons. The action sits alongside
"Upload draft for signing" and "Upload paper-signed copy" as a third
option for reps whose canonical paper lives elsewhere.
EOI not yet wired into a UI surface — the eoi flow already has a
full upload pipeline. Service supports it for completeness.
Followup: quick brochure/PDF download buttons + per-user reminder
digest schedule still pending in Step 4 backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||||
|
import { parseBody } from '@/lib/api/route-helpers';
|
||||||
|
import { errorResponse, NotFoundError } from '@/lib/errors';
|
||||||
|
import { markExternallySigned } from '@/lib/services/external-signing.service';
|
||||||
|
|
||||||
|
const bodySchema = z.object({
|
||||||
|
docType: z.enum(['eoi', 'reservation', 'contract']),
|
||||||
|
reason: z.string().trim().max(2000).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/v1/interests/[id]/mark-externally-signed
|
||||||
|
*
|
||||||
|
* Marks the named document type as signed without requiring a file
|
||||||
|
* upload. Sets the relevant `*_doc_status` column to 'signed', writes
|
||||||
|
* an audit log entry capturing the reason, and fires the appropriate
|
||||||
|
* berth-rule trigger (eoi_signed / contract_signed) if any.
|
||||||
|
*/
|
||||||
|
export const POST = withAuth(
|
||||||
|
withPermission('interests', 'edit', async (req, ctx, params) => {
|
||||||
|
try {
|
||||||
|
const interestId = params.id;
|
||||||
|
if (!interestId) throw new NotFoundError('Interest');
|
||||||
|
const input = await parseBody(req, bodySchema);
|
||||||
|
const result = await markExternallySigned(
|
||||||
|
{
|
||||||
|
interestId,
|
||||||
|
portId: ctx.portId,
|
||||||
|
docType: input.docType,
|
||||||
|
reason: input.reason ?? null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: ctx.userId,
|
||||||
|
portId: ctx.portId,
|
||||||
|
ipAddress: ctx.ipAddress,
|
||||||
|
userAgent: ctx.userAgent,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return NextResponse.json({ data: result });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -19,6 +19,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { ExternalEoiUploadDialog } from '@/components/interests/external-eoi-upload-dialog';
|
import { ExternalEoiUploadDialog } from '@/components/interests/external-eoi-upload-dialog';
|
||||||
|
import { MarkExternallySignedDialog } from '@/components/interests/mark-externally-signed-dialog';
|
||||||
import { SigningProgress } from '@/components/documents/signing-progress';
|
import { SigningProgress } from '@/components/documents/signing-progress';
|
||||||
import { UploadForSigningDialog } from '@/components/documents/upload-for-signing-dialog';
|
import { UploadForSigningDialog } from '@/components/documents/upload-for-signing-dialog';
|
||||||
import { apiFetch } from '@/lib/api/client';
|
import { apiFetch } from '@/lib/api/client';
|
||||||
@@ -91,6 +92,7 @@ export function InterestContractTab({ interestId, clientId: _clientId }: Interes
|
|||||||
const portSlug = useUIStore((s) => s.currentPortSlug);
|
const portSlug = useUIStore((s) => s.currentPortSlug);
|
||||||
const [uploadSignedOpen, setUploadSignedOpen] = useState(false);
|
const [uploadSignedOpen, setUploadSignedOpen] = useState(false);
|
||||||
const [uploadForSigningOpen, setUploadForSigningOpen] = useState(false);
|
const [uploadForSigningOpen, setUploadForSigningOpen] = useState(false);
|
||||||
|
const [markExternalOpen, setMarkExternalOpen] = useState(false);
|
||||||
|
|
||||||
const { data: docsRes, isLoading: docsLoading } = useQuery<{ data: DocumentRow[] }>({
|
const { data: docsRes, isLoading: docsLoading } = useQuery<{ data: DocumentRow[] }>({
|
||||||
queryKey: ['documents', { interestId, documentType: 'contract' }],
|
queryKey: ['documents', { interestId, documentType: 'contract' }],
|
||||||
@@ -118,6 +120,7 @@ export function InterestContractTab({ interestId, clientId: _clientId }: Interes
|
|||||||
<EmptyContractState
|
<EmptyContractState
|
||||||
onUploadSigned={() => setUploadSignedOpen(true)}
|
onUploadSigned={() => setUploadSignedOpen(true)}
|
||||||
onUploadForSigning={() => setUploadForSigningOpen(true)}
|
onUploadForSigning={() => setUploadForSigningOpen(true)}
|
||||||
|
onMarkExternal={() => setMarkExternalOpen(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -181,6 +184,17 @@ export function InterestContractTab({ interestId, clientId: _clientId }: Interes
|
|||||||
documentType="contract"
|
documentType="contract"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* "Mark as signed externally" — flips the contract doc-status
|
||||||
|
to 'signed' without uploading a file. Used when the rep is
|
||||||
|
keeping the canonical copy elsewhere and just wants the CRM
|
||||||
|
state to reflect the close. */}
|
||||||
|
<MarkExternallySignedDialog
|
||||||
|
open={markExternalOpen}
|
||||||
|
onOpenChange={setMarkExternalOpen}
|
||||||
|
interestId={interestId}
|
||||||
|
docType="contract"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -336,9 +350,11 @@ function ActiveContractCard({
|
|||||||
function EmptyContractState({
|
function EmptyContractState({
|
||||||
onUploadSigned,
|
onUploadSigned,
|
||||||
onUploadForSigning,
|
onUploadForSigning,
|
||||||
|
onMarkExternal,
|
||||||
}: {
|
}: {
|
||||||
onUploadSigned: () => void;
|
onUploadSigned: () => void;
|
||||||
onUploadForSigning: () => void;
|
onUploadForSigning: () => void;
|
||||||
|
onMarkExternal: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="rounded-xl border border-dashed bg-muted/20 p-8 text-center">
|
<section className="rounded-xl border border-dashed bg-muted/20 p-8 text-center">
|
||||||
@@ -361,6 +377,9 @@ function EmptyContractState({
|
|||||||
<Upload className="size-4" aria-hidden />
|
<Upload className="size-4" aria-hidden />
|
||||||
Upload paper-signed copy
|
Upload paper-signed copy
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={onMarkExternal} variant="ghost" size="sm" className="gap-1.5">
|
||||||
|
Mark as signed without file
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { ExternalEoiUploadDialog } from '@/components/interests/external-eoi-upload-dialog';
|
import { ExternalEoiUploadDialog } from '@/components/interests/external-eoi-upload-dialog';
|
||||||
|
import { MarkExternallySignedDialog } from '@/components/interests/mark-externally-signed-dialog';
|
||||||
import { SigningProgress } from '@/components/documents/signing-progress';
|
import { SigningProgress } from '@/components/documents/signing-progress';
|
||||||
import { UploadForSigningDialog } from '@/components/documents/upload-for-signing-dialog';
|
import { UploadForSigningDialog } from '@/components/documents/upload-for-signing-dialog';
|
||||||
import { apiFetch } from '@/lib/api/client';
|
import { apiFetch } from '@/lib/api/client';
|
||||||
@@ -94,6 +95,7 @@ export function InterestReservationTab({
|
|||||||
const portSlug = useUIStore((s) => s.currentPortSlug);
|
const portSlug = useUIStore((s) => s.currentPortSlug);
|
||||||
const [uploadSignedOpen, setUploadSignedOpen] = useState(false);
|
const [uploadSignedOpen, setUploadSignedOpen] = useState(false);
|
||||||
const [uploadForSigningOpen, setUploadForSigningOpen] = useState(false);
|
const [uploadForSigningOpen, setUploadForSigningOpen] = useState(false);
|
||||||
|
const [markExternalOpen, setMarkExternalOpen] = useState(false);
|
||||||
|
|
||||||
const { data: docsRes, isLoading: docsLoading } = useQuery<{ data: DocumentRow[] }>({
|
const { data: docsRes, isLoading: docsLoading } = useQuery<{ data: DocumentRow[] }>({
|
||||||
queryKey: ['documents', { interestId, documentType: 'reservation_agreement' }],
|
queryKey: ['documents', { interestId, documentType: 'reservation_agreement' }],
|
||||||
@@ -119,6 +121,7 @@ export function InterestReservationTab({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyReservationState
|
<EmptyReservationState
|
||||||
|
onMarkExternal={() => setMarkExternalOpen(true)}
|
||||||
onUploadSigned={() => setUploadSignedOpen(true)}
|
onUploadSigned={() => setUploadSignedOpen(true)}
|
||||||
onUploadForSigning={() => setUploadForSigningOpen(true)}
|
onUploadForSigning={() => setUploadForSigningOpen(true)}
|
||||||
/>
|
/>
|
||||||
@@ -181,6 +184,13 @@ export function InterestReservationTab({
|
|||||||
documentType="reservation_agreement"
|
documentType="reservation_agreement"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<MarkExternallySignedDialog
|
||||||
|
open={markExternalOpen}
|
||||||
|
onOpenChange={setMarkExternalOpen}
|
||||||
|
interestId={interestId}
|
||||||
|
docType="reservation"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -336,9 +346,11 @@ function ActiveReservationCard({
|
|||||||
function EmptyReservationState({
|
function EmptyReservationState({
|
||||||
onUploadSigned,
|
onUploadSigned,
|
||||||
onUploadForSigning,
|
onUploadForSigning,
|
||||||
|
onMarkExternal,
|
||||||
}: {
|
}: {
|
||||||
onUploadSigned: () => void;
|
onUploadSigned: () => void;
|
||||||
onUploadForSigning: () => void;
|
onUploadForSigning: () => void;
|
||||||
|
onMarkExternal: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="rounded-xl border border-dashed bg-muted/20 p-8 text-center">
|
<section className="rounded-xl border border-dashed bg-muted/20 p-8 text-center">
|
||||||
@@ -361,6 +373,9 @@ function EmptyReservationState({
|
|||||||
<Upload className="size-4" aria-hidden />
|
<Upload className="size-4" aria-hidden />
|
||||||
Upload paper-signed copy
|
Upload paper-signed copy
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={onMarkExternal} variant="ghost" size="sm" className="gap-1.5">
|
||||||
|
Mark as signed without file
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
127
src/components/interests/mark-externally-signed-dialog.tsx
Normal file
127
src/components/interests/mark-externally-signed-dialog.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirms a "mark as signed externally without file" action on an
|
||||||
|
* interest's contract / reservation / EOI sub-status. Used by the
|
||||||
|
* relevant interest tabs when the rep has paper or a digital copy
|
||||||
|
* filed elsewhere and doesn't want to duplicate-store it in the CRM
|
||||||
|
* just to flip the doc-status forward.
|
||||||
|
*
|
||||||
|
* The action is destructive in the sense that downstream berth-rules
|
||||||
|
* fire (eoi_signed / contract_signed triggers), so a confirmation
|
||||||
|
* step + optional reason capture is required.
|
||||||
|
*/
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { apiFetch } from '@/lib/api/client';
|
||||||
|
import { toastError } from '@/lib/api/toast-error';
|
||||||
|
|
||||||
|
type DocType = 'eoi' | 'reservation' | 'contract';
|
||||||
|
|
||||||
|
const LABELS: Record<DocType, { title: string; description: string }> = {
|
||||||
|
eoi: {
|
||||||
|
title: 'Mark EOI as signed externally?',
|
||||||
|
description:
|
||||||
|
'Flips the EOI sub-status to "signed" without uploading a file. Audit log captures who marked it + your reason. If a file turns up later, you can still upload it via the regular flow.',
|
||||||
|
},
|
||||||
|
reservation: {
|
||||||
|
title: 'Mark reservation as signed externally?',
|
||||||
|
description:
|
||||||
|
'Flips the reservation sub-status to "signed" without uploading a file. Audit log captures who marked it + your reason.',
|
||||||
|
},
|
||||||
|
contract: {
|
||||||
|
title: 'Mark contract as signed externally?',
|
||||||
|
description:
|
||||||
|
'Flips the contract sub-status to "signed" without uploading a file. The contract-signed berth-rule fires automatically (you can disable in admin/berth-rules).',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MarkExternallySignedDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
interestId,
|
||||||
|
docType,
|
||||||
|
onSuccess,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (o: boolean) => void;
|
||||||
|
interestId: string;
|
||||||
|
docType: DocType;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}) {
|
||||||
|
const [reason, setReason] = useState('');
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const labels = LABELS[docType];
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
await apiFetch(`/api/v1/interests/${interestId}/mark-externally-signed`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { docType, reason: reason.trim() || undefined },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(`Marked ${docType} as signed externally`);
|
||||||
|
qc.invalidateQueries({ queryKey: ['interest', interestId] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['interests'] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['documents'] });
|
||||||
|
onOpenChange(false);
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
onError: (e) => toastError(e),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{labels.title}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>{labels.description}</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="mes-reason">Reason (optional)</Label>
|
||||||
|
<Textarea
|
||||||
|
id="mes-reason"
|
||||||
|
rows={3}
|
||||||
|
placeholder="e.g. paper signed in-office; scan filed in shared drive"
|
||||||
|
value={reason}
|
||||||
|
onChange={(e) => setReason(e.target.value.slice(0, 2000))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel disabled={mutation.isPending}>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
mutation.mutate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mutation.isPending ? (
|
||||||
|
<Loader2 className="size-4 animate-spin" aria-hidden />
|
||||||
|
) : (
|
||||||
|
'Mark as signed'
|
||||||
|
)}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
src/lib/services/external-signing.service.ts
Normal file
98
src/lib/services/external-signing.service.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* "Mark as signed externally" service — records that a contract /
|
||||||
|
* reservation / EOI was signed outside the CRM without requiring the
|
||||||
|
* rep to upload a file. The interest's doc-status flips to 'signed'
|
||||||
|
* and an audit-log entry captures the optional reason ("paper signed
|
||||||
|
* in-office", "scanned and emailed later", etc.).
|
||||||
|
*
|
||||||
|
* Distinct from `uploadExternallySignedEoi` which requires a PDF.
|
||||||
|
* Reps reach for this when the original lives elsewhere (a physical
|
||||||
|
* folder, the client's email, another system) and they don't want to
|
||||||
|
* duplicate-store it just to flip the CRM forward.
|
||||||
|
*
|
||||||
|
* The UI surfaces a warning banner on the relevant tab: "No file on
|
||||||
|
* record — signed externally". Reps can later upload the file via
|
||||||
|
* the existing external-upload dialog if a copy turns up.
|
||||||
|
*/
|
||||||
|
import { and, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
import { interests } from '@/lib/db/schema/interests';
|
||||||
|
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||||
|
import { NotFoundError, ValidationError } from '@/lib/errors';
|
||||||
|
import { evaluateRule } from '@/lib/services/berth-rules-engine';
|
||||||
|
import { emitToRoom } from '@/lib/socket/server';
|
||||||
|
|
||||||
|
export type ExternalSignedDocType = 'eoi' | 'reservation' | 'contract';
|
||||||
|
|
||||||
|
const STATUS_COLUMNS: Record<
|
||||||
|
ExternalSignedDocType,
|
||||||
|
'eoiDocStatus' | 'reservationDocStatus' | 'contractDocStatus'
|
||||||
|
> = {
|
||||||
|
eoi: 'eoiDocStatus',
|
||||||
|
reservation: 'reservationDocStatus',
|
||||||
|
contract: 'contractDocStatus',
|
||||||
|
};
|
||||||
|
|
||||||
|
const BERTH_RULE_TRIGGERS: Record<ExternalSignedDocType, 'eoi_signed' | 'contract_signed' | null> =
|
||||||
|
{
|
||||||
|
eoi: 'eoi_signed',
|
||||||
|
reservation: null,
|
||||||
|
contract: 'contract_signed',
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface MarkExternallySignedInput {
|
||||||
|
interestId: string;
|
||||||
|
portId: string;
|
||||||
|
docType: ExternalSignedDocType;
|
||||||
|
reason?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function markExternallySigned(
|
||||||
|
input: MarkExternallySignedInput,
|
||||||
|
meta: AuditMeta,
|
||||||
|
): Promise<{ docType: ExternalSignedDocType; previousStatus: string | null }> {
|
||||||
|
const interest = await db.query.interests.findFirst({
|
||||||
|
where: and(eq(interests.id, input.interestId), eq(interests.portId, input.portId)),
|
||||||
|
});
|
||||||
|
if (!interest) throw new NotFoundError('Interest');
|
||||||
|
|
||||||
|
const column = STATUS_COLUMNS[input.docType];
|
||||||
|
const previousStatus = (interest[column] ?? null) as string | null;
|
||||||
|
if (previousStatus === 'signed') {
|
||||||
|
throw new ValidationError(
|
||||||
|
`${input.docType} is already marked as signed for this interest. Use the upload flow to attach a file.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
await db
|
||||||
|
.update(interests)
|
||||||
|
.set({ [column]: 'signed', updatedAt: now })
|
||||||
|
.where(and(eq(interests.id, input.interestId), eq(interests.portId, input.portId)));
|
||||||
|
|
||||||
|
void createAuditLog({
|
||||||
|
userId: meta.userId,
|
||||||
|
portId: input.portId,
|
||||||
|
action: 'update',
|
||||||
|
entityType: 'interest',
|
||||||
|
entityId: input.interestId,
|
||||||
|
oldValue: { [column]: previousStatus },
|
||||||
|
newValue: { [column]: 'signed', reason: input.reason ?? null },
|
||||||
|
metadata: { type: 'externally_signed', docType: input.docType, withoutFile: true },
|
||||||
|
ipAddress: meta.ipAddress,
|
||||||
|
userAgent: meta.userAgent,
|
||||||
|
});
|
||||||
|
|
||||||
|
emitToRoom(`port:${input.portId}`, 'interest:updated', {
|
||||||
|
interestId: input.interestId,
|
||||||
|
changedFields: [column, 'updatedAt'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const trigger = BERTH_RULE_TRIGGERS[input.docType];
|
||||||
|
if (trigger) {
|
||||||
|
void evaluateRule(trigger, input.interestId, input.portId, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { docType: input.docType, previousStatus };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user