// routes/salesExt.js — machine-to-machine API for the Sales Order module (/api/sales-ext) // msale received invoice PDFs from an external job (POST api/invoice_details/ // upload_invoice). That job must now be pointed here instead. Auth: header // MS-API-KEY = .env SALES_EXT_API_KEY (the endpoint is disabled while unset). // Invoices themselves, dispatch details and COA certificates are read live // from SAP, so they need no push at all any more. 'use strict'; const express = require('express'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); const { query } = require('../services/sales/db'); const masters = require('../services/sales/masters'); const { getPool } = require('../services/sqlPool'); const { DOC_DIR } = require('./sales'); const router = express.Router(); const INV_DIR = path.join(DOC_DIR, 'invoices'); fs.mkdirSync(INV_DIR, { recursive: true }); router.use((req, res, next) => { const key = process.env.SALES_EXT_API_KEY || ''; const got = String(req.headers['ms-api-key'] || ''); const ok = key && got.length === key.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(key)); if (!ok) return res.status(401).json({ success: false, message: 'Invalid API key' }); req.auditActor = 'sales-ext-api'; next(); }); // Invoice numbers of web orders (last N days, default 120) with no PDF yet. router.get('/invoices-without-pdf', async (req, res) => { try { const s = await masters.getSettings(); const days = Math.min(parseInt(req.query.days) || 120, 730); const pool = await getPool(s.company); const r = await pool.request().input('d', days).query(`SELECT DocNum, DocDate, CardCode, U_WEB_SO_NO FROM OINV WHERE CANCELED='N' AND U_WEB_SO_NO IS NOT NULL AND DocDate>=DATEADD(day,-@d,CAST(GETDATE() AS date))`); const have = new Set((await query(`SELECT DISTINCT INVOICE_NO FROM dbo.ZSO_INVOICE_FILES`)).map(x => String(x.INVOICE_NO))); res.json({ success: true, data: r.recordset.filter(x => !have.has(String(x.DocNum))).map(x => ({ invoice_no: x.DocNum, invoice_date: x.DocDate, card_code: x.CardCode, web_so_no: x.U_WEB_SO_NO })) }); } catch (e) { res.status(500).json({ success: false, message: e.message }); } }); // Body: [{invoice_no, invoice_file_name, invoice_file_data(base64)}] — also // accepts msale's index-keyed object form ({"0":{...},"1":{...}}). router.post('/invoice-pdf', async (req, res) => { const list = Array.isArray(req.body) ? req.body : Object.values(req.body || {}); const result = { success: [], error: [] }; for (const v of list) { try { const no = String(v.invoice_no || '').trim(); if (!/^\d+$/.test(no) || !v.invoice_file_data) throw new Error('invoice_no and invoice_file_data are required'); const buf = Buffer.from(String(v.invoice_file_data), 'base64'); if (buf.slice(0, 4).toString() !== '%PDF') throw new Error('file is not a PDF'); const name = `INV_${no}_${Date.now()}.pdf`; fs.writeFileSync(path.join(INV_DIR, name), buf); await query(`INSERT INTO dbo.ZSO_INVOICE_FILES (INVOICE_NO, FILE_NAME) VALUES (?,?)`, [no, name]); result.success.push({ invoice_no: no, invoice_file_name: v.invoice_file_name || name }); } catch (e) { result.error.push({ invoice_no: v && v.invoice_no, message: e.message }); } } res.json({ status: 'ok', ...result }); }); module.exports = router;