1import { NextRequest, NextResponse } from 'next/server';
2import { fieldpineServerApi } from '@/lib/server/fieldpineApi';
3import { getRequestContext, validateApiAccess } from '@/lib/server/sessionUtils';
6 * Products Endpoint (ELINK API)
7 * GET /api/v1/openapi/products
9 * Uses Fieldpine's ELINK API (retailmax.elink.products.list)
10 * Security: Retail stores can access this (ELINK only)
12export async function GET(request: NextRequest) {
14 // 1. Get session and store context
15 const context = await getRequestContext(request);
17 if (!context || !context.isAuthenticated) {
18 return NextResponse.json(
19 { error: 'Authentication required' },
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(
30 error: apiAccessValidation.error,
31 code: apiAccessValidation.errorCode
38 const clientId = request.headers.get('x-forwarded-for') ||
39 request.headers.get('x-real-ip') ||
40 context.session.userId ||
42 if (!fieldpineServerApi.checkClientRateLimit(clientId)) {
43 return NextResponse.json(
44 { error: 'Rate limit exceeded' },
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');
58 console.log(`[Products API] Search params:`, { search, plu, barcode, limit, skip, filter });
60 // 5. Build ELINK BUCK parameters
61 const buckParams: Record<string, string | string[]> = {
62 "3": "retailmax.elink.products.list",
64 "99": Math.random().toString() // Cache buster
68 // Field reference: f105=barcode, f102=PLU, f101=name/description, f100=product ID
70 buckParams["9"] = `f105,8,${encodeURIComponent(barcode)}`;
72 buckParams["9"] = `f102,0,${encodeURIComponent(plu)}`;
74 buckParams["9"] = `f101,8,${encodeURIComponent(search)}`;
76 // Handle OData-style filters if needed
77 buckParams["9"] = filter;
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
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.' },
96 // Use store-specific URL for API calls
97 const buckResponse = await fieldpineServerApi.buckApiCall(
99 context.session.apiKey,
103 // 7. Transform BUCK response to OData format
104 if (buckResponse && buckResponse.RootType === "ARAY") {
105 let products: any[] = [];
107 if (buckResponse.DATS && Array.isArray(buckResponse.DATS)) {
108 products = buckResponse.DATS.map((item: any) => ({
110 Name: item.f101 || item.f500,
111 Description: item.f500,
113 SecondaryBarcode: item.f501,
115 SellPrice: item.f103,
119 CostPrice: item.f150,
120 StockLevel: item.f130 || 0,
123 Department: item.f106,
124 Department2: item.f350,
125 Department3: item.f351,
126 Department4: item.f352,
127 SupplierId: item.f104,
128 ManufacturerCode: item.f506,
133 return NextResponse.json({
137 '@odata.count': products.length
143 // Empty or unexpected BUCK response
144 return NextResponse.json({
153 } catch (error: any) {
154 console.error('[Products API] Error:', error);
156 // Check if it's a 403/authentication error
157 if (error.message && error.message.includes('403')) {
158 return NextResponse.json(
160 error: 'Your session has expired. Please refresh the page and log in again.',
161 code: 'SESSION_EXPIRED'
167 return NextResponse.json(
168 { error: error.message || 'Failed to fetch products' },