EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
fieldpineApi.ts
Go to the documentation of this file.
1// Server-side Fieldpine API helper
2// Handles all communication with Fieldpine APIs securely
3//
4// Fieldpine provides multiple API interfaces:
5//
6// 1. OpenAPI2 - RESTful JSON API for general retail operations
7// - URL: /OpenApi2/{Resource}
8// - Methods: GET, POST, PUT, DELETE
9// - Auth: X-Api-Key header or FieldpineApiKey cookie
10//
11// 2. eLink API (BUCK/DATI) - Comprehensive retail data API
12// - BUCK (GET/Read): /GNAP/j/buck?3={endpoint}&9={filters}
13// - DATI (POST/Write): /GNAP/j/buck (POST DATI XML in body)
14// - Auth: FieldpineApiKey cookie
15// - See: /docs/elink-api-reference.md for complete endpoint list
16//
17// 3. GNAP Protocol - Generic network application protocol
18// - Base: /GNAP/{method}/{endpoint}
19// - Format prefixes: j (JSON numeric), J (JSON named), M (metadata)
20//
21// This class provides unified access to all Fieldpine APIs with:
22// - Authentication management (API key / cookie)
23// - Rate limiting
24// - Error handling
25// - Type-safe method wrappers
26
27// ========================================
28// Fieldpine Constants & Helpers
29// Extracted from Fieldpine's reference implementation
30// ========================================
31
32// Field Number Constants
33export const FIELDPINE_FIELDS = {
34 // Product fields
35 PRODUCT_ID: 'f100',
36 PRODUCT_NAME: 'f101',
37 PRODUCT_PLU: 'f105',
38 PRODUCT_BARCODE: 'f501',
39
40 // Customer fields
41 CUSTOMER_ID: 'f100',
42 CUSTOMER_NAME: 'f101',
43 CUSTOMER_ACCOUNT_LINK: 'f132',
44
45 // Account fields
46 ACCOUNT_ID: 'f100',
47 ACCOUNT_NAME: 'f101',
48 ACCOUNT_CREDIT_LIMIT: 'f103',
49 ACCOUNT_FLOOR_LIMIT: 'f105',
50 ACCOUNT_BALANCE: 'f114',
51 ACCOUNT_RETIRED: 'f147',
52 ACCOUNT_EMAIL: 'f1100',
53 ACCOUNT_RECEIPT_FORMAT: 'f1105',
54 ACCOUNT_EXTERNAL_ID: 'f184',
55
56 // Sale fields
57 SALE_DATETIME: 'f110',
58 SALE_STORE_ID: 'f120',
59
60 // Location fields
61 LOCATION_ID: 'f100',
62 LOCATION_NAME: 'f101',
63
64 // Staff fields
65 STAFF_ID: 'f100',
66 STAFF_NAME: 'f101',
67} as const;
68
69// Field predicate operators
70export const FIELD_OPERATORS = {
71 EQUALS: '0',
72 NOT_EQUALS: '1',
73 GREATER_THAN: '4',
74 GREATER_THAN_EQUAL: '5',
75 CONTAINS: '8', // Default for text search
76 LIKE: 'like',
77 IN: 'in',
78 REGEX: 'r',
79} as const;
80
81// Goods In History transaction types
82export const GIH_TRANSACTION_TYPES = {
83 5: 'Stocktake Point',
84 100: 'Received',
85 102: 'Transfer Received',
86 200: 'Supplier Return',
87 201: 'Write Off',
88 202: 'Shrinkage',
89 203: 'Transfer Out',
90 204: 'Stocktake Adjustment',
91 210: 'Sales',
92} as const;
93
94/**
95 * Escape XML content for safe inclusion in DATI packets
96 */
97export function escapeXml(value: string | number): string {
98 return String(value)
99 .replace(/&/g, '&')
100 .replace(/</g, '&lt;')
101 .replace(/>/g, '&gt;')
102 .replace(/"/g, '&quot;')
103 .replace(/'/g, '&apos;');
104}
105
106/**
107 * Build a field predicate string for BUCK API queries
108 * @example buildFieldPredicate('f501', '0', '123456') // "f501,0,123456"
109 */
110export function buildFieldPredicate(fieldNum: string, operator: string, value: string | number): string {
111 return `${fieldNum},${operator},${value}`;
112}
113
114/**
115 * Convert GIH transaction type code to readable string
116 */
117export function gihTypeToString(typeCode: number): string {
118 return GIH_TRANSACTION_TYPES[typeCode as keyof typeof GIH_TRANSACTION_TYPES] || `Type ${typeCode}`;
119}
120
121interface RateLimitEntry {
122 count: number;
123 resetTime: number;
124}
125
126// Simple in-memory rate limiting (use Redis in production)
127const rateLimitMap = new Map<string, RateLimitEntry>();
128
129export class FieldpineServerApi {
130 private baseUrl: string;
131 private retailerId: string;
132 private apiKey: string;
133
134 constructor(baseUrl?: string, apiKey?: string) {
135 // Allow override via constructor (for session-based routing)
136 this.baseUrl = baseUrl || process.env.FIELDPINE_BASE_URL || "https://iig.cwanz.online";
137 this.retailerId = process.env.NEXT_PUBLIC_FIELDPINE_RETAILER_ID || "CWA";
138 this.apiKey = apiKey || process.env.FIELDPINE_API_KEY || process.env.FIELDPINE_AUTH_HEADER || "";
139
140 if (!this.apiKey) {
141 console.warn("FIELDPINE_API_KEY not set - using store-specific authentication");
142 }
143 }
144
145 /**
146 * Get base URL (useful for debugging)
147 */
148 getBaseUrl(): string {
149 return this.baseUrl;
150 }
151
152 // Rate limiting check
153 private checkRateLimit(clientId: string, limit: number = 100, windowMs: number = 60000): boolean {
154 const now = Date.now();
155 const entry = rateLimitMap.get(clientId);
156
157 if (!entry || now > entry.resetTime) {
158 rateLimitMap.set(clientId, { count: 1, resetTime: now + windowMs });
159 return true;
160 }
161
162 if (entry.count >= limit) {
163 return false;
164 }
165
166 entry.count++;
167 return true;
168 }
169
170 // Generic API call method with cookie support
171 public async apiCall(endpoint: string, options: {
172 method?: string;
173 body?: any;
174 params?: Record<string, any>;
175 useOpenApi?: boolean;
176 cookie?: string;
177 } = {}): Promise<any> {
178 const { method = "GET", body, params, useOpenApi = true, cookie } = options;
179
180 // Build URL
181 const apiBase = useOpenApi
182 ? `${this.baseUrl}/OpenApi2`
183 : this.baseUrl;
184 let url = `${apiBase}${endpoint}`;
185
186 if (params) {
187 const query = new URLSearchParams(params as Record<string, string>).toString();
188 url += (url.includes("?") ? "&" : "?") + query;
189 }
190
191 console.log('Making Fieldpine API call to:', url);
192
193 // Headers
194 const headers: Record<string, string> = {
195 "Accept": "application/json",
196 "User-Agent": "EverydayPOS/1.0"
197 };
198
199 // Add authentication - prefer cookie over API key
200 if (cookie) {
201 headers["Cookie"] = `FieldpineApiKey=${cookie}`;
202 } else if (this.apiKey && useOpenApi) {
203 headers["X-Api-Key"] = this.apiKey;
204 }
205
206 if (body) {
207 headers["Content-Type"] = "application/json";
208 }
209
210 try {
211 const response = await fetch(url, {
212 method,
213 headers,
214 body: body ? JSON.stringify(body) : undefined,
215 });
216
217 if (!response.ok) {
218 throw new Error(`Fieldpine API error: ${response.status} ${response.statusText}`);
219 }
220
221 return await response.json();
222 } catch (error) {
223 console.error(`Fieldpine API call failed:`, { url, error });
224 throw error;
225 }
226 }
227
228 /**
229 * Call Fieldpine BUCK (eLink) API
230 *
231 * eLink is Fieldpine's comprehensive retail API for reading/writing data.
232 * BUCK endpoints are used for GET operations.
233 *
234 * @param paramsOrEndpoint - Either:
235 * - Object with query params: {"3": "retailmax.elink.products", "9": "f501,0,barcode"}
236 * - String endpoint: "/buck?3=retailmax.elink.products&9=f501,0,barcode"
237 * @param cookie - API key for authentication (passed as FieldpineApiKey cookie)
238 *
239 * Common parameters:
240 * - "3" or "8": eLink endpoint name (e.g., "retailmax.elink.products")
241 * - "8": Limit (max records)
242 * - "9": Field predicate/filter (format: "f{fieldnum},{operator},{value}")
243 * - "10": Field selection (which fields to return)
244 * - "13": Dimensions for grouping
245 * - "15": Format options
246 * - "16": Response detail level (4 = detailed)
247 *
248 * Field operators:
249 * - 0: Equals
250 * - 4: Greater than
251 * - 5: Greater than or equal
252 * - like: Pattern match
253 * - in: In list
254 *
255 * @example
256 * // Get products by barcode
257 * buckApiCall({"3": "retailmax.elink.products", "10": "199", "9": "f501,0,123456"}, apiKey)
258 *
259 * // Get sales totals with string endpoint
260 * buckApiCall("/buck?3=retailmax.elink.sale.totals&9=f110,4,today", apiKey)
261 *
262 * @see /docs/elink-api-reference.md for full endpoint list
263 */
264 // BUCK API call method for alternative API access
265 async buckApiCall(
266 paramsOrEndpoint: Record<string, string | string[]> | string,
267 cookie?: string,
268 baseUrlOverride?: string
269 ): Promise<any> {
270 const baseUrl = baseUrlOverride || this.baseUrl;
271 let url: URL;
272
273 if (typeof paramsOrEndpoint === 'string') {
274 // If given a string starting with /, assume it's a relative path
275 if (paramsOrEndpoint.startsWith('/')) {
276 // Already has path, just prepend GNAP/j if not present
277 if (paramsOrEndpoint.includes('/GNAP/j/')) {
278 url = new URL(paramsOrEndpoint, baseUrl);
279 } else if (paramsOrEndpoint.startsWith('/buck')) {
280 url = new URL(`/GNAP/j${paramsOrEndpoint}`, baseUrl);
281 } else {
282 url = new URL(paramsOrEndpoint, baseUrl);
283 }
284 } else {
285 // Relative endpoint without /, add full path
286 url = new URL(`/GNAP/j/buck?${paramsOrEndpoint}`, baseUrl);
287 }
288 } else {
289 // Build URL from params Record
290 url = new URL('/GNAP/j/buck', baseUrl);
291
292 // Add all parameters to URL
293 Object.entries(paramsOrEndpoint).forEach(([key, value]) => {
294 if (Array.isArray(value)) {
295 value.forEach(v => url.searchParams.append(key, v));
296 } else {
297 url.searchParams.append(key, value);
298 }
299 });
300 }
301
302 const headers: Record<string, string> = {
303 'Accept': 'application/json',
304 };
305
306 if (cookie) {
307 headers['Cookie'] = `FieldpineApiKey=${cookie}`;
308 }
309
310 try {
311 const response = await fetch(url.toString(), {
312 method: 'GET',
313 headers,
314 });
315
316 if (!response.ok) {
317 // On 403 Forbidden, the API key has likely expired
318 if (response.status === 403) {
319 throw new Error(`BUCK API error: 403 Forbidden - API key expired or invalid. Please refresh the page and log in again.`);
320 }
321 throw new Error(`BUCK API error: ${response.status} ${response.statusText}`);
322 }
323
324 return await response.json();
325 } catch (error) {
326 console.error(`BUCK API call failed:`, { url: url.toString(), error });
327 throw error;
328 }
329 }
330
331 /**
332 * Call Fieldpine DATI API
333 *
334 * DATI is used for write operations (create, update, delete).
335 * Sends XML data to modify records in Fieldpine.
336 *
337 * @param xml - XML payload (DATI format)
338 * @param cookie - API key for authentication
339 *
340 * @example
341 * ```xml
342 * <DATI>
343 * <f8_s>retailmax.elink.customers.edit</f8_s>
344 * <f11_B>E</f11_B>
345 * <f100_E>12345</f100_E>
346 * <f101_s>John Smith</f101_s>
347 * </DATI>
348 * ```
349 */
350 async datiApiCall(xml: string, cookie?: string): Promise<{ success: boolean; data?: any; error?: string }> {
351 const url = new URL('/GNAP/j/buck', this.baseUrl);
352
353 const headers: Record<string, string> = {
354 'Content-Type': 'application/xml',
355 'Accept': 'application/json',
356 };
357
358 if (cookie) {
359 headers['Cookie'] = `FieldpineApiKey=${cookie}`;
360 }
361
362 try {
363 const response = await fetch(url.toString(), {
364 method: 'POST',
365 headers,
366 body: xml,
367 });
368
369 if (!response.ok) {
370 const errorText = await response.text();
371 console.error(`DATI API error: ${response.status}`, errorText);
372 return {
373 success: false,
374 error: `DATI API error: ${response.status} ${response.statusText}`,
375 };
376 }
377
378 const data = await response.json();
379
380 // Check if the response indicates success
381 if (data && (data.error || data.Status === 'Error')) {
382 return {
383 success: false,
384 error: data.error || data.Message || 'DATI operation failed',
385 };
386 }
387
388 return {
389 success: true,
390 data,
391 };
392 } catch (error: any) {
393 console.error(`DATI API call failed:`, error);
394 return {
395 success: false,
396 error: error.message || 'DATI API call failed',
397 };
398 }
399 }
400
401 // Authentication using correct CWA format
402 async authenticate(username: string, password: string, storeUrl?: string): Promise<{ success: boolean; data?: any; error?: string; cookie?: string }> {
403 try {
404 // Use provided store URL or fall back to default baseUrl
405 const authUrl = storeUrl || this.baseUrl;
406 console.log(`Attempting CWA authentication for user: ${username} at ${authUrl}`);
407
408 // Demo credentials for testing
409 if ((username === 'demo' && password === 'demo') ||
410 (username === 'admin' && password === 'admin')) {
411 console.log('Demo credentials accepted');
412 return {
413 success: true,
414 data: {
415 valid: 1,
416 authenticated: true,
417 user: username,
418 demo: true,
419 HomePage: '/pages/home'
420 }
421 };
422 }
423
424 // Use correct CWA login endpoint format
425 const loginUrl = `${authUrl}/cwlogin`;
426
427 const loginData = {
428 Name: username,
429 PassPlain: password,
430 Realm: 'site'
431 };
432
433 console.log(`Trying CWA auth at: ${loginUrl}`);
434
435 const response = await fetch(loginUrl, {
436 method: 'POST',
437 headers: {
438 'Content-Type': 'application/json',
439 'Accept': 'application/json',
440 'User-Agent': 'EverydayPOS/1.0'
441 },
442 body: JSON.stringify(loginData)
443 });
444
445 if (!response.ok) {
446 console.log(`Auth response failed with status: ${response.status}`);
447 return {
448 success: false,
449 error: `Authentication failed: Server returned ${response.status}`
450 };
451 }
452
453 // Extract the FieldpineApiKey cookie from response headers
454 const setCookieHeader = response.headers.get('set-cookie');
455 let fieldpineApiKey = '';
456
457 if (setCookieHeader) {
458 const cookieMatch = setCookieHeader.match(/FieldpineApiKey=([^;]+)/);
459 if (cookieMatch) {
460 fieldpineApiKey = cookieMatch[1];
461 console.log('Extracted FieldpineApiKey from cookie');
462 }
463 }
464
465 const result = await response.json();
466 console.log(`Auth response:`, result);
467
468 // Check for successful login using CWA format - also extract ApiKey from response
469 if (result.data && result.data.valid === 1) {
470 // Use ApiKey from response data if no cookie was found
471 if (!fieldpineApiKey && result.data.ApiKey) {
472 fieldpineApiKey = result.data.ApiKey;
473 console.log('Using ApiKey from response data');
474 }
475
476 return {
477 success: true,
478 data: {
479 valid: 1,
480 authenticated: true,
481 user: username,
482 HomePage: result.data.HomePage || '/pages/home',
483 ApiKey: result.data.ApiKey,
484 RmSystem: result.data.RmSystem,
485 ...result.data
486 },
487 cookie: fieldpineApiKey
488 };
489 }
490
491 return {
492 success: false,
493 error: `Invalid credentials`
494 };
495
496 } catch (error) {
497 console.error('CWA Authentication error:', error);
498 return {
499 success: false,
500 error: `Authentication service error: ${error instanceof Error ? error.message : 'Unknown error'}`
501 };
502 }
503 }
504
505 // Product operations with optional cookie authentication
506 async getProducts(params: { search?: string; plu?: string; barcode?: string; limit?: number } = {}, cookie?: string): Promise<any> {
507 try {
508 // Map 'search' to 'EnglishQuery' for OpenAPI compatibility
509 const apiParams: Record<string, string> = {};
510 if (params.search) apiParams.EnglishQuery = params.search;
511 if (params.plu) apiParams.plu = params.plu;
512 if (params.barcode) apiParams.filter = `barcode=${params.barcode}`;
513 if (params.limit) apiParams.limit = params.limit.toString();
514
515 // Try main API first
516 return await this.apiCall("/Products", { params: apiParams, cookie });
517 } catch (error) {
518 console.log('Fieldpine API call failed:', { url: `${this.baseUrl}/OpenApi2/Products`, error });
519 }
520
521 try {
522 // Try BUCK API fallback with consistent method
523 const searchTerm = params.search || params.plu || params.barcode || '';
524 let buckParams: Record<string, string> = {
525 "3": "retailmax.elink.products.list",
526 "8": (params.limit || 30).toString(),
527 "9": `f101,8,${encodeURIComponent(searchTerm)}` // f101 = Name/Description field
528 };
529
530 console.log('Trying BUCK API fallback:', `${this.baseUrl}/GNAP/j/buck?${Object.entries(buckParams).map(([k, v]) => `${k}=${v}`).join('&')}`);
531
532 const buckResponse = await this.buckApiCall(buckParams, cookie);
533 console.log('BUCK API Response:', JSON.stringify(buckResponse, null, 2));
534
535 // Handle BUCK response format
536 if (buckResponse && buckResponse.RootType === "ARAY") {
537 if (buckResponse.DATS && Array.isArray(buckResponse.DATS)) {
538 const products = buckResponse.DATS.map((item: any) => ({
539 pid: item.f100,
540 name: item.f101 || item.f500,
541 barcode: item.f105,
542 sellprice: item.f103,
543 stocklevel: item.f106 || 0,
544 category: item.f106
545 }));
546
547 console.log('Products API Response:', { data: products });
548 console.log('Final product data:', JSON.stringify(products, null, 2));
549 return { data: products };
550 } else {
551 // Empty result from BUCK API
552 console.log('BUCK API returned empty result');
553 console.log('Products API Response:', { data: [] });
554 console.log('Final product data:', []);
555 return { data: [] };
556 }
557 }
558
559 console.log('BUCK response does not match expected format, returning raw result');
560 console.log('Products API Response:', buckResponse);
561 console.log('Final product data:', []);
562 return { data: [] };
563 } catch (buckError) {
564 console.log('BUCK API also failed, returning empty result');
565 console.log('Final product data:', []);
566 return { data: [] };
567 }
568 }
569
570 async getProductById(id: string, cookie?: string): Promise<any> {
571 return this.apiCall(`/Product/${id}`, { cookie });
572 }
573
574 // Sales operations with optional cookie authentication
575 async createSale(saleData: any, cookie?: string, storeUrl?: string): Promise<any> {
576 const baseUrl = storeUrl || this.baseUrl;
577 const url = new URL('/OpenApi2/Sales', baseUrl);
578
579 const headers: Record<string, string> = {
580 'Content-Type': 'application/json',
581 'Accept': 'application/json',
582 'X-Fieldpine-CallerHandling': 'audit'
583 };
584
585 if (cookie) {
586 headers['Cookie'] = `FieldpineApiKey=${cookie}`;
587 }
588
589 console.log('Making Fieldpine Sale POST to:', url.toString());
590 console.log('Sale payload:', JSON.stringify(saleData, null, 2));
591
592 const response = await fetch(url.toString(), {
593 method: 'POST',
594 headers,
595 body: JSON.stringify(saleData)
596 });
597
598 if (!response.ok) {
599 const errorText = await response.text();
600 console.error('Sale creation failed:', response.status, errorText);
601 throw new Error(`Fieldpine API error: ${response.status} ${response.statusText}`);
602 }
603
604 return await response.json();
605 }
606
607 // Location operations with optional cookie authentication
608 async getLocations(params: { limit?: number } = {}, cookie?: string): Promise<any> {
609 try {
610 return await this.apiCall("/Locations", { params, cookie });
611 } catch (error) {
612 console.warn('Location API not available, returning empty result');
613 return { data: [] };
614 }
615 }
616
617 // Supplier operations with optional cookie authentication
618 async getSuppliers(params: { search?: string; limit?: number } = {}, cookie?: string): Promise<any> {
619 try {
620 // Try BUCK API first with format that works
621 let buckParams: Record<string, string> = {
622 "3": "retailmax.elink.suppliers",
623 "16": "4",
624 "20": (params.limit || 100).toString(),
625 "99": Math.random().toString()
626 };
627
628 const buckResponse = await this.buckApiCall(buckParams, cookie);
629 console.log('BUCK Suppliers API Response:', buckResponse);
630
631 // Handle BUCK response format
632 if (buckResponse && buckResponse.RootType === "ARAY" && buckResponse.DATS && Array.isArray(buckResponse.DATS)) {
633 return {
634 data: buckResponse.DATS
635 };
636 } else {
637 console.log('BUCK suppliers response contains no data or wrong format, trying main API');
638 }
639 } catch (buckError) {
640 console.warn('BUCK suppliers API failed, trying main API:', buckError);
641 }
642
643 try {
644 // Fallback to main API - but clean search parameter first
645 const cleanParams = { ...params };
646 if (cleanParams.search === 'undefined' || !cleanParams.search?.trim()) {
647 delete cleanParams.search;
648 }
649 return await this.apiCall("/Supplier", { params: cleanParams, cookie });
650 } catch (error) {
651 console.warn('Supplier API not available, returning empty result');
652 return { data: [] };
653 }
654 }
655
656 // Customer operations with optional cookie authentication
657 async getCustomers(params: { search?: string; limit?: number } = {}, cookie?: string): Promise<any> {
658 try {
659 // Try BUCK API first with format that works
660 let buckParams: Record<string, string> = {
661 "3": "retailmax.elink.customers",
662 "16": "4",
663 "20": (params.limit || 100).toString(),
664 "99": Math.random().toString()
665 };
666
667 const buckResponse = await this.buckApiCall(buckParams, cookie);
668 console.log('BUCK Customers API Response:', buckResponse);
669
670 // Handle BUCK response format
671 if (buckResponse && buckResponse.RootType === "ARAY" && buckResponse.DATS && Array.isArray(buckResponse.DATS)) {
672 return {
673 data: buckResponse.DATS
674 };
675 } else {
676 console.log('BUCK customers response contains no data or wrong format, trying main API');
677 }
678 } catch (buckError) {
679 console.warn('BUCK customers API failed, trying main API:', buckError);
680 }
681
682 try {
683 // Fallback to main API - but clean search parameter first
684 const cleanParams = { ...params };
685 if (cleanParams.search === 'undefined' || !cleanParams.search?.trim()) {
686 delete cleanParams.search;
687 }
688 return await this.apiCall("/Customer", { params: cleanParams, cookie });
689 } catch (error) {
690 console.warn('Customer API not available, returning empty result');
691 return { data: [] };
692 }
693 }
694
695 async getCustomerById(customerId: string | number, cookie?: string): Promise<any> {
696 try {
697 // Use BUCK API to get a single customer with account details
698 let buckParams: Record<string, string> = {
699 "3": "retailmax.elink.customers",
700 "9": `f100,0,${customerId}`,
701 "10": "132,1130,153,154,1131,1132", // Additional fields including account name fields
702 "99": Math.random().toString()
703 };
704
705 const buckResponse = await this.buckApiCall(buckParams, cookie);
706 console.log('BUCK Customer By ID API Response:', buckResponse);
707
708 // Handle BUCK response format
709 if (buckResponse && buckResponse.DATS && Array.isArray(buckResponse.DATS)) {
710 const customerData = buckResponse.DATS[0];
711
712 // If customer has an account link (f132), fetch the account details
713 if (customerData && customerData.f132 && customerData.f132 > 0) {
714 try {
715 const accountParams: Record<string, string> = {
716 "3": "retailmax.elink.accounts",
717 "9": `f100,0,${customerData.f132}`,
718 "99": Math.random().toString()
719 };
720
721 const accountResponse = await this.buckApiCall(accountParams, cookie);
722 console.log('Account details response:', accountResponse);
723
724 if (accountResponse && accountResponse.DATS && accountResponse.DATS[0]) {
725 // Add account name to customer data
726 customerData.accountName = accountResponse.DATS[0].f101;
727 customerData.accountBalance = accountResponse.DATS[0].f114 || 0;
728 }
729 } catch (error) {
730 console.error('Failed to fetch account details:', error);
731 }
732 }
733
734 return buckResponse.DATS;
735 }
736
737 return [];
738 } catch (error) {
739 console.error('Failed to fetch customer by ID:', error);
740 return [];
741 }
742 }
743
744 // Rate limit check for client requests
745 checkClientRateLimit(clientId: string): boolean {
746 return this.checkRateLimit(clientId, 100, 60000); // 100 requests per minute
747 }
748
749 // ========================================
750 // eLink API Helper Methods
751 // ========================================
752 // Based on official Fieldpine eLink API documentation
753 // See: /docs/elink-api-reference.md
754
755 /**
756 * Get product stock levels
757 * eLink: retailmax.elink.product.stocklevels
758 */
759 async getProductStockLevels(productId: string, cookie?: string): Promise<any> {
760 return this.buckApiCall({
761 "3": "retailmax.elink.product.stocklevels",
762 "9": `f100,0,${productId}`
763 }, cookie);
764 }
765
766 /**
767 * Get all product stock levels (no product filter)
768 * eLink: retailmax.elink.product.stocklevels
769 * Returns DATS array with Pid, Description, Qty, Locid, LocationName, DispatchWait
770 */
771 async getAllProductStockLevels(cookie?: string): Promise<any> {
772 return this.buckApiCall({
773 "3": "retailmax.elink.product.stocklevels",
774 "100": "2" // SelectionRange: specific
775 }, cookie);
776 }
777
778 /**
779 * Get product media/images
780 * eLink: retailmax.elink.media.list
781 */
782 async getProductMedia(params: { productId?: string; limit?: number } = {}, cookie?: string): Promise<any> {
783 const buckParams: Record<string, string> = {
784 "3": "retailmax.elink.media.list"
785 };
786
787 if (params.productId) buckParams["9"] = `f100,0,${params.productId}`;
788 if (params.limit) buckParams["8"] = params.limit.toString();
789
790 return this.buckApiCall(buckParams, cookie);
791 }
792
793 /**
794 * Get loyalty card details
795 * eLink: retailmax.elink.cardinquiry
796 */
797 async getCardInquiry(cardNumber: string, cookie?: string): Promise<any> {
798 return this.buckApiCall({
799 "3": "retailmax.elink.cardinquiry",
800 "9": `f100,0,${cardNumber}`
801 }, cookie);
802 }
803
804 /**
805 * Get staff list
806 * eLink: retailmax.elink.staff.list
807 */
808 async getStaffList(params: { limit?: number; search?: string } = {}, cookie?: string): Promise<any> {
809 const buckParams: Record<string, string> = {
810 "3": "retailmax.elink.staff.list",
811 "16": "4"
812 };
813
814 if (params.limit) buckParams["8"] = params.limit.toString();
815 if (params.search) buckParams["9"] = `f101,like,${params.search}`;
816
817 return this.buckApiCall(buckParams, cookie);
818 }
819
820 /**
821 * Get staff usage/activity
822 * eLink: retailmax.elink.staff.used.list
823 */
824 async getStaffUsage(cookie?: string): Promise<any> {
825 return this.buckApiCall({
826 "3": "retailmax.elink.staff.used.list"
827 }, cookie);
828 }
829
830 /**
831 * Get department statistics for today
832 * eLink: retailmax.elink.stats.today.department
833 */
834 async getDepartmentStatsToday(cookie?: string): Promise<any> {
835 return this.buckApiCall({
836 "3": "retailmax.elink.stats.today.department"
837 }, cookie);
838 }
839
840 /**
841 * Get product Pareto analysis (top sellers)
842 * eLink: retailmax.elink.product.paretolist
843 */
844 async getProductPareto(params: { limit?: number; storeId?: string } = {}, cookie?: string): Promise<any> {
845 const buckParams: Record<string, string> = {
846 "3": "retailmax.elink.product.paretolist"
847 };
848
849 if (params.limit) buckParams["8"] = params.limit.toString();
850 if (params.storeId) buckParams["9"] = `f101,0,${params.storeId}`;
851
852 return this.buckApiCall(buckParams, cookie);
853 }
854
855 /**
856 * Get flat sale list (denormalized sales with items and payments)
857 * eLink: retailmax.elink.saleflat.list
858 */
859 async getSaleFlatList(params: {
860 limit?: number;
861 startDate?: string;
862 endDate?: string;
863 storeId?: string;
864 } = {}, cookie?: string): Promise<any> {
865 const buckParams: Record<string, string> = {
866 "3": "retailmax.elink.saleflat.list"
867 };
868
869 if (params.limit) buckParams["8"] = params.limit.toString();
870 if (params.startDate) buckParams["9"] = `f110,5,${params.startDate}`;
871 if (params.storeId) buckParams["9"] = `f120,0,${params.storeId}`;
872
873 return this.buckApiCall(buckParams, cookie);
874 }
875
876 /**
877 * Get single sale details
878 * eLink: retailmax.elink.sale.fetch
879 */
880 async getSaleById(saleId: string, cookie?: string): Promise<any> {
881 return this.buckApiCall({
882 "3": "retailmax.elink.sale.fetch",
883 "9": `f100,0,${saleId}`
884 }, cookie);
885 }
886
887 /**
888 * Get price map list
889 * eLink: retailmax.elink.pricemap.list
890 */
891 async getPriceMapList(params: { limit?: number } = {}, cookie?: string): Promise<any> {
892 const buckParams: Record<string, string> = {
893 "3": "retailmax.elink.pricemap.list"
894 };
895
896 if (params.limit) buckParams["8"] = params.limit.toString();
897
898 return this.buckApiCall(buckParams, cookie);
899 }
900
901 /**
902 * Decode supplier pricebook
903 * eLink: retailmax.elink.supplier.pricebook.decode
904 */
905 async decodeSupplierPricebook(supplierId: string, cookie?: string): Promise<any> {
906 return this.buckApiCall({
907 "3": "retailmax.elink.supplier.pricebook.decode",
908 "9": `f100,0,${supplierId}`
909 }, cookie);
910 }
911}
912
913export const fieldpineServerApi = new FieldpineServerApi();