EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
route.ts
Go to the documentation of this file.
1import { NextRequest, NextResponse } from 'next/server';
2import { fieldpineServerApi } from '@/lib/server/fieldpineApi';
3import { getRequestContext, validateApiAccess } from '@/lib/server/sessionUtils';
4
5/**
6 * Products Endpoint (ELINK API)
7 * GET /api/v1/openapi/products
8 *
9 * Uses Fieldpine's ELINK API (retailmax.elink.products.list)
10 * Security: Retail stores can access this (ELINK only)
11 */
12export async function GET(request: NextRequest) {
13 try {
14 // 1. Get session and store context
15 const context = await getRequestContext(request);
16
17 if (!context || !context.isAuthenticated) {
18 return NextResponse.json(
19 { error: 'Authentication required' },
20 { status: 401 }
21 );
22 }
23
24 // 2. SECURITY: Validate ELINK access
25 const apiAccessValidation = validateApiAccess(context, 'elink');
26 if (!apiAccessValidation.valid) {
27 console.warn(`[API Security] ELINK access denied: ${apiAccessValidation.error}`);
28 return NextResponse.json(
29 {
30 error: apiAccessValidation.error,
31 code: apiAccessValidation.errorCode
32 },
33 { status: 403 }
34 );
35 }
36
37 // 3. Rate limiting
38 const clientId = request.headers.get('x-forwarded-for') ||
39 request.headers.get('x-real-ip') ||
40 context.session.userId ||
41 'unknown';
42 if (!fieldpineServerApi.checkClientRateLimit(clientId)) {
43 return NextResponse.json(
44 { error: 'Rate limit exceeded' },
45 { status: 429 }
46 );
47 }
48
49 // 4. Parse query parameters
50 const { searchParams } = new URL(request.url);
51 const search = searchParams.get('search') || searchParams.get('EnglishQuery');
52 const plu = searchParams.get('plu');
53 const barcode = searchParams.get('barcode');
54 const limit = searchParams.get('limit') || searchParams.get('$top') || '50';
55 const skip = searchParams.get('skip') || searchParams.get('$skip');
56 const filter = searchParams.get('filter') || searchParams.get('$filter');
57
58 console.log(`[Products API] Search params:`, { search, plu, barcode, limit, skip, filter });
59
60 // 5. Build ELINK BUCK parameters
61 const buckParams: Record<string, string | string[]> = {
62 "3": "retailmax.elink.products.list",
63 "8": limit,
64 "99": Math.random().toString() // Cache buster
65 };
66
67 // Add search filters
68 // Field reference: f105=barcode, f102=PLU, f101=name/description, f100=product ID
69 if (barcode) {
70 buckParams["9"] = `f105,8,${encodeURIComponent(barcode)}`;
71 } else if (plu) {
72 buckParams["9"] = `f102,0,${encodeURIComponent(plu)}`;
73 } else if (search) {
74 buckParams["9"] = `f101,8,${encodeURIComponent(search)}`;
75 } else if (filter) {
76 // Handle OData-style filters if needed
77 buckParams["9"] = filter;
78 }
79
80 // 6. Make ELINK API call using singleton
81 console.log(`[Products API] Session data:`, {
82 hasApiKey: !!context.session.apiKey,
83 apiKeyLength: context.session.apiKey?.length,
84 storeUrl: context.store.url,
85 userId: context.session.userId
86 });
87
88 if (!context.session.apiKey) {
89 console.error('[Products API] No API key in session!');
90 return NextResponse.json(
91 { error: 'Session expired or invalid. Please log in again.' },
92 { status: 401 }
93 );
94 }
95
96 // Use store-specific URL for API calls
97 const buckResponse = await fieldpineServerApi.buckApiCall(
98 buckParams,
99 context.session.apiKey,
100 context.store.url
101 );
102
103 // 7. Transform BUCK response to OData format
104 if (buckResponse && buckResponse.RootType === "ARAY") {
105 let products: any[] = [];
106
107 if (buckResponse.DATS && Array.isArray(buckResponse.DATS)) {
108 products = buckResponse.DATS.map((item: any) => ({
109 Pid: item.f100,
110 Name: item.f101 || item.f500,
111 Description: item.f500,
112 Barcode: item.f105,
113 SecondaryBarcode: item.f501,
114 PLU: item.f102,
115 SellPrice: item.f103,
116 Price: item.f103,
117 LastCost: item.f108,
118 Cost: item.f150,
119 CostPrice: item.f150,
120 StockLevel: item.f130 || 0,
121 StockMin: item.f131,
122 Depid: item.f106,
123 Department: item.f106,
124 Department2: item.f350,
125 Department3: item.f351,
126 Department4: item.f352,
127 SupplierId: item.f104,
128 ManufacturerCode: item.f506,
129 MfrCode: item.f506,
130 }));
131 }
132
133 return NextResponse.json({
134 success: true,
135 data: {
136 value: products,
137 '@odata.count': products.length
138 },
139 source: 'elink'
140 });
141 }
142
143 // Empty or unexpected BUCK response
144 return NextResponse.json({
145 success: true,
146 data: {
147 value: [],
148 '@odata.count': 0
149 },
150 source: 'elink'
151 });
152
153 } catch (error: any) {
154 console.error('[Products API] Error:', error);
155
156 // Check if it's a 403/authentication error
157 if (error.message && error.message.includes('403')) {
158 return NextResponse.json(
159 {
160 error: 'Your session has expired. Please refresh the page and log in again.',
161 code: 'SESSION_EXPIRED'
162 },
163 { status: 401 }
164 );
165 }
166
167 return NextResponse.json(
168 { error: error.message || 'Failed to fetch products' },
169 { status: 500 }
170 );
171 }
172}