Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { and, desc, eq } from 'drizzle-orm';
|
|
|
|
|
|
|
|
|
|
import { db } from '@/lib/db';
|
|
|
|
|
import { generatedReports } from '@/lib/db/schema/operations';
|
|
|
|
|
import { notifications } from '@/lib/db/schema/operations';
|
|
|
|
|
import { files } from '@/lib/db/schema/documents';
|
|
|
|
|
import { ports } from '@/lib/db/schema/ports';
|
|
|
|
|
import { generatePdf } from '@/lib/pdf/generate';
|
fix(storage): route every file op through getStorageBackend()
Removes 12 direct minioClient.{put,get,remove}Object call sites that
bypassed the pluggable storage abstraction. Filesystem-mode deploys
(MULTI_NODE_DEPLOYMENT=false, storage_backend=filesystem) silently
broke at every site: GDPR export, invoice PDF, EOI generation, portal
download, file upload, folder create/rename/delete, signed PDF land,
maintenance cleanup, etc. Each site now resolves the active backend
and uses its put/get/delete + the new presignDownloadUrl() helper.
Folder marker objects in /files/folders/* keep the same on-the-wire
shape but route through the backend. A future refactor should move
folder bookkeeping to a DB-backed virtual-folder table (see audit
HIGH §3 follow-up note in the route file).
Sites left untouched: src/lib/services/system-monitoring.service.ts
and src/app/api/ready/route.ts use minioClient.bucketExists as an S3-
specific health probe — those are correctly mode-aware and stay.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §3 (auditor-D Issue 1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:41:02 +02:00
|
|
|
import { buildStoragePath } from '@/lib/minio/index';
|
|
|
|
|
import { getStorageBackend, presignDownloadUrl } from '@/lib/storage';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { emitToRoom } from '@/lib/socket/server';
|
|
|
|
|
import { getQueue } from '@/lib/queue';
|
|
|
|
|
import { env } from '@/lib/env';
|
|
|
|
|
import { logger } from '@/lib/logger';
|
fix(audit-tier-2): error-surface hygiene — toastError + CodedError sweep
Two mechanical sweeps closing the audit's HIGH §16 + MED §11 findings:
* 38 client components / 56 toast.error sites converted to
toastError(err) so the new admin error inspector becomes usable from
user-reported issues — every failed inline-edit, save, send, archive,
upload, etc. now carries the request-id + error-code (Copy ID action).
* 26 service files / 62 bare-Error throws converted to CodedError or
the existing AppError subclasses. Adds new error codes:
DOCUMENSO_UPSTREAM_ERROR (502), DOCUMENSO_AUTH_FAILURE (502),
DOCUMENSO_TIMEOUT (504), OCR_UPSTREAM_ERROR (502),
IMAP_UPSTREAM_ERROR (502), UMAMI_UPSTREAM_ERROR (502),
UMAMI_NOT_CONFIGURED (409), and INSERT_RETURNING_EMPTY (500) for
post-insert returning-empty guards.
* Five vitest assertions updated to match the new user-facing wording
(client-merge "already been merged", expense/interest "couldn't find
that …", documenso "signing service didn't respond").
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §16 (auditor-H Issue 1)
+ MED §11 (auditor-G Issue 1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:18:05 +02:00
|
|
|
import { CodedError, ConflictError, NotFoundError } from '@/lib/errors';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
fetchPipelineData,
|
|
|
|
|
fetchRevenueData,
|
|
|
|
|
fetchActivityData,
|
|
|
|
|
fetchOccupancyData,
|
|
|
|
|
} from '@/lib/services/report-generators';
|
|
|
|
|
import {
|
|
|
|
|
pipelineReportTemplate,
|
|
|
|
|
buildPipelineInputs,
|
|
|
|
|
} from '@/lib/pdf/templates/reports/pipeline-report';
|
|
|
|
|
import {
|
|
|
|
|
revenueReportTemplate,
|
|
|
|
|
buildRevenueInputs,
|
|
|
|
|
} from '@/lib/pdf/templates/reports/revenue-report';
|
|
|
|
|
import {
|
|
|
|
|
activityReportTemplate,
|
|
|
|
|
buildActivityInputs,
|
|
|
|
|
} from '@/lib/pdf/templates/reports/activity-report';
|
|
|
|
|
import {
|
|
|
|
|
occupancyReportTemplate,
|
|
|
|
|
buildOccupancyInputs,
|
|
|
|
|
} from '@/lib/pdf/templates/reports/occupancy-report';
|
|
|
|
|
|
|
|
|
|
import type { RequestReportInput, ListReportsInput } from '@/lib/validators/reports';
|
|
|
|
|
|
|
|
|
|
// ─── Report Type Map ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const REPORT_TYPE_MAP = {
|
|
|
|
|
pipeline: {
|
|
|
|
|
fetchData: fetchPipelineData,
|
|
|
|
|
template: pipelineReportTemplate,
|
|
|
|
|
buildInputs: buildPipelineInputs,
|
|
|
|
|
},
|
|
|
|
|
revenue: {
|
|
|
|
|
fetchData: fetchRevenueData,
|
|
|
|
|
template: revenueReportTemplate,
|
|
|
|
|
buildInputs: buildRevenueInputs,
|
|
|
|
|
},
|
|
|
|
|
activity: {
|
|
|
|
|
fetchData: fetchActivityData,
|
|
|
|
|
template: activityReportTemplate,
|
|
|
|
|
buildInputs: buildActivityInputs,
|
|
|
|
|
},
|
|
|
|
|
occupancy: {
|
|
|
|
|
fetchData: fetchOccupancyData,
|
|
|
|
|
template: occupancyReportTemplate,
|
|
|
|
|
buildInputs: buildOccupancyInputs,
|
|
|
|
|
},
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
type ReportType = keyof typeof REPORT_TYPE_MAP;
|
|
|
|
|
|
|
|
|
|
// ─── requestReport ────────────────────────────────────────────────────────────
|
|
|
|
|
|
fix(storage): route every file op through getStorageBackend()
Removes 12 direct minioClient.{put,get,remove}Object call sites that
bypassed the pluggable storage abstraction. Filesystem-mode deploys
(MULTI_NODE_DEPLOYMENT=false, storage_backend=filesystem) silently
broke at every site: GDPR export, invoice PDF, EOI generation, portal
download, file upload, folder create/rename/delete, signed PDF land,
maintenance cleanup, etc. Each site now resolves the active backend
and uses its put/get/delete + the new presignDownloadUrl() helper.
Folder marker objects in /files/folders/* keep the same on-the-wire
shape but route through the backend. A future refactor should move
folder bookkeeping to a DB-backed virtual-folder table (see audit
HIGH §3 follow-up note in the route file).
Sites left untouched: src/lib/services/system-monitoring.service.ts
and src/app/api/ready/route.ts use minioClient.bucketExists as an S3-
specific health probe — those are correctly mode-aware and stay.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §3 (auditor-D Issue 1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:41:02 +02:00
|
|
|
export async function requestReport(portId: string, userId: string, data: RequestReportInput) {
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
const [report] = await db
|
|
|
|
|
.insert(generatedReports)
|
|
|
|
|
.values({
|
|
|
|
|
portId,
|
|
|
|
|
reportType: data.reportType,
|
|
|
|
|
name: data.name,
|
|
|
|
|
status: 'queued',
|
|
|
|
|
parameters: data.parameters ?? {},
|
|
|
|
|
requestedBy: userId,
|
|
|
|
|
})
|
|
|
|
|
.returning();
|
|
|
|
|
|
|
|
|
|
if (!report) {
|
fix(audit-tier-2): error-surface hygiene — toastError + CodedError sweep
Two mechanical sweeps closing the audit's HIGH §16 + MED §11 findings:
* 38 client components / 56 toast.error sites converted to
toastError(err) so the new admin error inspector becomes usable from
user-reported issues — every failed inline-edit, save, send, archive,
upload, etc. now carries the request-id + error-code (Copy ID action).
* 26 service files / 62 bare-Error throws converted to CodedError or
the existing AppError subclasses. Adds new error codes:
DOCUMENSO_UPSTREAM_ERROR (502), DOCUMENSO_AUTH_FAILURE (502),
DOCUMENSO_TIMEOUT (504), OCR_UPSTREAM_ERROR (502),
IMAP_UPSTREAM_ERROR (502), UMAMI_UPSTREAM_ERROR (502),
UMAMI_NOT_CONFIGURED (409), and INSERT_RETURNING_EMPTY (500) for
post-insert returning-empty guards.
* Five vitest assertions updated to match the new user-facing wording
(client-merge "already been merged", expense/interest "couldn't find
that …", documenso "signing service didn't respond").
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §16 (auditor-H Issue 1)
+ MED §11 (auditor-G Issue 1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:18:05 +02:00
|
|
|
throw new CodedError('INSERT_RETURNING_EMPTY', {
|
|
|
|
|
internalMessage: 'Failed to create report record',
|
|
|
|
|
});
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await getQueue('reports').add('generate-report', { reportJobId: report.id });
|
|
|
|
|
|
|
|
|
|
emitToRoom(`user:${userId}`, 'report:queued', {
|
|
|
|
|
reportId: report.id,
|
|
|
|
|
reportType: report.reportType,
|
|
|
|
|
name: report.name,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return report;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── listReports ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export async function listReports(portId: string, query: ListReportsInput) {
|
|
|
|
|
const conditions = [eq(generatedReports.portId, portId)];
|
|
|
|
|
if (query.status) {
|
|
|
|
|
conditions.push(eq(generatedReports.status, query.status));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const offset = (query.page - 1) * query.limit;
|
|
|
|
|
|
|
|
|
|
const [rows, countResult] = await Promise.all([
|
|
|
|
|
db
|
|
|
|
|
.select()
|
|
|
|
|
.from(generatedReports)
|
|
|
|
|
.where(and(...conditions))
|
|
|
|
|
.orderBy(desc(generatedReports.createdAt))
|
|
|
|
|
.limit(query.limit)
|
|
|
|
|
.offset(offset),
|
|
|
|
|
db.$count(generatedReports, and(...conditions)),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
data: rows,
|
|
|
|
|
total: Number(countResult),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── getReport ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export async function getReport(reportId: string, portId: string) {
|
|
|
|
|
const report = await db.query.generatedReports.findFirst({
|
|
|
|
|
where: and(eq(generatedReports.id, reportId), eq(generatedReports.portId, portId)),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!report) {
|
|
|
|
|
throw new NotFoundError('Report');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return report;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── getDownloadUrl ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export async function getDownloadUrl(reportId: string, portId: string) {
|
|
|
|
|
const report = await db.query.generatedReports.findFirst({
|
|
|
|
|
where: and(eq(generatedReports.id, reportId), eq(generatedReports.portId, portId)),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!report) {
|
|
|
|
|
throw new NotFoundError('Report');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (report.status !== 'ready' || !report.fileId) {
|
fix(audit-tier-2): error-surface hygiene — toastError + CodedError sweep
Two mechanical sweeps closing the audit's HIGH §16 + MED §11 findings:
* 38 client components / 56 toast.error sites converted to
toastError(err) so the new admin error inspector becomes usable from
user-reported issues — every failed inline-edit, save, send, archive,
upload, etc. now carries the request-id + error-code (Copy ID action).
* 26 service files / 62 bare-Error throws converted to CodedError or
the existing AppError subclasses. Adds new error codes:
DOCUMENSO_UPSTREAM_ERROR (502), DOCUMENSO_AUTH_FAILURE (502),
DOCUMENSO_TIMEOUT (504), OCR_UPSTREAM_ERROR (502),
IMAP_UPSTREAM_ERROR (502), UMAMI_UPSTREAM_ERROR (502),
UMAMI_NOT_CONFIGURED (409), and INSERT_RETURNING_EMPTY (500) for
post-insert returning-empty guards.
* Five vitest assertions updated to match the new user-facing wording
(client-merge "already been merged", expense/interest "couldn't find
that …", documenso "signing service didn't respond").
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §16 (auditor-H Issue 1)
+ MED §11 (auditor-G Issue 1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:18:05 +02:00
|
|
|
throw new ConflictError('Report is not ready for download');
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const file = await db.query.files.findFirst({
|
|
|
|
|
where: eq(files.id, report.fileId),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!file) {
|
|
|
|
|
throw new NotFoundError('File');
|
|
|
|
|
}
|
|
|
|
|
|
fix(storage): route every file op through getStorageBackend()
Removes 12 direct minioClient.{put,get,remove}Object call sites that
bypassed the pluggable storage abstraction. Filesystem-mode deploys
(MULTI_NODE_DEPLOYMENT=false, storage_backend=filesystem) silently
broke at every site: GDPR export, invoice PDF, EOI generation, portal
download, file upload, folder create/rename/delete, signed PDF land,
maintenance cleanup, etc. Each site now resolves the active backend
and uses its put/get/delete + the new presignDownloadUrl() helper.
Folder marker objects in /files/folders/* keep the same on-the-wire
shape but route through the backend. A future refactor should move
folder bookkeeping to a DB-backed virtual-folder table (see audit
HIGH §3 follow-up note in the route file).
Sites left untouched: src/lib/services/system-monitoring.service.ts
and src/app/api/ready/route.ts use minioClient.bucketExists as an S3-
specific health probe — those are correctly mode-aware and stay.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §3 (auditor-D Issue 1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:41:02 +02:00
|
|
|
const url = await presignDownloadUrl(file.storagePath);
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
return { url };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── generateReport ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export async function generateReport(reportJobId: string): Promise<void> {
|
|
|
|
|
// 1. Fetch the generatedReports record
|
|
|
|
|
const report = await db.query.generatedReports.findFirst({
|
|
|
|
|
where: eq(generatedReports.id, reportJobId),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!report) {
|
fix(audit-tier-2): error-surface hygiene — toastError + CodedError sweep
Two mechanical sweeps closing the audit's HIGH §16 + MED §11 findings:
* 38 client components / 56 toast.error sites converted to
toastError(err) so the new admin error inspector becomes usable from
user-reported issues — every failed inline-edit, save, send, archive,
upload, etc. now carries the request-id + error-code (Copy ID action).
* 26 service files / 62 bare-Error throws converted to CodedError or
the existing AppError subclasses. Adds new error codes:
DOCUMENSO_UPSTREAM_ERROR (502), DOCUMENSO_AUTH_FAILURE (502),
DOCUMENSO_TIMEOUT (504), OCR_UPSTREAM_ERROR (502),
IMAP_UPSTREAM_ERROR (502), UMAMI_UPSTREAM_ERROR (502),
UMAMI_NOT_CONFIGURED (409), and INSERT_RETURNING_EMPTY (500) for
post-insert returning-empty guards.
* Five vitest assertions updated to match the new user-facing wording
(client-merge "already been merged", expense/interest "couldn't find
that …", documenso "signing service didn't respond").
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §16 (auditor-H Issue 1)
+ MED §11 (auditor-G Issue 1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:18:05 +02:00
|
|
|
throw new NotFoundError('report job');
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { portId, reportType, name, parameters, requestedBy } = report;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 2. Update status = 'processing', startedAt = now
|
|
|
|
|
await db
|
|
|
|
|
.update(generatedReports)
|
|
|
|
|
.set({ status: 'processing', startedAt: new Date() })
|
|
|
|
|
.where(eq(generatedReports.id, reportJobId));
|
|
|
|
|
|
|
|
|
|
// 3. Look up REPORT_TYPE_MAP[reportType]
|
|
|
|
|
const typeKey = reportType as ReportType;
|
|
|
|
|
const config = REPORT_TYPE_MAP[typeKey];
|
|
|
|
|
if (!config) {
|
fix(audit-tier-2): error-surface hygiene — toastError + CodedError sweep
Two mechanical sweeps closing the audit's HIGH §16 + MED §11 findings:
* 38 client components / 56 toast.error sites converted to
toastError(err) so the new admin error inspector becomes usable from
user-reported issues — every failed inline-edit, save, send, archive,
upload, etc. now carries the request-id + error-code (Copy ID action).
* 26 service files / 62 bare-Error throws converted to CodedError or
the existing AppError subclasses. Adds new error codes:
DOCUMENSO_UPSTREAM_ERROR (502), DOCUMENSO_AUTH_FAILURE (502),
DOCUMENSO_TIMEOUT (504), OCR_UPSTREAM_ERROR (502),
IMAP_UPSTREAM_ERROR (502), UMAMI_UPSTREAM_ERROR (502),
UMAMI_NOT_CONFIGURED (409), and INSERT_RETURNING_EMPTY (500) for
post-insert returning-empty guards.
* Five vitest assertions updated to match the new user-facing wording
(client-merge "already been merged", expense/interest "couldn't find
that …", documenso "signing service didn't respond").
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §16 (auditor-H Issue 1)
+ MED §11 (auditor-G Issue 1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:18:05 +02:00
|
|
|
throw new CodedError('VALIDATION_ERROR', {
|
|
|
|
|
internalMessage: `Unknown report type: ${reportType}`,
|
|
|
|
|
});
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const params = (parameters ?? {}) as Record<string, unknown>;
|
|
|
|
|
|
|
|
|
|
// 4. Fetch data
|
|
|
|
|
const data = await config.fetchData(portId, params);
|
|
|
|
|
|
|
|
|
|
// 5. Get port info for name in PDF
|
|
|
|
|
const port = await db.query.ports.findFirst({
|
|
|
|
|
where: eq(ports.id, portId),
|
|
|
|
|
});
|
|
|
|
|
const portName = port?.name ?? 'Port Nimara';
|
|
|
|
|
const portSlug = port?.slug ?? 'port';
|
|
|
|
|
|
|
|
|
|
// 6. Build inputs (pass portName)
|
fix(storage): route every file op through getStorageBackend()
Removes 12 direct minioClient.{put,get,remove}Object call sites that
bypassed the pluggable storage abstraction. Filesystem-mode deploys
(MULTI_NODE_DEPLOYMENT=false, storage_backend=filesystem) silently
broke at every site: GDPR export, invoice PDF, EOI generation, portal
download, file upload, folder create/rename/delete, signed PDF land,
maintenance cleanup, etc. Each site now resolves the active backend
and uses its put/get/delete + the new presignDownloadUrl() helper.
Folder marker objects in /files/folders/* keep the same on-the-wire
shape but route through the backend. A future refactor should move
folder bookkeeping to a DB-backed virtual-folder table (see audit
HIGH §3 follow-up note in the route file).
Sites left untouched: src/lib/services/system-monitoring.service.ts
and src/app/api/ready/route.ts use minioClient.bucketExists as an S3-
specific health probe — those are correctly mode-aware and stay.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §3 (auditor-D Issue 1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:41:02 +02:00
|
|
|
const inputs = (
|
|
|
|
|
config.buildInputs as (data: unknown, portName: string) => Record<string, string>[]
|
|
|
|
|
)(data, portName);
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
// 7. Generate PDF
|
|
|
|
|
const pdfBytes = await generatePdf(config.template, inputs);
|
|
|
|
|
|
|
|
|
|
// 8. Build storage path
|
|
|
|
|
const fileId = crypto.randomUUID();
|
|
|
|
|
const storagePath = buildStoragePath(portSlug, 'reports', reportJobId, fileId, 'pdf');
|
|
|
|
|
|
fix(storage): route every file op through getStorageBackend()
Removes 12 direct minioClient.{put,get,remove}Object call sites that
bypassed the pluggable storage abstraction. Filesystem-mode deploys
(MULTI_NODE_DEPLOYMENT=false, storage_backend=filesystem) silently
broke at every site: GDPR export, invoice PDF, EOI generation, portal
download, file upload, folder create/rename/delete, signed PDF land,
maintenance cleanup, etc. Each site now resolves the active backend
and uses its put/get/delete + the new presignDownloadUrl() helper.
Folder marker objects in /files/folders/* keep the same on-the-wire
shape but route through the backend. A future refactor should move
folder bookkeeping to a DB-backed virtual-folder table (see audit
HIGH §3 follow-up note in the route file).
Sites left untouched: src/lib/services/system-monitoring.service.ts
and src/app/api/ready/route.ts use minioClient.bucketExists as an S3-
specific health probe — those are correctly mode-aware and stay.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §3 (auditor-D Issue 1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:41:02 +02:00
|
|
|
// 9. Upload PDF via the active storage backend (filesystem or s3)
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
const buffer = Buffer.from(pdfBytes);
|
fix(storage): route every file op through getStorageBackend()
Removes 12 direct minioClient.{put,get,remove}Object call sites that
bypassed the pluggable storage abstraction. Filesystem-mode deploys
(MULTI_NODE_DEPLOYMENT=false, storage_backend=filesystem) silently
broke at every site: GDPR export, invoice PDF, EOI generation, portal
download, file upload, folder create/rename/delete, signed PDF land,
maintenance cleanup, etc. Each site now resolves the active backend
and uses its put/get/delete + the new presignDownloadUrl() helper.
Folder marker objects in /files/folders/* keep the same on-the-wire
shape but route through the backend. A future refactor should move
folder bookkeeping to a DB-backed virtual-folder table (see audit
HIGH §3 follow-up note in the route file).
Sites left untouched: src/lib/services/system-monitoring.service.ts
and src/app/api/ready/route.ts use minioClient.bucketExists as an S3-
specific health probe — those are correctly mode-aware and stay.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §3 (auditor-D Issue 1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:41:02 +02:00
|
|
|
const backend = await getStorageBackend();
|
|
|
|
|
await backend.put(storagePath, buffer, {
|
|
|
|
|
contentType: 'application/pdf',
|
|
|
|
|
sizeBytes: buffer.length,
|
|
|
|
|
});
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
// 10. Insert into files table
|
|
|
|
|
const [fileRecord] = await db
|
|
|
|
|
.insert(files)
|
|
|
|
|
.values({
|
|
|
|
|
id: fileId,
|
|
|
|
|
portId,
|
|
|
|
|
filename: `${name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_${Date.now()}.pdf`,
|
|
|
|
|
originalName: `${name}.pdf`,
|
|
|
|
|
mimeType: 'application/pdf',
|
|
|
|
|
sizeBytes: String(buffer.length),
|
|
|
|
|
storagePath,
|
|
|
|
|
storageBucket: env.MINIO_BUCKET,
|
|
|
|
|
category: 'misc',
|
|
|
|
|
uploadedBy: requestedBy,
|
|
|
|
|
})
|
|
|
|
|
.returning();
|
|
|
|
|
|
|
|
|
|
if (!fileRecord) {
|
fix(audit-tier-2): error-surface hygiene — toastError + CodedError sweep
Two mechanical sweeps closing the audit's HIGH §16 + MED §11 findings:
* 38 client components / 56 toast.error sites converted to
toastError(err) so the new admin error inspector becomes usable from
user-reported issues — every failed inline-edit, save, send, archive,
upload, etc. now carries the request-id + error-code (Copy ID action).
* 26 service files / 62 bare-Error throws converted to CodedError or
the existing AppError subclasses. Adds new error codes:
DOCUMENSO_UPSTREAM_ERROR (502), DOCUMENSO_AUTH_FAILURE (502),
DOCUMENSO_TIMEOUT (504), OCR_UPSTREAM_ERROR (502),
IMAP_UPSTREAM_ERROR (502), UMAMI_UPSTREAM_ERROR (502),
UMAMI_NOT_CONFIGURED (409), and INSERT_RETURNING_EMPTY (500) for
post-insert returning-empty guards.
* Five vitest assertions updated to match the new user-facing wording
(client-merge "already been merged", expense/interest "couldn't find
that …", documenso "signing service didn't respond").
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §16 (auditor-H Issue 1)
+ MED §11 (auditor-G Issue 1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:18:05 +02:00
|
|
|
throw new CodedError('INSERT_RETURNING_EMPTY', {
|
|
|
|
|
internalMessage: 'Failed to insert file record for generated report',
|
|
|
|
|
});
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 11. Update generatedReports: status='ready', fileId, completedAt
|
|
|
|
|
await db
|
|
|
|
|
.update(generatedReports)
|
|
|
|
|
.set({
|
|
|
|
|
status: 'ready',
|
|
|
|
|
fileId: fileRecord.id,
|
|
|
|
|
completedAt: new Date(),
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
})
|
|
|
|
|
.where(eq(generatedReports.id, reportJobId));
|
|
|
|
|
|
|
|
|
|
// 12. Emit report:ready socket event
|
|
|
|
|
emitToRoom(`user:${requestedBy}`, 'report:ready', {
|
|
|
|
|
reportId: reportJobId,
|
|
|
|
|
name,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 13. Create notification for requestedBy user
|
|
|
|
|
await db.insert(notifications).values({
|
|
|
|
|
portId,
|
|
|
|
|
userId: requestedBy,
|
|
|
|
|
type: 'system_alert',
|
|
|
|
|
title: 'Report Ready',
|
|
|
|
|
description: `Your report "${name}" is ready to download.`,
|
|
|
|
|
entityType: 'report',
|
|
|
|
|
entityId: reportJobId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
logger.info({ reportJobId, reportType }, 'Report generated successfully');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
|
|
|
logger.error({ reportJobId, err }, 'Report generation failed');
|
|
|
|
|
|
|
|
|
|
await db
|
|
|
|
|
.update(generatedReports)
|
|
|
|
|
.set({
|
|
|
|
|
status: 'failed',
|
|
|
|
|
errorMessage,
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
})
|
|
|
|
|
.where(eq(generatedReports.id, reportJobId));
|
|
|
|
|
|
|
|
|
|
emitToRoom(`user:${requestedBy}`, 'report:failed', {
|
|
|
|
|
reportId: reportJobId,
|
|
|
|
|
name,
|
|
|
|
|
error: errorMessage,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
}
|