This commit is contained in:
phaichayon
2026-06-30 12:36:11 +07:00
parent 9ab254ef0d
commit 9698228c51
4 changed files with 711 additions and 1 deletions

View File

@@ -1,5 +1,7 @@
const fs = require('node:fs');
const path = require('node:path');
const { createHash } = require('node:crypto');
const { PDFDocument, StandardFonts, rgb } = require('@pdfme/pdf-lib');
const postgres = require('postgres');
type SqlClient = ReturnType<typeof postgres>;
@@ -45,6 +47,24 @@ type ReportDefinitionSeed = {
category: string;
};
type DocumentLibraryVersionSeed = {
version: string;
status: 'draft' | 'published' | 'active' | 'archived';
fileName: string;
summary: string;
};
type DocumentLibrarySeed = {
code: string;
name: string;
documentType: string;
description: string;
brand: string;
language: string;
productType: string;
versions: DocumentLibraryVersionSeed[];
};
function loadEnvFile(filePath: string) {
if (!fs.existsSync(filePath)) {
return;
@@ -643,6 +663,197 @@ const REPORT_DEFINITIONS: ReportDefinitionSeed[] = [
}
];
const DOCUMENT_LIBRARY_SEEDS: DocumentLibrarySeed[] = [
{
code: 'SLA_ALLA_CRANE_TH',
name: 'SLA - ALLA - Crane - TH',
documentType: 'sla',
description: 'Seeded service level agreement pack for ALLA crane quotations.',
brand: 'alla',
language: 'th',
productType: 'crane',
versions: [
{
version: '1.0',
status: 'archived',
fileName: 'sla-alla-crane-th-v1.0.pdf',
summary: 'Archived baseline SLA kept for historical reference.'
},
{
version: '1.1',
status: 'active',
fileName: 'sla-alla-crane-th-v1.1.pdf',
summary: 'Current active SLA version used as the default append document.'
}
]
},
{
code: 'WARRANTY_ONVALLA_DOCK_DOOR_EN',
name: 'Warranty - ONVALLA - Dock Door - EN',
documentType: 'warranty',
description: 'Seeded warranty booklet for ONVALLA dock door opportunities.',
brand: 'onvalla',
language: 'en',
productType: 'dock_door',
versions: [
{
version: '1.0',
status: 'active',
fileName: 'warranty-onvalla-dock-door-en-v1.0.pdf',
summary: 'Active warranty issue for English dock door quotations.'
}
]
},
{
code: 'COMPANY_PROFILE_ALLA_ALL_TH',
name: 'Company Profile - ALLA - All Product Types - TH',
documentType: 'company_profile',
description: 'Seeded company profile for Thai proposals across all ALLA product families.',
brand: 'alla',
language: 'th',
productType: 'all',
versions: [
{
version: '2025.12',
status: 'active',
fileName: 'company-profile-alla-all-th-2025.12.pdf',
summary: 'Current active company profile packaged with selected quotations.'
},
{
version: '2026.01',
status: 'published',
fileName: 'company-profile-alla-all-th-2026.01.pdf',
summary: 'Published refresh ready for operator activation review.'
}
]
},
{
code: 'INSTALLATION_MANUAL_GENERIC_SERVICE_EN',
name: 'Installation Manual - Generic - Service - EN',
documentType: 'installation_manual',
description: 'Seeded draft service installation manual for future operator review.',
brand: 'generic',
language: 'en',
productType: 'service',
versions: [
{
version: '0.9-draft',
status: 'draft',
fileName: 'installation-manual-generic-service-en-v0.9-draft.pdf',
summary: 'Draft-only technical manual left unpublished to exercise lifecycle controls.'
}
]
}
];
function sha256(buffer: Buffer) {
return createHash('sha256').update(buffer).digest('hex');
}
function sanitizeStorageSegment(value: string) {
return value
.trim()
.replace(/\\/g, '/')
.replace(/^\/+|\/+$/g, '')
.replace(/[^a-zA-Z0-9._/-]+/g, '-');
}
function buildOrganizationStorageKey(organizationId: string, segments: string[]) {
return ['organizations', organizationId, ...segments.map(sanitizeStorageSegment).filter(Boolean)].join('/');
}
function resolveLifecycleTimestamps(status: DocumentLibraryVersionSeed['status']) {
switch (status) {
case 'draft':
return {
publishedAt: null,
archivedAt: null
};
case 'published':
return {
publishedAt: new Date('2026-01-20T02:00:00.000Z'),
archivedAt: null
};
case 'active':
return {
publishedAt: new Date('2026-01-10T02:00:00.000Z'),
archivedAt: null
};
case 'archived':
return {
publishedAt: new Date('2025-12-15T02:00:00.000Z'),
archivedAt: new Date('2026-01-05T02:00:00.000Z')
};
default:
return {
publishedAt: null,
archivedAt: null
};
}
}
async function createDocumentLibrarySeedPdf(input: {
organizationName: string;
libraryName: string;
documentType: string;
brand: string;
language: string;
productType: string;
version: string;
summary: string;
}) {
const pdf = await PDFDocument.create();
const page = pdf.addPage([595.28, 841.89]);
const titleFont = await pdf.embedFont(StandardFonts.HelveticaBold);
const bodyFont = await pdf.embedFont(StandardFonts.Helvetica);
let cursorY = 780;
page.drawText(input.libraryName, {
x: 48,
y: cursorY,
size: 18,
font: titleFont,
color: rgb(0.1, 0.16, 0.24)
});
cursorY -= 32;
const lines = [
`Organization: ${input.organizationName}`,
`Document Type: ${input.documentType}`,
`Brand: ${input.brand}`,
`Language: ${input.language}`,
`Product Type: ${input.productType}`,
`Version: ${input.version}`,
'',
input.summary,
'',
'Seeded by foundation.seed.ts for Document Library management verification.'
];
for (const line of lines) {
page.drawText(line, {
x: 48,
y: cursorY,
size: line === input.summary ? 12 : 11,
font: line === input.summary ? titleFont : bodyFont,
color: rgb(0.23, 0.27, 0.33)
});
cursorY -= line === '' ? 18 : 20;
}
const bytes = await pdf.save();
return Buffer.from(bytes);
}
async function getSeedStorageProvider() {
const storageModule = await import('../../features/foundation/storage/server/provider.ts');
return storageModule.resolveStorageProvider();
}
const DOCUMENT_SEQUENCES = [
{ documentType: 'customer', prefix: 'CUS' },
{ documentType: 'contact', prefix: 'CON' },
@@ -1402,6 +1613,176 @@ async function upsertDocumentTemplatesForOrganization(
`;
}
async function upsertDocumentLibraryForOrganization(
sql: SqlClient,
organization: { id: string; name: string; createdBy: string }
) {
const storageProvider = await getSeedStorageProvider();
for (const librarySeed of DOCUMENT_LIBRARY_SEEDS) {
const [libraryRow] = await sql`
insert into crm_document_libraries (
id,
organization_id,
code,
name,
document_type,
description,
brand,
language,
product_type,
status,
is_active,
created_by,
updated_by
) values (
${crypto.randomUUID()},
${organization.id},
${librarySeed.code},
${librarySeed.name},
${librarySeed.documentType},
${librarySeed.description},
${librarySeed.brand},
${librarySeed.language},
${librarySeed.productType},
${'active'},
${true},
${organization.createdBy},
${organization.createdBy}
)
on conflict (organization_id, code) do update
set
name = excluded.name,
document_type = excluded.document_type,
description = excluded.description,
brand = excluded.brand,
language = excluded.language,
product_type = excluded.product_type,
status = excluded.status,
is_active = excluded.is_active,
updated_by = excluded.updated_by,
deleted_at = null,
updated_at = now()
returning id
`;
const hasSeededActiveVersion = librarySeed.versions.some((version) => version.status === 'active');
if (hasSeededActiveVersion) {
await sql`
update crm_document_library_versions
set
is_active = ${false},
updated_at = now()
where library_id = ${libraryRow.id}
`;
}
for (const versionSeed of librarySeed.versions) {
const existingVersion =
(
await sql`
select id
from crm_document_library_versions
where library_id = ${libraryRow.id}
and version = ${versionSeed.version}
limit 1
`
)[0] ?? null;
const versionId = existingVersion?.id ?? crypto.randomUUID();
const pdfBuffer = await createDocumentLibrarySeedPdf({
organizationName: organization.name,
libraryName: librarySeed.name,
documentType: librarySeed.documentType,
brand: librarySeed.brand,
language: librarySeed.language,
productType: librarySeed.productType,
version: versionSeed.version,
summary: versionSeed.summary
});
const storageKey = buildOrganizationStorageKey(organization.id, [
'document-library',
libraryRow.id,
versionId,
versionSeed.fileName
]);
const storedObject = await storageProvider.putObject({
key: storageKey,
body: pdfBuffer,
contentType: 'application/pdf',
fileName: versionSeed.fileName
});
const lifecycleTimestamps = resolveLifecycleTimestamps(versionSeed.status);
const isActiveVersion = versionSeed.status === 'active';
await sql`
insert into crm_document_library_versions (
id,
organization_id,
library_id,
version,
file_name,
storage_provider,
storage_key,
mime_type,
file_size,
checksum,
page_count,
status,
is_active,
published_at,
published_by,
archived_at,
archived_by,
deleted_at,
created_by
) values (
${versionId},
${organization.id},
${libraryRow.id},
${versionSeed.version},
${versionSeed.fileName},
${storedObject.provider},
${storedObject.key},
${'application/pdf'},
${pdfBuffer.byteLength},
${sha256(pdfBuffer)},
${1},
${versionSeed.status},
${isActiveVersion},
${lifecycleTimestamps.publishedAt},
${lifecycleTimestamps.publishedAt ? organization.createdBy : null},
${lifecycleTimestamps.archivedAt},
${lifecycleTimestamps.archivedAt ? organization.createdBy : null},
${null},
${organization.createdBy}
)
on conflict (library_id, version) do update
set
file_name = excluded.file_name,
storage_provider = excluded.storage_provider,
storage_key = excluded.storage_key,
mime_type = excluded.mime_type,
file_size = excluded.file_size,
checksum = excluded.checksum,
page_count = excluded.page_count,
status = excluded.status,
is_active = excluded.is_active,
published_at = excluded.published_at,
published_by = excluded.published_by,
archived_at = excluded.archived_at,
archived_by = excluded.archived_by,
deleted_at = excluded.deleted_at,
updated_at = now()
`;
}
}
}
async function upsertReportDefinitionsForOrganization(
sql: SqlClient,
organization: { id: string }
@@ -1572,6 +1953,7 @@ async function main() {
await upsertApprovalWorkflowsForOrganization(sql, organization.id);
await upsertApprovalMatricesForOrganization(sql, organization);
await upsertDocumentTemplatesForOrganization(sql, organization);
await upsertDocumentLibraryForOrganization(sql, organization);
await upsertNotificationTemplatesForOrganization(sql, organization);
await upsertReportDefinitionsForOrganization(sql, organization);
console.log(`[seed-foundation] Seeded foundation data for ${organization.name}`);