import { NextRequest, NextResponse } from 'next/server' import http from 'node:http' import https from 'node:https' // Service-type routing: NEXT_PUBLIC_MEDUSA_SERVICE_TYPE (from .env.local) selects the correct // internal Medusa container. Falls back to ecommerce. const _st = process.env.NEXT_PUBLIC_MEDUSA_SERVICE_TYPE || 'ecommerce' const _urlMap: Record = { ecommerce: process.env.VULA_ECOMMERCE_BACKEND_URL || 'http://vula-ecommerce:9000', hotel: process.env.VULA_HOTEL_BACKEND_URL || 'http://vula-hotel:9000', restaurant: process.env.VULA_RESTAURANT_BACKEND_URL || 'http://vula-restaurant:9000', services: process.env.VULA_SERVICES_BACKEND_URL || 'http://vula-services:9000', } const MEDUSA_URL = _urlMap[_st] || _urlMap.ecommerce function nodeRequest( url: string, method: string, headers: Record, body?: string, timeoutMs = 15000 ): Promise<{ status: number; data: unknown }> { return new Promise((resolve, reject) => { const parsed = new URL(url) const lib = parsed.protocol === 'https:' ? https : http const req = lib.request({ hostname: parsed.hostname, port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80), path: parsed.pathname + parsed.search, method, headers: { ...headers, ...(body ? { 'content-length': Buffer.byteLength(body).toString() } : {}) }, }, (res) => { let raw = '' res.setEncoding('utf8') res.on('data', (chunk) => { raw += chunk }) res.on('end', () => { try { resolve({ status: res.statusCode ?? 200, data: JSON.parse(raw) }) } catch { resolve({ status: res.statusCode ?? 200, data: {} }) } }) }) req.setTimeout(timeoutMs, () => { req.destroy(new Error('timeout')) }) req.on('error', reject) if (body) req.write(body) req.end() }) } async function proxy(req: NextRequest, segments: string[], method: string, body?: string) { const search = req.nextUrl.search const url = `${MEDUSA_URL}/${segments.join('/')}${search}` const headers: Record = { 'content-type': 'application/json', accept: 'application/json' } const pubKey = req.headers.get('x-publishable-api-key') || process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || '' if (pubKey) headers['x-publishable-api-key'] = pubKey const timeoutMs = (method === 'POST' && segments.includes('carts')) ? 25000 : 15000 try { const { status, data } = await nodeRequest(url, method, headers, body, timeoutMs) return NextResponse.json(data, { status }) } catch { return NextResponse.json({ error: 'Medusa proxy error' }, { status: 502 }) } } type RouteCtx = { params: { path: string[] } } export async function GET(req: NextRequest, { params }: RouteCtx) { return proxy(req, params.path, 'GET') } export async function POST(req: NextRequest, { params }: RouteCtx) { const path = params.path let bodyText = await req.text() // Inject project_id server-side for booking/appointment creation so each // deployed site's records are scoped to its own sales channel. const projectId = process.env.VULA_PROJECT_ID || process.env.VULA_SALES_CHANNEL_ID const isBookingPost = projectId && ( (path.includes('hotel') && path.includes('bookings')) || path.includes('appointments') ) if (isBookingPost && bodyText) { try { const parsed = JSON.parse(bodyText) if (!parsed.project_id) { parsed.project_id = projectId bodyText = JSON.stringify(parsed) } } catch { /* leave body unchanged if not JSON */ } } return proxy(req, path, 'POST', bodyText) } export async function PUT(req: NextRequest, { params }: RouteCtx) { return proxy(req, params.path, 'PUT', await req.text()) } export async function PATCH(req: NextRequest, { params }: RouteCtx) { return proxy(req, params.path, 'PATCH', await req.text()) } export async function DELETE(req: NextRequest, { params }: RouteCtx) { return proxy(req, params.path, 'DELETE') }