feat(client-archive): async Documenso voids + next-in-line sales notifications
Post-archive side-effects now run with backpressure: - Documenso envelope voids enqueue to BullMQ documents queue with retry/DLQ - Released berths fan out a "next in line" notification to port users with interests.change_stage; informational only, no auto stage transitions Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,9 @@ import {
|
||||
type ArchiveDecisions,
|
||||
} from '@/lib/services/client-archive.service';
|
||||
import { getClientArchiveDossier } from '@/lib/services/client-archive-dossier.service';
|
||||
import { notifyNextInLine } from '@/lib/services/next-in-line-notify.service';
|
||||
import { getQueue } from '@/lib/queue';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { errorResponse, NotFoundError } from '@/lib/errors';
|
||||
|
||||
const decisionsSchema = z.object({
|
||||
@@ -80,11 +83,54 @@ export const POST = withAuth(
|
||||
},
|
||||
});
|
||||
|
||||
// External cleanups (Documenso void) + next-in-line notifications
|
||||
// are queued post-commit. v1 fires them best-effort inline; future
|
||||
// iteration: enqueue to BullMQ for retry/dead-letter (see
|
||||
// bulletproof-webhooks design).
|
||||
// TODO(bulletproof-webhooks): move to queue.
|
||||
// ─── Post-commit side-effects ────────────────────────────────────
|
||||
// 1. Documenso envelope voids → queued for retry on the documents
|
||||
// queue. Permanently-failed jobs land in BullMQ's failed jobs
|
||||
// list (the DLQ in admin/monitoring).
|
||||
if (result.externalCleanups.length > 0) {
|
||||
const queue = getQueue('documents');
|
||||
for (const c of result.externalCleanups) {
|
||||
if (c.kind === 'documenso_void') {
|
||||
await queue
|
||||
.add('documenso-void', {
|
||||
documentId: c.documentId,
|
||||
documensoId: c.documensoId,
|
||||
portId: ctx.portId,
|
||||
})
|
||||
.catch((err) =>
|
||||
logger.error({ err, documentId: c.documentId }, 'Failed to enqueue Documenso void'),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Next-in-line notifications → fire-and-forget per released
|
||||
// berth so the sales team knows who else expressed interest.
|
||||
// No automatic stage transitions on the next interests; sales
|
||||
// rep decides what to do.
|
||||
for (const released of result.releasedBerths) {
|
||||
if (released.nextInLineInterestIds.length === 0) continue;
|
||||
const otherInterests =
|
||||
dossier.berths
|
||||
.find((b) => b.berthId === released.berthId)
|
||||
?.otherInterests.map((o) => ({
|
||||
interestId: o.interestId,
|
||||
clientName: o.clientName,
|
||||
pipelineStage: o.pipelineStage,
|
||||
})) ?? [];
|
||||
void notifyNextInLine({
|
||||
portId: ctx.portId,
|
||||
berthId: released.berthId,
|
||||
mooringNumber: released.mooringNumber,
|
||||
archivedClientName: dossier.client.fullName,
|
||||
nextInLineInterests: otherInterests,
|
||||
}).catch((err) =>
|
||||
logger.error(
|
||||
{ err, berthId: released.berthId },
|
||||
'Failed to fire next-in-line notification',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ data: result });
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user