import type { AuthPermission, AuthUser } from "./types"; /** * Mirror of the API's FINANCE_PERMS registry * (apps/finance-api/src/seed/finance-permissions.registry.ts). * * Kept as a hand-written mirror rather than an import: the API is a separate * deployable and this app must not take a build dependency on it. The keys are a * wire contract, so drift here surfaces as a UI control that is visible but * 403s — which is why the server check is the real gate and this is only used to * hide or disable controls. */ export const FINANCE_PERMS = { account: { view: "can:view:gl_account", manage: "can:manage:gl_account", }, journal: { create: "can:create:journal_entry", view: "can:view:journal_entry", post: "can:post:journal_entry", reverse: "can:reverse:journal_entry", }, period: { view: "can:view:fiscal_period", manage: "can:manage:fiscal_period", close: "can:close:fiscal_period", }, receivable: { view: "can:view:receivable", manageCustomer: "can:manage:finance_customer", recordReceipt: "can:record:receipt", manageRevenueMapping: "can:manage:revenue_mapping", }, payable: { view: "can:view:payable", manageSupplier: "can:manage:supplier", manageBill: "can:manage:supplier_bill", approveBill: "can:approve:supplier_bill", recordPayment: "can:record:supplier_payment", postPayroll: "can:post:payroll_to_gl", manageStatutory: "can:manage:statutory_payable", }, budget: { view: "can:view:budget", manage: "can:manage:budget", approve: "can:approve:budget", manageCostCenter: "can:manage:cost_center", }, asset: { view: "can:view:fixed_asset", manage: "can:manage:fixed_asset", depreciate: "can:run:depreciation", dispose: "can:dispose:fixed_asset", }, report: { view: "can:view:finance_report", export: "can:export:finance_report", }, } as const; const SUPER_ADMIN_ROLE = "super_admin"; const positionsOf = (user: AuthUser | null | undefined) => { const employee = user?.employee; if (!employee) return []; if (Array.isArray(employee)) return employee.flatMap((e) => e.positions ?? []); return [ ...(employee.position ? [employee.position] : []), ...(employee.positions ?? []), ...(employee.delegatedPositions ?? []), ]; }; const keysOf = (permissions: AuthPermission[] | undefined) => (permissions ?? []).map((p) => p.key).filter((k): k is string => Boolean(k)); /** Every permission key the token carries — roles, positions, position types. */ export const collectPermissionKeys = ( user: AuthUser | null | undefined, ): string[] => { if (!user) return []; const keys = new Set(keysOf(user.permissions)); for (const position of positionsOf(user)) { keysOf(position.permissions).forEach((key) => keys.add(key)); keysOf(position.positionType?.permissions).forEach((key) => keys.add(key)); } return [...keys]; }; export const isSuperAdmin = (user: AuthUser | null | undefined): boolean => Boolean(user?.roles?.some((role) => role.key === SUPER_ADMIN_ROLE)); /** An array is "any of" — kept in step with edr-hr-web, where a screen * reachable by either of two permissions made the widening necessary. */ export const hasPermission = ( user: AuthUser | null | undefined, permission: string | string[], ): boolean => { if (!user) return false; if (isSuperAdmin(user)) return true; const keys = collectPermissionKeys(user); return Array.isArray(permission) ? permission.some((p) => keys.includes(p)) : keys.includes(permission); }; /** The caller's `iam.employees.id` — who a posting is recorded against. */ export const currentEmployeeId = ( user: AuthUser | null | undefined, ): string | null => { const employee = user?.employee; if (!employee) return null; if (Array.isArray(employee)) return employee[0]?.id ?? null; return employee.id ?? null; };