erpClient.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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. export interface ProductImageRecord {
  168. model: string;
  169. apppicture_path: string;
  170. full_url: string;
  171. }
  172. export interface ProductImagesResult {
  173. base_url: string;
  174. images: ProductImageRecord[];
  175. }
  176. // ─── API calls ────────────────────────────────────────────────────────────────
  177. export interface CatalogParams {
  178. model?: string;
  179. manufacturer?: string;
  180. description?: string;
  181. category?: string;
  182. status?: string;
  183. limit?: number;
  184. }
  185. export async function fetchCatalog(params: CatalogParams, userCtx?: UserCtx): Promise<CatalogItem[]> {
  186. return erpFetch<CatalogItem[]>("/catalog", {
  187. method: "POST",
  188. body: JSON.stringify({ limit: 20, ...params }),
  189. }, userCtx);
  190. }
  191. export interface ContactParams {
  192. contact_id?: string;
  193. company?: string;
  194. name?: string;
  195. limit?: number;
  196. }
  197. export async function fetchContacts(params: ContactParams, userCtx?: UserCtx): Promise<ContactRecord[]> {
  198. return erpFetch<ContactRecord[]>("/contacts", {
  199. method: "POST",
  200. body: JSON.stringify({ limit: 10, ...params }),
  201. }, userCtx);
  202. }
  203. export interface OrderListParams {
  204. so_id?: string;
  205. customer_name?: string;
  206. customer_cid?: string;
  207. po_id?: string;
  208. status?: string;
  209. limit?: number;
  210. }
  211. export async function fetchOrdersList(params: OrderListParams, userCtx?: UserCtx): Promise<OrderListItem[]> {
  212. return erpFetch<OrderListItem[]>("/orders", {
  213. method: "POST",
  214. body: JSON.stringify({ limit: 20, ...params }),
  215. }, userCtx);
  216. }
  217. export async function fetchOrderDetail(soId: string, userCtx?: UserCtx): Promise<OrderDetail | null> {
  218. try {
  219. return await erpFetch<OrderDetail>(`/orders/${encodeURIComponent(soId)}`, {}, userCtx);
  220. } catch (err: unknown) {
  221. if ((err as Error).message?.includes("404")) return null;
  222. throw err;
  223. }
  224. }
  225. export interface StockParams {
  226. model?: string;
  227. warehouse_cid?: string;
  228. limit?: number;
  229. }
  230. export async function fetchStock(params: StockParams, userCtx?: UserCtx): Promise<StockRecord[]> {
  231. return erpFetch<StockRecord[]>("/stock", {
  232. method: "POST",
  233. body: JSON.stringify({ limit: 50, ...params }),
  234. }, userCtx);
  235. }
  236. export interface ProductImagesParams {
  237. model: string;
  238. limit?: number;
  239. }
  240. export async function fetchProductImages(params: ProductImagesParams, userCtx?: UserCtx): Promise<ProductImagesResult> {
  241. return erpFetch<ProductImagesResult>("/catalog/images", {
  242. method: "POST",
  243. body: JSON.stringify({ limit: 10, ...params }),
  244. }, userCtx);
  245. }