import fs from "node:fs/promises"; import crypto from "node:crypto"; import path from "node:path"; import process from "node:process"; import { pathToFileURL } from "node:url"; let FileBlob; let SpreadsheetFile; let Workbook; const EXPECTED_HEADERS = [ "ARRIVAL", "DEPARTURE", "NIGHTS", "BLOCK_CODE", "RES_COMMENT", "Booking Room", "Total Booking Price", ]; const DUPLICATE_FILL = "#FFF2CC"; const DUPLICATE_FONT = "#9C6500"; const REVIEW_FILL = "#FCE4D6"; const REVIEW_FONT = "#C65911"; const HEADER_FILL = "#FFFFFF"; const BODY_FONT = "#222222"; const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; async function loadArtifactTool() { const configured = String(process.env.COMPANY_REPORT_ARTIFACT_TOOL_MODULE ?? "").trim(); const module = configured ? await import(pathToFileURL(path.resolve(configured)).href) : await import("@oai/artifact-tool"); ({ FileBlob, SpreadsheetFile, Workbook } = module); if (!FileBlob || !SpreadsheetFile || !Workbook) fail("artifact-tool module is invalid"); } function fail(message) { throw new Error(message); } function safeText(value) { const text = String(value ?? ""); return /^[=+\-@]/.test(text) ? `'${text}` : text; } function excelDate(value) { if (!DATE_PATTERN.test(value)) fail("invalid date value"); const [year, month, day] = value.split("-").map(Number); const parsed = new Date(Date.UTC(year, month - 1, day)); if ( parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day ) { fail("invalid date value"); } return parsed; } function dateKey(value) { if (value instanceof Date && !Number.isNaN(value.getTime())) { return value.toISOString().slice(0, 10); } if (typeof value === "number" && Number.isFinite(value)) { const epoch = Date.UTC(1899, 11, 30); return new Date(epoch + Math.floor(value) * 86400000).toISOString().slice(0, 10); } if (typeof value === "string" && DATE_PATTERN.test(value.slice(0, 10))) { return value.slice(0, 10); } return ""; } function validatePayload(payload) { if (!payload || payload.schema_version !== "1.0") fail("invalid payload schema"); if (!Array.isArray(payload.headers) || payload.headers.length !== EXPECTED_HEADERS.length) { fail("invalid headers"); } if (!payload.headers.every((header, index) => header === EXPECTED_HEADERS[index])) { fail("invalid headers"); } if (!Array.isArray(payload.periods) || payload.periods.length !== 3) { fail("invalid period count"); } const names = new Set(); for (const period of payload.periods) { if ( typeof period.sheet_name !== "string" || period.sheet_name.length < 1 || period.sheet_name.length > 31 || names.has(period.sheet_name) || !Array.isArray(period.rows) ) { fail("invalid worksheet contract"); } names.add(period.sheet_name); for (const row of period.rows) { if ( !DATE_PATTERN.test(String(row.arrival ?? "")) || !DATE_PATTERN.test(String(row.departure ?? "")) || !Number.isInteger(row.nights) || row.nights < 0 || typeof row.block_code !== "string" || typeof row.res_comment !== "string" || typeof row.booking_room !== "string" || typeof row.total_booking_price !== "string" || typeof row.duplicate_group !== "boolean" || typeof row.multi_price_review !== "boolean" ) { fail("invalid output row contract"); } } } } function writeSheet(workbook, period, periodIndex) { const sheet = workbook.worksheets.add(period.sheet_name); const body = period.rows.map((row) => [ excelDate(row.arrival), excelDate(row.departure), row.nights, safeText(row.block_code), safeText(row.res_comment), safeText(row.booking_room), safeText(row.total_booking_price), ]); const matrix = [EXPECTED_HEADERS, ...body]; const lastRow = matrix.length; const used = sheet.getRange(`A1:G${lastRow}`); used.values = matrix; used.format.font = { name: "Arial", size: 10, color: BODY_FONT }; used.format.verticalAlignment = "center"; sheet.getRange("A1").format.columnWidth = 13; sheet.getRange("B1").format.columnWidth = 13; sheet.getRange("C1").format.columnWidth = 9; sheet.getRange("D1").format.columnWidth = 20; sheet.getRange("E1").format.columnWidth = 25; sheet.getRange("F1").format.columnWidth = 34; sheet.getRange("G1").format.columnWidth = 48; const table = sheet.tables.add( `A1:G${lastRow}`, true, `CompanyReportPeriod${periodIndex + 1}`, ); table.style = "TableStyleLight1"; table.showHeaders = true; table.showTotals = false; table.showBandedColumns = false; table.showFilterButton = true; const header = sheet.getRange("A1:G1"); header.format.fill = HEADER_FILL; header.format.font = { name: "Arial", size: 10, bold: true, color: "#000000" }; header.format.horizontalAlignment = "center"; header.format.verticalAlignment = "center"; header.format.rowHeight = 24; header.format.borders = { bottom: { style: "thin", color: "#7F7F7F" }, }; sheet.freezePanes.freezeRows(1); if (body.length > 0) { const data = sheet.getRange(`A2:G${lastRow}`); data.format.rowHeight = 30; sheet.getRange(`A2:B${lastRow}`).setNumberFormat("yyyy-mm-dd"); sheet.getRange(`A2:D${lastRow}`).format.horizontalAlignment = "center"; sheet.getRange(`E2:G${lastRow}`).format.horizontalAlignment = "left"; sheet.getRange(`D2:G${lastRow}`).format.wrapText = true; period.rows.forEach((row, rowIndex) => { const excelRow = rowIndex + 2; if (row.duplicate_group) { sheet.getRange(`A${excelRow}:G${excelRow}`).format.fill = DUPLICATE_FILL; sheet.getRange(`E${excelRow}`).format.font = { name: "Arial", size: 10, bold: true, color: DUPLICATE_FONT, }; } if (row.multi_price_review) { sheet.getRange(`G${excelRow}`).format.fill = REVIEW_FILL; sheet.getRange(`G${excelRow}`).format.font = { name: "Arial", size: 10, bold: true, color: REVIEW_FONT, }; } }); data.format.autofitRows(); } sheet.showGridLines = true; return sheet; } async function validateWorkbook(workbook, payload, stage) { const names = workbook.worksheets.items.map((sheet) => sheet.name); const expectedNames = payload.periods.map((period) => period.sheet_name); if (JSON.stringify(names) !== JSON.stringify(expectedNames)) { fail(`${stage} worksheet names do not match`); } let formulaCount = 0; for (let index = 0; index < payload.periods.length; index += 1) { const period = payload.periods[index]; const sheet = workbook.worksheets.getItem(period.sheet_name); const expectedRows = period.rows.length + 1; const used = sheet.getUsedRange(); const values = used?.values ?? []; if (values.length !== expectedRows || (values[0]?.length ?? 0) !== 7) { fail(`${stage} worksheet dimensions do not match`); } if (!EXPECTED_HEADERS.every((header, column) => values[0][column] === header)) { fail(`${stage} worksheet headers do not match`); } period.rows.forEach((row, rowIndex) => { const actual = values[rowIndex + 1] ?? []; const expected = [ row.arrival, row.departure, row.nights, safeText(row.block_code), safeText(row.res_comment), safeText(row.booking_room), safeText(row.total_booking_price), ]; if ( dateKey(actual[0]) !== expected[0] || dateKey(actual[1]) !== expected[1] || Number(actual[2]) !== expected[2] || String(actual[3] ?? "") !== expected[3] || String(actual[4] ?? "") !== expected[4] || String(actual[5] ?? "") !== expected[5] || String(actual[6] ?? "") !== expected[6] ) { fail(`${stage} worksheet values do not match`); } }); if (sheet.tables.items.length !== 1 || !sheet.tables.items[0].showFilterButton) { fail(`${stage} worksheet filter is missing`); } const formulaInspection = await workbook.inspect({ kind: "formula", sheetId: period.sheet_name, range: `A1:G${expectedRows}`, maxChars: 4000, options: { maxResults: 100 }, }); const text = String(formulaInspection.ndjson ?? ""); formulaCount += text .split("\n") .filter((line) => line.includes('"kind":"formula"')).length; if (/#REF!|#DIV\/0!|#VALUE!|#NAME\?|#N\/A/.test(text)) { fail(`${stage} workbook contains a formula error`); } } if (formulaCount !== 0) fail(`${stage} workbook must contain no formulas`); return formulaCount; } async function main() { const [inputPath, outputPath, previewDir, summaryPath] = process.argv.slice(2); if (!inputPath || !outputPath || !previewDir || !summaryPath) { fail("usage: build_workbook.mjs INPUT_JSON OUTPUT_XLSX PREVIEW_DIR SUMMARY_JSON"); } await loadArtifactTool(); const payload = JSON.parse(await fs.readFile(inputPath, "utf8")); validatePayload(payload); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.mkdir(previewDir, { recursive: true }); await fs.mkdir(path.dirname(summaryPath), { recursive: true }); const workbook = Workbook.create(); payload.periods.forEach((period, index) => writeSheet(workbook, period, index)); await validateWorkbook(workbook, payload, "pre-export"); const xlsx = await SpreadsheetFile.exportXlsx(workbook); await xlsx.save(outputPath); await fs.chmod(outputPath, 0o600); const reopened = await SpreadsheetFile.importXlsx(await FileBlob.load(outputPath)); const formulaCount = await validateWorkbook(reopened, payload, "post-export"); const previews = []; for (let index = 0; index < payload.periods.length; index += 1) { const period = payload.periods[index]; const preview = await reopened.render({ sheetName: period.sheet_name, autoCrop: "all", scale: 1.5, format: "png", }); const previewPath = path.join(previewDir, `sheet-${index + 1}.png`); await fs.writeFile(previewPath, new Uint8Array(await preview.arrayBuffer()), { mode: 0o600, }); previews.push(previewPath); } const stat = await fs.stat(outputPath); const semanticSha256 = crypto .createHash("sha256") .update(JSON.stringify({ headers: payload.headers, periods: payload.periods }), "utf8") .digest("hex"); const summary = { status: "success", schema_version: payload.schema_version, company: payload.company, filename: payload.filename, sheet_names: payload.periods.map((period) => period.sheet_name), row_counts: payload.periods.map((period) => period.rows.length), formula_count: formulaCount, semantic_sha256: semanticSha256, preview_count: previews.length, byte_size: stat.size, }; await fs.writeFile(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, { encoding: "utf8", mode: 0o600, }); await fs.rm(`${outputPath}.inspect.ndjson`, { force: true }); process.stdout.write(`${JSON.stringify(summary)}\n`); } try { await main(); } catch (_error) { const outputPath = process.argv[3]; if (outputPath) { await fs.rm(`${outputPath}.inspect.ndjson`, { force: true }).catch(() => undefined); } process.stderr.write( `${JSON.stringify({ status: "failed", code: "COMPANY_REPORT_OUTPUT_VALIDATION_FAILED", })}\n`, ); process.exitCode = 4; }