import { string } from 'alga-js' import refreshTokenHelper from "../../utils/refreshTokenHelper" import errorHandlingHelper from "../../utils/errorHandlingHelper" /** * Dunning gate status for the current organization's linked business partner. * * Reads two Yes-No flags on the org's C_BPartner: * - IsTerminationNotice → warning modal on each login (last reminder before termination) * - IsTerminated → permanent blocking modal until the flag is cleared * * When either flag is set, also returns the partner's open (unpaid) invoices so the * modal can list them. Fail-soft in every branch: any error (including the columns * not existing in iDempiere yet) returns the inactive shape — an API hiccup must * never lock users out or pop a false modal. */ const INACTIVE = { notice: false, terminated: false, invoices: [], totalOpen: 0, sessionKey: '' } const handleFunc = async (event: any, authToken: any = null) => { const config = useRuntimeConfig() const serviceToken = config.api.idempieretoken const organizationId = getCookie(event, 'logship_organization_id') // logship_session is httpOnly and regenerated on every login — expose a short // key so the client can show the notice once per login without reading the cookie. const sessionKey = String(getCookie(event, 'logship_session') || '').slice(-12) if (!organizationId || !serviceToken) { return { ...INACTIVE } } try { // Resolve the org's linked business partner const orgFilter = `AD_Org_ID eq ${organizationId}` const orgUrl = `${config.api.url}/models/ad_org?$filter=${string.urlEncode(orgFilter)}&$select=C_BPartner_ID` const orgRes: any = await $fetch(orgUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + serviceToken } }) const bpartnerId = orgRes?.records?.[0]?.C_BPartner_ID?.id if (!bpartnerId) { return { ...INACTIVE, sessionKey } } // Read the two flags. Own try/catch: while the custom columns don't exist in // iDempiere yet, the $select errors — treat as inactive (fail-soft). let notice = false let terminated = false try { const bpartnerRes: any = await $fetch(`${config.api.url}/models/c_bpartner/${bpartnerId}?$select=IsTerminationNotice,IsTerminated`, { method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + serviceToken } }) const rawNotice = bpartnerRes?.IsTerminationNotice ?? bpartnerRes?.isTerminationNotice const rawTerminated = bpartnerRes?.IsTerminated ?? bpartnerRes?.isTerminated notice = rawNotice === true || rawNotice === 'Y' terminated = rawTerminated === true || rawTerminated === 'Y' } catch (err) { return { ...INACTIVE, sessionKey } } if (!notice && !terminated) { return { ...INACTIVE, sessionKey } } // Fetch open invoices of the partner. Do NOT scope by AD_Org_ID: fulfillment // invoices are issued BY LogYou TO the merchant, so they are booked under the // issuing LogYou org — never under the merchant's cookie org (see ffn-unpaid-count). const invoiceFilter = `C_BPartner_ID eq ${bpartnerId} and IsPaid eq 'N' and (DocStatus eq 'CO' or DocStatus eq 'CL')` const invoiceUrl = `${config.api.url}/models/C_Invoice?$filter=${string.urlEncode(invoiceFilter)}&$select=C_Invoice_ID,DocumentNo,DateInvoiced,GrandTotal,IsPaid,C_Currency_ID&$orderby=DateInvoiced asc` const res: any = await $fetch(invoiceUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + serviceToken } }) const records = res?.records || [] // Cross-check allocations (IsPaid can lag behind completed payments) let allocationSums: Record = {} if (records.length > 0) { try { const invoiceIds = records.map((inv: any) => inv.id) const allocUrl = `${config.api.url}/models/c_allocationline?$filter=${string.urlEncode(`C_Invoice_ID in (${invoiceIds.join(',')})`)}&$select=C_Invoice_ID,Amount,C_Payment_ID&$expand=C_Payment_ID($select=DocStatus)` const allocRes: any = await $fetch(allocUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + serviceToken } }) for (const alloc of allocRes?.records || []) { const invoiceId = alloc.C_Invoice_ID?.id const paymentDocStatus = alloc.C_Payment_ID?.DocStatus?.id || alloc.C_Payment_ID?.DocStatus || '' if (invoiceId && paymentDocStatus === 'CO') { allocationSums[invoiceId] = (allocationSums[invoiceId] || 0) + (alloc.Amount || 0) } } } catch (err) { // If allocations fail, treat all fetched invoices as unpaid } } const invoices: any[] = [] let totalOpen = 0 for (const inv of records) { const paidAmt = Math.abs(allocationSums[inv.id] || 0) const grandTotal = Math.abs(inv.GrandTotal || 0) const openAmt = grandTotal - paidAmt if (openAmt > 0.009) { invoices.push({ id: inv.id, documentNo: inv.DocumentNo, dateInvoiced: inv.DateInvoiced, grandTotal: inv.GrandTotal, openAmt: Math.round(openAmt * 100) / 100, currency: inv.C_Currency_ID?.identifier || 'EUR' }) totalOpen += openAmt } } return { notice, terminated, invoices, totalOpen: Math.round(totalOpen * 100) / 100, sessionKey } } catch (err) { console.warn('Could not fetch termination status:', err) return { ...INACTIVE, sessionKey } } } export default defineEventHandler(async (event) => { let data: any = {} try { data = await handleFunc(event) } catch (err: any) { try { let authToken: any = await refreshTokenHelper(event) data = await handleFunc(event, authToken) } catch (error) { data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) } } return data })