import fetchHelper from './fetchHelper' /** * Self-service access to the merchant's own company C_BPartner record * (used by /api/accounts/company-profile GET/PUT). * * Security model: * - A limited customer role ('c') is denied/filtered on ad_user / ad_org / * c_bpartner via REST, so all iDempiere reads/writes here use the service * token (config.api.idempieretoken) — same pattern as * materials/products/image-permission.get.ts and adUserCredentials.ts. * - BECAUSE the service token bypasses role security, the routes must never * forward the raw record or accept arbitrary fields. Everything that * crosses the browser boundary is projected through the whitelist below. * - The target partner is always resolved server-side from the CALLER's own * identity (JWT payload first, cookie fallback) — the client cannot choose * a partner id. */ /** * Whitelist of C_BPartner fields a merchant may see/edit through * /api/accounts/company-profile. Extend HERE when the merchant self-service * page gains fields — nothing outside this list ever crosses the network or * is written to iDempiere. * - column: DB column name, matched case-insensitively on reads * (iDempiere OData response casing varies by model) * - writeKey: lowercase-first key required by the iDempiere REST write API */ export const MERCHANT_PARTNER_FIELDS: Record = { eMailInvoice: { column: 'EMail_Invoice', writeKey: 'eMail_Invoice' } } // Case-insensitive column read — iDempiere response casing varies by model. export const readPartnerFieldCI = (record: any, column: string): any => { if (!record || typeof record !== 'object') return '' const target = column.toLowerCase() for (const k of Object.keys(record)) { if (k.toLowerCase() === target) return record[k] ?? '' } return '' } // Projects a raw c_bpartner record down to the whitelisted fields only. export const pickMerchantFields = (record: any): Record => { const out: Record = {} for (const [key, def] of Object.entries(MERCHANT_PARTNER_FIELDS)) { out[key] = readPartnerFieldCI(record, def.column) } return out } // The session JWT carries AD_User_ID — prefer it over the client-writable // logship_user_id cookie as the identity source. const decodeJwtPayload = (token: string): any => { try { const payload = token.split('.')[1] return JSON.parse(Buffer.from(payload, 'base64').toString()) } catch { return null } } /** * Resolves the caller's own company partner: * ad_user.AD_Org_ID -> ad_org.C_BPartner_ID (the org's linked partner, the * one fulfillment invoices are billed to), falling back to the user's own * ad_user.C_BPartner_ID when the org has no linked partner. * Returns { id, record } or null when no partner can be resolved. */ export const resolveMerchantPartner = async (event: any, serviceToken: string): Promise<{ id: number, record: any } | null> => { const sessionToken = getCookie(event, 'logship_it') const jwtUserId = sessionToken ? Number(decodeJwtPayload(sessionToken)?.AD_User_ID) : NaN const userId = Number.isFinite(jwtUserId) && jwtUserId > 0 ? jwtUserId : Number(getCookie(event, 'logship_user_id')) if (!Number.isFinite(userId) || userId <= 0) return null const userRes: any = await fetchHelper(event, `models/ad_user/${userId}?$select=AD_Org_ID,C_BPartner_ID`, 'GET', serviceToken, null) const orgId = userRes?.AD_Org_ID?.id if (orgId && Number(orgId) > 0) { const orgRes: any = await fetchHelper(event, `models/ad_org/${orgId}?$expand=C_BPartner_ID`, 'GET', serviceToken, null) const bpartner = orgRes?.C_BPartner_ID if (bpartner?.id) return { id: Number(bpartner.id), record: bpartner } } const userPartnerId = userRes?.C_BPartner_ID?.id if (userPartnerId && Number(userPartnerId) > 0) { const bpartner: any = await fetchHelper(event, `models/c_bpartner/${userPartnerId}`, 'GET', serviceToken, null) if (bpartner?.id) return { id: Number(bpartner.id), record: bpartner } } return null }