import { Pool } from 'pg' import refreshTokenHelper from "../../utils/refreshTokenHelper" /** * Open (unpaid) sales invoices of fulfillment customers for the dashboard card. * * "Open" mirrors the /sales/invoices list + ffn-unpaid-count logic: * - DocStatus = 'CO' only — Closed (CL) invoices had their open amount written * off and Drafts (DR) are not yet real receivables, so neither is open. * - IsPaid = 'N' * - openAmt = |GrandTotal| − |sum of allocation lines whose payment is CO| * must be > 0 (IsPaid may lag behind allocations). * Partner scope: c_bpartner.isfulfillmentcustomer = 'Y' only. */ const handleFunc = async (event: any) => { const config = useRuntimeConfig() const dbConfig = { host: config.pgHost, port: parseInt(config.pgPort || '5432'), database: config.pgDatabase, user: config.pgUser, password: config.pgPassword, } if (!dbConfig.host || !dbConfig.database || !dbConfig.user || !dbConfig.password) { throw createError({ statusCode: 500, message: 'PostgreSQL credentials not configured. Please set PG_HOST, PG_DATABASE, PG_USER, PG_PASSWORD in .env' }) } // Cross-partner financial data — admin/staff dashboard only. Limited // (merchant) roles get an empty result even if they hit the URL directly. const roleCookie = getCookie(event, 'logship_role') let menuType: string = 'Menu' try { const role = roleCookie ? JSON.parse(roleCookie) : null menuType = role?.FrontendMenu?.id || role?.FrontendMenu || 'Menu' } catch {} if (menuType === 'c') { return { records: [], count: 0, totalOpenAmt: 0 } } const pool = new Pool(dbConfig) try { // Same invoice population as /sales/invoices (isSOTrx OR doctype 1000006), // restricted to fulfillment customers and to potentially-open documents. const sql = ` SELECT i.c_invoice_id AS id, i.documentno, i.dateinvoiced, i.grandtotal, i.c_bpartner_id AS partner_id, i.ad_org_id AS org_id, i.c_currency_id AS currency_id, cur.iso_code AS currency, dt.docbasetype, bp.name AS partner, org.name AS org_name, COALESCE(pt.netdays, 0) AS netdays, COALESCE(alloc.paid, 0) AS paidamt FROM c_invoice i JOIN c_bpartner bp ON bp.c_bpartner_id = i.c_bpartner_id AND bp.isfulfillmentcustomer = 'Y' LEFT JOIN ad_org org ON org.ad_org_id = i.ad_org_id LEFT JOIN c_currency cur ON cur.c_currency_id = i.c_currency_id LEFT JOIN c_doctype dt ON dt.c_doctype_id = COALESCE(NULLIF(i.c_doctype_id, 0), i.c_doctypetarget_id) LEFT JOIN c_paymentterm pt ON pt.c_paymentterm_id = i.c_paymentterm_id LEFT JOIN ( SELECT al.c_invoice_id, SUM(al.amount) AS paid FROM c_allocationline al JOIN c_payment p ON p.c_payment_id = al.c_payment_id AND p.docstatus = 'CO' GROUP BY al.c_invoice_id ) alloc ON alloc.c_invoice_id = i.c_invoice_id WHERE (i.issotrx = 'Y' OR i.c_doctypetarget_id = 1000006) AND i.docstatus = 'CO' AND i.ispaid = 'N' AND i.isactive = 'Y' AND i.ad_client_id = 1000000 ORDER BY i.dateinvoiced ASC, i.c_invoice_id ASC ` const result = await pool.query(sql) const records = result.rows .map((r: any) => { const grandTotal = parseFloat(r.grandtotal || 0) // Raw allocation sum is signed (reversals cancel originals); the net // paid amount is its absolute value — same as TotalPaidAmt on /sales/invoices. const paidAmt = Math.abs(parseFloat(r.paidamt || 0)) const openAmt = Math.max(0, Math.abs(grandTotal) - paidAmt) return { id: Number(r.id), documentNo: r.documentno, dateInvoiced: r.dateinvoiced, partner: r.partner || '', partnerId: Number(r.partner_id) || 0, orgName: r.org_name || '', organizationId: Number(r.org_id) || 0, currencyId: Number(r.currency_id) || 0, currency: r.currency || '', // 'ARI' / 'ARC' — the quick-payment modal flips sign + doc type for credit memos. docBaseType: r.docbasetype || '', netDays: parseInt(r.netdays || 0), grandTotal, paidAmt, openAmt } }) // Guard against float noise on fully-allocated invoices still flagged IsPaid='N'. .filter((r: any) => r.openAmt > 0.005) const totalOpenAmt = records.reduce((sum: number, r: any) => sum + r.openAmt, 0) return { records, count: records.length, totalOpenAmt } } catch (err: any) { console.error('PostgreSQL query error (open-fulfillment-invoices):', err) throw createError({ statusCode: 500, message: `Database error: ${err.message}` }) } finally { await pool.end() } } export default defineEventHandler(async (event) => { try { return await handleFunc(event) } catch(err: any) { try { let authToken: any = await refreshTokenHelper(event) return await handleFunc(event) } catch(error: any) { console.error('Fatal error in open-fulfillment-invoices:', error) throw error } } })