erpClient.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. /**
  2. * ERP Bridge HTTP client
  3. * Calls the FastAPI erp-bridge service running at 127.0.0.1:8080 (internal only).
  4. * All requests are authenticated with X-API-Key header.
  5. */
  6. import { ENV } from "./_core/env";
  7. const TIMEOUT_MS = 8_000;
  8. /** Caller identity forwarded to the ERP bridge for permission scoping. */
  9. export interface UserCtx {
  10. role: string; // "admin" | "agent" | "user"
  11. erpContactCid?: string | null; // ERP ContactID for dealer-role scoping
  12. }
  13. /** Generic fetch wrapper with timeout and error handling */
  14. async function erpFetch<T>(
  15. path: string,
  16. options: RequestInit = {},
  17. userCtx?: UserCtx
  18. ): Promise<T> {
  19. const controller = new AbortController();
  20. const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
  21. const url = `${ENV.erpApiUrl}${path}`;
  22. const roleHeader = userCtx?.role ?? "user";
  23. const cidHeader = userCtx?.erpContactCid ?? "";
  24. try {
  25. const res = await fetch(url, {
  26. ...options,
  27. signal: controller.signal,
  28. headers: {
  29. "Content-Type": "application/json",
  30. "X-API-Key": ENV.erpApiKey,
  31. "X-User-Role": roleHeader,
  32. "X-Customer-CID": cidHeader,
  33. ...(options.headers ?? {}),
  34. },
  35. });
  36. if (!res.ok) {
  37. const text = await res.text().catch(() => "");
  38. throw new Error(`ERP bridge ${res.status} for ${path}: ${text}`);
  39. }
  40. return (await res.json()) as T;
  41. } catch (err: unknown) {
  42. if ((err as Error).name === "AbortError") {
  43. throw new Error(`ERP bridge timeout after ${TIMEOUT_MS}ms for ${path}`);
  44. }
  45. throw err;
  46. } finally {
  47. clearTimeout(timer);
  48. }
  49. }
  50. // ─── Request / response types ─────────────────────────────────────────────────
  51. export interface CatalogItem {
  52. Model: string | null;
  53. Manufacturer: string | null;
  54. Description: string | null;
  55. Type: string | null;
  56. Department: string | null;
  57. Color: string | null;
  58. UOM: string | null;
  59. Status: string | null;
  60. StockQTY: number | null;
  61. EstimatedValue: number | null;
  62. }
  63. export interface ContactRecord {
  64. ContactID: string | null;
  65. Company: string | null;
  66. ContactName1: string | null;
  67. Phone1: string | null;
  68. Email1: string | null;
  69. Address1: string | null;
  70. City: string | null;
  71. State: string | null;
  72. Country: string | null;
  73. Sales1: string | null;
  74. AR_CreditHold: boolean | null;
  75. Active: boolean | null;
  76. Terms: string | null;
  77. PriceType: string | null;
  78. }
  79. export interface OrderListItem {
  80. SOID: string | null;
  81. OrderType: string | null;
  82. Status: string | null;
  83. SubStatus: string | null;
  84. CustomerName: string | null;
  85. CustomerCID: string | null;
  86. POID: string | null;
  87. Model: string | null;
  88. Quantity: number | null;
  89. UnitPrice: number | null;
  90. TotalPrice: number | null;
  91. InvoiceNo: string | null;
  92. InvoiceDate: string | null;
  93. TrackingNumber: string | null;
  94. ETD: string | null;
  95. ShipVia: string | null;
  96. Carrier: string | null;
  97. SODate: string | null;
  98. ScheduleDate: string | null;
  99. }
  100. export interface OrderItem {
  101. LineNo: number | null;
  102. ItemID: string | null;
  103. Model: string | null;
  104. Manufacturer: string | null;
  105. Description: string | null;
  106. Quantity: number | null;
  107. UnitPrice: number | null;
  108. TotalPrice: number | null;
  109. Status: string | null;
  110. StatusDesc: string | null;
  111. ETD: string | null;
  112. ItemNote: string | null;
  113. WarehouseID: number | null;
  114. InvoiceQuantity: number | null;
  115. TransactedQuantity: number | null;
  116. }
  117. export interface OrderNote {
  118. ID: number;
  119. GeneralNote: string | null;
  120. DeliveryCarrier: string | null;
  121. CODCharges: number | null;
  122. PickOrderNote: string | null;
  123. InternalNote: string | null;
  124. AccNotes: string | null;
  125. Pickslip: string | null;
  126. CreateUser: string | null;
  127. CreateTime: string | null;
  128. }
  129. export interface OrderDetail {
  130. SOID: string | null;
  131. POID: string | null;
  132. OrderType: string | null;
  133. Status: string | null;
  134. StatusDesc: string | null;
  135. CustomerName: string | null;
  136. CustomerCID: string | null;
  137. ReceiverName: string | null;
  138. ReceiverAddress1: string | null;
  139. ReceiverCity: string | null;
  140. ReceiverState: string | null;
  141. ReceiverCountry: string | null;
  142. ReceiverZipcode: string | null;
  143. ShipVia: string | null;
  144. Shipper: string | null;
  145. ScheduleDate: string | null;
  146. ArrivalDate: string | null;
  147. Notes: string | null;
  148. InternalNote: string | null;
  149. ModifyTime: string | null;
  150. Items: OrderItem[];
  151. Notes_: OrderNote[]; // mapped from "Notes" array in the response
  152. }
  153. export interface StockRecord {
  154. Model: string | null;
  155. Manufacturer: string | null;
  156. Description: string | null;
  157. Quantity: number | null;
  158. PalletID: string | null;
  159. WarehouseCID: string | null;
  160. SubInv: string | null;
  161. HoldByPalletID: string | null;
  162. HoldByRef: string | null;
  163. B_ETA: string | null;
  164. A_ETA: string | null;
  165. SalesOrderID: string | null;
  166. }
  167. // ─── API calls ────────────────────────────────────────────────────────────────
  168. export interface CatalogParams {
  169. model?: string;
  170. manufacturer?: string;
  171. description?: string;
  172. category?: string;
  173. status?: string;
  174. limit?: number;
  175. }
  176. export async function fetchCatalog(params: CatalogParams, userCtx?: UserCtx): Promise<CatalogItem[]> {
  177. return erpFetch<CatalogItem[]>("/catalog", {
  178. method: "POST",
  179. body: JSON.stringify({ limit: 20, ...params }),
  180. }, userCtx);
  181. }
  182. export interface ContactParams {
  183. contact_id?: string;
  184. company?: string;
  185. name?: string;
  186. limit?: number;
  187. }
  188. export async function fetchContacts(params: ContactParams, userCtx?: UserCtx): Promise<ContactRecord[]> {
  189. return erpFetch<ContactRecord[]>("/contacts", {
  190. method: "POST",
  191. body: JSON.stringify({ limit: 10, ...params }),
  192. }, userCtx);
  193. }
  194. export interface OrderListParams {
  195. so_id?: string;
  196. customer_name?: string;
  197. customer_cid?: string;
  198. po_id?: string;
  199. status?: string;
  200. limit?: number;
  201. }
  202. export async function fetchOrdersList(params: OrderListParams, userCtx?: UserCtx): Promise<OrderListItem[]> {
  203. return erpFetch<OrderListItem[]>("/orders", {
  204. method: "POST",
  205. body: JSON.stringify({ limit: 20, ...params }),
  206. }, userCtx);
  207. }
  208. export async function fetchOrderDetail(soId: string, userCtx?: UserCtx): Promise<OrderDetail | null> {
  209. try {
  210. return await erpFetch<OrderDetail>(`/orders/${encodeURIComponent(soId)}`, {}, userCtx);
  211. } catch (err: unknown) {
  212. if ((err as Error).message?.includes("404")) return null;
  213. throw err;
  214. }
  215. }
  216. export interface StockParams {
  217. model?: string;
  218. warehouse_cid?: string;
  219. limit?: number;
  220. }
  221. export async function fetchStock(params: StockParams, userCtx?: UserCtx): Promise<StockRecord[]> {
  222. return erpFetch<StockRecord[]>("/stock", {
  223. method: "POST",
  224. body: JSON.stringify({ limit: 50, ...params }),
  225. }, userCtx);
  226. }