fix(signing): route paper-signed reservation/contract uploads to the right doc type
The Reservation and Contract tabs reused ExternalEoiUploadDialog, but the service hard-coded the EOI document type, status columns, stage target, and berth rule. A signed contract uploaded from the Contract tab filed as an `eoi`, flipped `eoi_status`, and advanced the stage to `eoi` - wrong doc kind, wrong sub-state, wrong stage. - external-eoi.service: UPLOAD_CONFIG keyed off docType (eoi | reservation | contract) parameterises documentType, file category, storage prefix, doc-status column, signed-date column, target stage, advance-from set, and berth rule. eoi_status is written only for docType=eoi. - route: parse docType from the form (default eoi). - dialog: docType prop; generalised copy; EOI-only UI (active-EOI replace banner, public-map flip, cancelActiveDocumentId) gated to docType=eoi. - reservation/contract tabs: pass docType; drop the coming-soon comments. - test: docType routing cases (reservation -> reservation_agreement + reservation cols; contract -> contract + contract cols; eoi_status stays null on both; contract idempotent at/past contract stage). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,10 @@ interface Props {
|
||||
* from the active Documenso EOI's signers). Falls through to the
|
||||
* client-only seed when omitted or empty. */
|
||||
prefillSignatories?: SignatoryRow[];
|
||||
/** Which signed document this upload is for. Defaults to 'eoi'; the
|
||||
* reservation/contract tabs pass their own type so the file is filed +
|
||||
* the deal advanced under the right kind (EOI-specific UI is hidden). */
|
||||
docType?: 'eoi' | 'reservation' | 'contract';
|
||||
}
|
||||
|
||||
export function ExternalEoiUploadDialog({
|
||||
@@ -63,7 +67,15 @@ export function ExternalEoiUploadDialog({
|
||||
interestId,
|
||||
onSuccess,
|
||||
prefillSignatories,
|
||||
docType = 'eoi',
|
||||
}: Props) {
|
||||
const DOC_LABEL =
|
||||
docType === 'reservation'
|
||||
? 'reservation agreement'
|
||||
: docType === 'contract'
|
||||
? 'contract'
|
||||
: 'EOI';
|
||||
const isEoi = docType === 'eoi';
|
||||
const qc = useQueryClient();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [title, setTitle] = useState('');
|
||||
@@ -173,7 +185,7 @@ export function ExternalEoiUploadDialog({
|
||||
apiFetch<{
|
||||
data: Array<{ id: string; status: string; title: string; createdAt: string }>;
|
||||
}>(`/api/v1/documents?interestId=${interestId}&documentType=eoi`),
|
||||
enabled: open,
|
||||
enabled: open && isEoi,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
const activeEoi = useMemo(
|
||||
@@ -191,12 +203,12 @@ export function ExternalEoiUploadDialog({
|
||||
.filter((m): m is string => !!m);
|
||||
const berthLabel = moorings.length > 0 ? formatBerthRange(moorings) : null;
|
||||
const clientName = interestData?.clientName ?? null;
|
||||
const parts = ['External EOI'];
|
||||
const parts = [`External ${DOC_LABEL}`];
|
||||
if (clientName) parts.push(clientName);
|
||||
if (berthLabel) parts.push(berthLabel);
|
||||
parts.push(date);
|
||||
return parts.join(' - ');
|
||||
}, [interestData, berthsData, signedAt]);
|
||||
}, [interestData, berthsData, signedAt, DOC_LABEL]);
|
||||
|
||||
// The title input is controlled with `displayTitle` (derived from
|
||||
// either the rep's typed value or the auto-derived default). Reps
|
||||
@@ -226,7 +238,8 @@ export function ExternalEoiUploadDialog({
|
||||
// When a generated EOI is active AND the rep didn't opt out via the
|
||||
// Advanced toggle, tell the server to cancel it as part of this
|
||||
// upload so the deal carries one canonical EOI.
|
||||
if (activeEoi && replaceMode === 'replace') {
|
||||
form.append('docType', docType);
|
||||
if (isEoi && activeEoi && replaceMode === 'replace') {
|
||||
form.append('cancelActiveDocumentId', activeEoi.id);
|
||||
}
|
||||
const res = await fetch(`/api/v1/interests/${interestId}/external-eoi`, {
|
||||
@@ -250,7 +263,7 @@ export function ExternalEoiUploadDialog({
|
||||
// sync are skipped. Failures here don't undo the upload (the doc
|
||||
// is already filed) but surface as a non-blocking toast so the
|
||||
// rep knows the flag didn't propagate.
|
||||
if (inBundleBerths.length > 0) {
|
||||
if (isEoi && inBundleBerths.length > 0) {
|
||||
const targets = inBundleBerths.filter((b) => b.isSpecificInterest !== publicFlagChecked);
|
||||
if (targets.length > 0) {
|
||||
try {
|
||||
@@ -270,8 +283,8 @@ export function ExternalEoiUploadDialog({
|
||||
}
|
||||
toast.success(
|
||||
stageChanged
|
||||
? 'External EOI uploaded. Stage advanced to EOI Signed.'
|
||||
: 'External EOI uploaded. Filed against this deal (stage unchanged).',
|
||||
? `Signed ${DOC_LABEL} uploaded. Pipeline stage advanced.`
|
||||
: `Signed ${DOC_LABEL} uploaded. Filed against this deal (stage unchanged).`,
|
||||
);
|
||||
qc.invalidateQueries({ queryKey: ['interests', interestId] });
|
||||
qc.invalidateQueries({ queryKey: ['interests'] });
|
||||
@@ -293,16 +306,16 @@ export function ExternalEoiUploadDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-2xl lg:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Upload externally-signed EOI</DialogTitle>
|
||||
<DialogTitle>Upload externally-signed {DOC_LABEL}</DialogTitle>
|
||||
<DialogDescription>
|
||||
For EOIs signed outside our signing service (paper, in person, alternate e-sign vendor).
|
||||
The uploaded PDF is filed against this interest and the pipeline stage is advanced to
|
||||
EOI Signed.
|
||||
For a {DOC_LABEL} signed outside our signing service (paper, in person, alternate e-sign
|
||||
vendor). The uploaded PDF is filed against this interest and the pipeline stage is
|
||||
advanced accordingly.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 py-2">
|
||||
{activeEoi ? (
|
||||
{isEoi && activeEoi ? (
|
||||
<div className="rounded-md border border-amber-300 bg-amber-50 p-3 text-sm dark:border-amber-900 dark:bg-amber-950/40">
|
||||
<p className="font-medium text-amber-900 dark:text-amber-100">
|
||||
A generated EOI is already in flight on this deal.
|
||||
@@ -466,7 +479,7 @@ export function ExternalEoiUploadDialog({
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
{inBundleBerths.length > 0 ? (
|
||||
{isEoi && inBundleBerths.length > 0 ? (
|
||||
<div className="rounded-md border bg-muted/40 p-3 text-sm">
|
||||
<label className="flex cursor-pointer items-start gap-2">
|
||||
<input
|
||||
|
||||
@@ -167,17 +167,17 @@ export function InterestContractTab({ interestId, clientId: _clientId }: Interes
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Reuses the external-EOI upload dialog. The endpoint
|
||||
`/api/v1/interests/{id}/external-eoi` is EOI-specific today
|
||||
- for contract paper-uploads we'll need the equivalent
|
||||
contract endpoint (deferred to a follow-up; the dialog UI
|
||||
is the pattern we'll clone). For now the flow is documented
|
||||
as 'coming soon' rather than misrouting through EOI. */}
|
||||
{/* Shared upload dialog, parameterised by docType. With
|
||||
docType="contract" the service files the PDF as a contract,
|
||||
sets contractDocStatus='signed' + contractSignedAt, advances
|
||||
the stage to `contract`, and fires the contract_signed berth
|
||||
rule (no EOI-specific banner / public-map flip). */}
|
||||
{uploadSignedOpen && (
|
||||
<ExternalEoiUploadDialog
|
||||
open={uploadSignedOpen}
|
||||
onOpenChange={setUploadSignedOpen}
|
||||
interestId={interestId}
|
||||
docType="contract"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -170,17 +170,17 @@ export function InterestReservationTab({
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Reuses the external-EOI upload dialog. The endpoint
|
||||
`/api/v1/interests/{id}/external-eoi` is EOI-specific today
|
||||
- for reservation paper-uploads we'll need the equivalent
|
||||
reservation endpoint (deferred to a follow-up; the dialog UI
|
||||
is the pattern we'll clone). For now the flow is documented
|
||||
as 'coming soon' rather than misrouting through EOI. */}
|
||||
{/* Shared upload dialog, parameterised by docType. With
|
||||
docType="reservation" the service files the PDF as a
|
||||
reservation_agreement, sets reservationDocStatus='signed' +
|
||||
reservationSignedAt, and advances the stage to `reservation`
|
||||
(no EOI-specific banner / public-map flip). */}
|
||||
{uploadSignedOpen && (
|
||||
<ExternalEoiUploadDialog
|
||||
open={uploadSignedOpen}
|
||||
onOpenChange={setUploadSignedOpen}
|
||||
interestId={interestId}
|
||||
docType="reservation"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user