import { ZipArchive } from "archiver"; import * as storage from "../storage/documents.storage"; import { DocumentGenerationService } from "./generation.service"; import { uploadToS3 } from "../utils/s3"; import { db } from "../db"; import { fileUploads } from "@shared/schema"; import type { DocumentPackage, DocumentPackageItem } from "@shared/schema"; export interface GeneratePackageRequest { packageId: number; taskId: number; organizationId: number; userId: number; } export interface GeneratePackageResult { packageId: number; zipUrl: string; generatedFiles: number; skippedItems: number; } export class PackageService { constructor(private generationService: DocumentGenerationService) {} async list(organizationId: number): Promise> { const packages = await storage.listDocumentPackages(organizationId); const result: Array = []; for (const pkg of packages) { const items = await storage.listDocumentPackageItems(pkg.id); result.push({ ...pkg, items }); } return result; } async getById(packageId: number, organizationId: number): Promise<(DocumentPackage & { items: DocumentPackageItem[] }) | undefined> { const pkg = await storage.getDocumentPackage(packageId, organizationId); if (!pkg) return undefined; const items = await storage.listDocumentPackageItems(packageId); return { ...pkg, items }; } async create( data: { name: string; description?: string; category?: string; items: Array<{ templateId: number; sortOrder?: number; customName?: string; condition?: any }> }, organizationId: number, ): Promise { const pkg = await storage.createDocumentPackage({ organizationId, name: data.name, description: data.description, category: data.category, }); const items: DocumentPackageItem[] = []; for (let i = 0; i < data.items.length; i++) { const item = data.items[i]; items.push( await storage.createDocumentPackageItem({ packageId: pkg.id, templateId: item.templateId, sortOrder: item.sortOrder ?? i, customName: item.customName, condition: item.condition, }), ); } return { ...pkg, items }; } async update( packageId: number, organizationId: number, data: Partial & { items?: Array<{ id?: number; templateId: number; sortOrder?: number; customName?: string; condition?: any }> }, ): Promise<(DocumentPackage & { items: DocumentPackageItem[] }) | undefined> { const pkg = await storage.updateDocumentPackage(packageId, organizationId, data); if (!pkg) return undefined; if (data.items) { const existing = await storage.listDocumentPackageItems(packageId); const incomingIds = data.items.filter((i) => i.id && i.id > 0).map((i) => i.id!); const toDelete = existing.filter((e) => !incomingIds.includes(e.id)); for (const item of toDelete) { await storage.deleteDocumentPackageItem(item.id, packageId); } for (let i = 0; i < data.items.length; i++) { const item = data.items[i]; const itemData = { packageId, templateId: item.templateId, sortOrder: item.sortOrder ?? i, customName: item.customName, condition: item.condition, }; if (item.id && item.id > 0) { await storage.updateDocumentPackageItem(item.id, packageId, itemData); } else { await storage.createDocumentPackageItem(itemData); } } } return this.getById(packageId, organizationId); } async delete(packageId: number, organizationId: number): Promise { const items = await storage.listDocumentPackageItems(packageId); for (const item of items) { await storage.deleteDocumentPackageItem(item.id, packageId); } await storage.deleteDocumentPackage(packageId, organizationId); } async generatePackage(request: GeneratePackageRequest): Promise { const pkg = await this.getById(request.packageId, request.organizationId); if (!pkg) throw new Error("Пакет не найден"); if (!pkg.items.length) throw new Error("В пакете нет шаблонов"); const files: Array<{ name: string; buffer: Buffer; format: 'pdf' | 'docx' }> = []; let skippedItems = 0; for (const item of pkg.items) { // TODO: evaluate condition try { const result = await this.generationService.generate({ templateId: item.templateId, taskId: request.taskId, organizationId: request.organizationId, userId: request.userId, outputFormat: "both", }); if (result.pdfUrl) { const fileKey = result.pdfUrl.replace("/api/files/", ""); const buffer = await this.downloadFile(fileKey); files.push({ name: `${item.customName || `document_${item.templateId}`}.pdf`, buffer, format: "pdf", }); } if (result.docxUrl) { const fileKey = result.docxUrl.replace("/api/files/", ""); const buffer = await this.downloadFile(fileKey); files.push({ name: `${item.customName || `document_${item.templateId}`}.docx`, buffer, format: "docx", }); } } catch { skippedItems++; } } if (files.length === 0) throw new Error("Не удалось сгенерировать ни одного документа"); const zipBuffer = await this.createZip(files); const zipName = `${pkg.name}-${Date.now()}.zip`; const { key } = await uploadToS3(zipBuffer, zipName, "application/zip"); try { await db.insert(fileUploads).values({ organizationId: request.organizationId, uploadedBy: request.userId, fileKey: key, originalName: zipName, sizeBytes: zipBuffer.length, taskId: request.taskId, fieldId: null, }).onConflictDoNothing(); } catch (err) { console.warn('[Documents] Failed to track package zip:', err); } return { packageId: pkg.id, zipUrl: `/api/files/${key}`, generatedFiles: files.length, skippedItems, }; } private async downloadFile(fileKey: string): Promise { const { isS3Enabled, streamFromS3 } = await import("../utils/s3.js"); const fs = await import("fs/promises"); const path = await import("path"); if (isS3Enabled) { const result = await streamFromS3(fileKey); if (!result?.body) throw new Error("Не удалось загрузить файл"); const chunks: Buffer[] = []; for await (const chunk of result.body as any) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } return Buffer.concat(chunks); } const filePath = path.join(process.env.UPLOADS_DIR || "/app/data/uploads", path.basename(fileKey)); return fs.readFile(filePath); } private async createZip(files: Array<{ name: string; buffer: Buffer; format: 'pdf' | 'docx' }>): Promise { return new Promise((resolve, reject) => { const archive = new ZipArchive({ zlib: { level: 9 } }); const chunks: Buffer[] = []; archive.on("data", (chunk: Buffer) => chunks.push(chunk)); archive.on("end", () => resolve(Buffer.concat(chunks))); archive.on("error", (err: Error) => reject(err)); for (const file of files) { archive.append(file.buffer, { name: file.name }); } archive.finalize(); }); } }