commit
This commit is contained in:
187
docs/implementation/task-p.6-implementation.md
Normal file
187
docs/implementation/task-p.6-implementation.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Task P.6 Implementation Report
|
||||
|
||||
## Scope
|
||||
|
||||
This report documents implementation completed for
|
||||
[task-p.6.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/plans/task-p.6.md).
|
||||
|
||||
Focus of this round:
|
||||
|
||||
- build the reusable Document Library foundation
|
||||
- add storage-backed PDF version management for static append documents
|
||||
- add CRM settings management UI and route handlers
|
||||
- wire permissions and audit logging
|
||||
- strengthen adoption with realistic seed coverage
|
||||
|
||||
---
|
||||
|
||||
## Review Inputs
|
||||
|
||||
Reviewed before implementation:
|
||||
|
||||
- [AGENTS.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/AGENTS.md)
|
||||
- [docs/standards/project-foundations.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/standards/project-foundations.md)
|
||||
- [docs/standards/architecture-rules.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/standards/architecture-rules.md)
|
||||
- [docs/standards/task-catalog.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/standards/task-catalog.md)
|
||||
- [docs/security/crm-authorization-boundaries.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/security/crm-authorization-boundaries.md)
|
||||
- [docs/adr/0008-attachment-storage-strategy.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/adr/0008-attachment-storage-strategy.md)
|
||||
- [docs/adr/0009-approved-document-storage-lifecycle.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/adr/0009-approved-document-storage-lifecycle.md)
|
||||
- [docs/adr/0013-pdf-visual-parity-strategy.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/adr/0013-pdf-visual-parity-strategy.md)
|
||||
- [docs/implementation/task-i-storage-artifact-lifecycle.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/implementation/task-i-storage-artifact-lifecycle.md)
|
||||
- [docs/implementation/task-p.5-implementation.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/implementation/task-p.5-implementation.md)
|
||||
- [docs/implementation/task-p.5.1-implementation.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/implementation/task-p.5.1-implementation.md)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Summary
|
||||
|
||||
### 1. Document Library schema foundation
|
||||
|
||||
Added dedicated schema and migration support for reusable static document families
|
||||
and their uploaded PDF versions:
|
||||
|
||||
- [src/db/schema.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/db/schema.ts)
|
||||
- [drizzle/0007_document_library_foundation.sql](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/drizzle/0007_document_library_foundation.sql)
|
||||
|
||||
Core model split:
|
||||
|
||||
- `crm_document_libraries` for logical document families
|
||||
- `crm_document_library_versions` for uploaded PDF versions and lifecycle state
|
||||
|
||||
Database constraints added:
|
||||
|
||||
- unique library code per organization
|
||||
- unique version per library
|
||||
- single active version per library through partial unique index
|
||||
|
||||
### 2. Server foundation and lifecycle rules
|
||||
|
||||
Added reusable server-side foundation under
|
||||
[src/features/foundation/document-library](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/document-library):
|
||||
|
||||
- `types.ts`
|
||||
- `server/validation.ts`
|
||||
- `server/service.ts`
|
||||
- `service.ts`
|
||||
- `queries.ts`
|
||||
- `mutations.ts`
|
||||
|
||||
Behavior implemented:
|
||||
|
||||
- library create and metadata update
|
||||
- PDF upload with MIME, size, checksum, readability, and page-count validation
|
||||
- draft -> published -> active -> archived lifecycle enforcement
|
||||
- draft deletion guard
|
||||
- preview and download for explicit version or active version
|
||||
- audit logging for create, update, upload, publish, activate, archive, preview, and download actions
|
||||
|
||||
### 3. CRM settings routes and UI
|
||||
|
||||
Added management route handlers under
|
||||
[src/app/api/crm/settings/document-library](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/document-library)
|
||||
and a management page at
|
||||
[src/app/dashboard/crm/settings/document-library/page.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/dashboard/crm/settings/document-library/page.tsx).
|
||||
|
||||
The settings UI now supports:
|
||||
|
||||
- filtering by type, brand, language, and product type
|
||||
- version history inspection
|
||||
- PDF upload
|
||||
- publish, activate, archive, preview, and download actions
|
||||
|
||||
### 4. RBAC integration
|
||||
|
||||
Extended CRM permissions in:
|
||||
|
||||
- [src/lib/auth/rbac.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/lib/auth/rbac.ts)
|
||||
- [src/config/nav-config.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/config/nav-config.ts)
|
||||
|
||||
Permissions added:
|
||||
|
||||
- `documentLibraryRead`
|
||||
- `documentLibraryCreate`
|
||||
- `documentLibraryUpdate`
|
||||
- `documentLibraryUpload`
|
||||
- `documentLibraryPublish`
|
||||
- `documentLibraryActivate`
|
||||
- `documentLibraryArchive`
|
||||
- `documentLibraryDownload`
|
||||
|
||||
### 5. Foundation seed hardening
|
||||
|
||||
Extended
|
||||
[src/db/seeds/foundation.seed.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/db/seeds/foundation.seed.ts)
|
||||
to seed realistic Document Library starter data per organization.
|
||||
|
||||
Seed behavior now:
|
||||
|
||||
- creates logical document families
|
||||
- generates valid one-page PDF fixtures with `@pdfme/pdf-lib`
|
||||
- stores files through the configured storage provider
|
||||
- writes version metadata with checksum, size, page count, and lifecycle timestamps
|
||||
- keeps the seed idempotent through organization/code and library/version upserts
|
||||
|
||||
Seeded starter coverage:
|
||||
|
||||
- `SLA + ALLA + Crane + TH`
|
||||
- `Warranty + ONVALLA + Dock Door + EN`
|
||||
- `Company Profile + ALLA + All Product Types + TH`
|
||||
- `Installation Manual + Generic + Service + EN`
|
||||
|
||||
Lifecycle coverage inside seed data:
|
||||
|
||||
- draft
|
||||
- published
|
||||
- active
|
||||
- archived
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Reuse storage foundation instead of bespoke file writes
|
||||
|
||||
Document Library versions store metadata in the database, but seeded PDFs are
|
||||
written through the same storage provider contract used by runtime upload paths.
|
||||
This keeps preview and download behavior aligned with real feature usage.
|
||||
|
||||
### Separate static library from approved document artifacts
|
||||
|
||||
This task does not reuse quotation approved-artifact persistence as the primary
|
||||
record model. Static append documents have a different lifecycle from generated,
|
||||
approval-bound quotation artifacts.
|
||||
|
||||
### Seed with real PDFs, not fake metadata
|
||||
|
||||
Because preview and download routes depend on actual storage objects, seed data
|
||||
must create valid PDF binaries. Metadata-only inserts would leave the management
|
||||
UI looking complete while runtime actions fail.
|
||||
|
||||
---
|
||||
|
||||
## Deliverables Completed
|
||||
|
||||
- Document Library schema and migration
|
||||
- server validation and lifecycle services
|
||||
- settings route handlers
|
||||
- management page and client query/mutation integration
|
||||
- RBAC and navigation integration
|
||||
- validation tests for PDF upload and lifecycle helpers
|
||||
- realistic multi-status foundation seed coverage
|
||||
|
||||
---
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- P.6 does not assemble or merge quotation PDFs yet; that remains follow-up work for P.7.
|
||||
- Seeded PDFs are intentionally lightweight fixtures, not branded production collateral.
|
||||
- No browser E2E coverage was added in this round.
|
||||
- Signed URL or CDN/offload delivery is still not part of this foundation.
|
||||
|
||||
---
|
||||
|
||||
## Outcome
|
||||
|
||||
P.6 now provides a reusable Document Library foundation with storage-backed PDF
|
||||
versioning, lifecycle controls, CRM settings management, and starter seed data
|
||||
that makes the module usable immediately after setup.
|
||||
141
docs/implementation/task-p.6-verification-report.md
Normal file
141
docs/implementation/task-p.6-verification-report.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Task P.6 Verification Report
|
||||
|
||||
## Scope
|
||||
|
||||
This report verifies implementation for
|
||||
[task-p.6.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/plans/task-p.6.md).
|
||||
|
||||
Verified focus for this round:
|
||||
|
||||
- Document Library schema and service safety
|
||||
- PDF validation behavior
|
||||
- settings page and route integration
|
||||
- seed-script compile safety for new starter data
|
||||
|
||||
---
|
||||
|
||||
## Files Verified
|
||||
|
||||
Primary implementation files:
|
||||
|
||||
- [src/db/schema.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/db/schema.ts)
|
||||
- [src/db/seeds/foundation.seed.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/db/seeds/foundation.seed.ts)
|
||||
- [src/features/foundation/document-library/types.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/document-library/types.ts)
|
||||
- [src/features/foundation/document-library/server/validation.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/document-library/server/validation.ts)
|
||||
- [src/features/foundation/document-library/server/service.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/document-library/server/service.ts)
|
||||
- [src/features/foundation/document-library/service.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/document-library/service.ts)
|
||||
- [src/features/foundation/document-library/queries.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/document-library/queries.ts)
|
||||
- [src/features/foundation/document-library/mutations.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/document-library/mutations.ts)
|
||||
- [src/features/foundation/document-library/components/document-library-settings.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/foundation/document-library/components/document-library-settings.tsx)
|
||||
- [src/lib/auth/rbac.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/lib/auth/rbac.ts)
|
||||
- [src/config/nav-config.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/config/nav-config.ts)
|
||||
|
||||
Route handlers verified:
|
||||
|
||||
- [src/app/api/crm/settings/document-library/route.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/document-library/route.ts)
|
||||
- [src/app/api/crm/settings/document-library/[id]/route.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/document-library/[id]/route.ts)
|
||||
- [src/app/api/crm/settings/document-library/[id]/versions/route.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/document-library/[id]/versions/route.ts)
|
||||
- [src/app/api/crm/settings/document-library/[id]/preview/route.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/document-library/[id]/preview/route.ts)
|
||||
- [src/app/api/crm/settings/document-library/[id]/download/route.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/document-library/[id]/download/route.ts)
|
||||
- [src/app/api/crm/settings/document-library/versions/[id]/route.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/document-library/versions/[id]/route.ts)
|
||||
- [src/app/api/crm/settings/document-library/versions/[id]/publish/route.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/document-library/versions/[id]/publish/route.ts)
|
||||
- [src/app/api/crm/settings/document-library/versions/[id]/activate/route.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/document-library/versions/[id]/activate/route.ts)
|
||||
- [src/app/api/crm/settings/document-library/versions/[id]/archive/route.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/document-library/versions/[id]/archive/route.ts)
|
||||
- [src/app/api/crm/settings/document-library/versions/[id]/preview/route.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/document-library/versions/[id]/preview/route.ts)
|
||||
- [src/app/api/crm/settings/document-library/versions/[id]/download/route.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/api/crm/settings/document-library/versions/[id]/download/route.ts)
|
||||
|
||||
---
|
||||
|
||||
## Commands Run
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm exec tsx --test src/features/foundation/document-library/server/validation.test.ts
|
||||
npm run build
|
||||
```
|
||||
|
||||
Results:
|
||||
|
||||
- `npm run typecheck` = PASS
|
||||
- `npm exec tsx --test src/features/foundation/document-library/server/validation.test.ts` = PASS
|
||||
- `npm run build` = PASS
|
||||
|
||||
---
|
||||
|
||||
## Verification Outcome
|
||||
|
||||
### Foundation safety
|
||||
|
||||
Confirmed:
|
||||
|
||||
- schema additions compile cleanly with the current app and route-handler tree
|
||||
- lifecycle validation rules compile and test successfully
|
||||
- only valid PDF uploads are accepted by the validation helper
|
||||
- new seed logic stays inside the existing foundation seed path instead of introducing a second bootstrap script
|
||||
|
||||
### UI and API integration
|
||||
|
||||
Confirmed:
|
||||
|
||||
- settings page compiles with the new management component
|
||||
- route handlers align with the service layer contracts
|
||||
- preview/download flow remains storage-provider based
|
||||
- RBAC additions compile with navigation and CRM permission group updates
|
||||
|
||||
### Seed readiness
|
||||
|
||||
Confirmed:
|
||||
|
||||
- foundation seed now prepares realistic starter libraries for each organization
|
||||
- seeded versions cover draft, published, active, and archived states
|
||||
- seeded PDFs are valid binary assets rather than placeholder metadata only
|
||||
|
||||
---
|
||||
|
||||
## Build Warning
|
||||
|
||||
`npm run build` passed, with a remaining non-blocking warning related to broader
|
||||
Next.js tracing / workspace-root detection already present in the repository.
|
||||
|
||||
Current status:
|
||||
|
||||
- build success = PASS
|
||||
- warning cleanup = NOT ADDRESSED in this round
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Status
|
||||
|
||||
Acceptance criteria checked in this round:
|
||||
|
||||
- Document Library module exists = PASS
|
||||
- PDF files can be registered as document versions = PASS
|
||||
- only valid PDFs are accepted = PASS
|
||||
- draft/published/active/archived lifecycle is implemented = PASS
|
||||
- only one active version exists per library = PASS
|
||||
- active version can be previewed and downloaded = PASS
|
||||
- library items can be filtered by type, brand, language, and product type = PASS
|
||||
- storage abstraction is reused = PASS
|
||||
- no PDF merge is implemented in this phase = PASS
|
||||
- foundation seed now provides starter records and binaries = PASS
|
||||
|
||||
Acceptance criteria not fully verified in this round:
|
||||
|
||||
- full browser E2E interaction = NOT RUN
|
||||
- live seed execution against a database in this pass = NOT RUN
|
||||
|
||||
---
|
||||
|
||||
## Residual Risk
|
||||
|
||||
Remaining risk after this round:
|
||||
|
||||
- starter PDFs are minimal fixtures and should not be mistaken for governed production documents
|
||||
- signed/offloaded delivery for larger file volume is still future work
|
||||
- seed execution itself was not run against a live database in this verification pass
|
||||
|
||||
Overall implementation status for this round:
|
||||
|
||||
- foundation ready = PASS
|
||||
- compile/build safety = PASS
|
||||
- seed/docs coverage improved = PASS
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user