From 65eff660d5113d97baefc58f01089f8e748aacec Mon Sep 17 00:00:00 2001 From: Vula Builder Date: Tue, 14 Jul 2026 18:35:22 +0000 Subject: [PATCH] Deploy --- src/app/api/medusa/products/route.ts | 81 ++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/app/api/medusa/products/route.ts diff --git a/src/app/api/medusa/products/route.ts b/src/app/api/medusa/products/route.ts new file mode 100644 index 0000000..2632fcf --- /dev/null +++ b/src/app/api/medusa/products/route.ts @@ -0,0 +1,81 @@ +import { NextRequest, NextResponse } from 'next/server' + +// Medusa v2 store API requires region_id to return products with pricing context. +// Without it, GET /store/products returns {products: []} even for published products. +// Cache in module scope — regions don't change between requests. +let cachedRegionId: string | null = null + +async function getRegionId(backendUrl: string, publishableKey: string): Promise { + if (cachedRegionId) return cachedRegionId + // Retry up to 3 times — Medusa may be slow on cold start + for (let attempt = 0; attempt < 3; attempt++) { + try { + const res = await fetch(`${backendUrl}/store/regions?limit=1`, { + headers: { 'x-publishable-api-key': publishableKey }, + cache: 'no-store', + signal: AbortSignal.timeout(10000), + }) + if (!res.ok) continue // 5xx during cold start — retry + const data = await res.json() + const id = data.regions?.[0]?.id ?? null + if (id) { cachedRegionId = id; return id } + } catch { /* timeout or network error — retry */ } + } + return null +} + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url) + // Sanitise limit: reject non-numeric values (e.g. "[object Object]") and clamp 1–100. + const rawLimit = searchParams.get('limit') || '12' + const parsedLimit = parseInt(rawLimit, 10) + const limit = isNaN(parsedLimit) ? 12 : Math.min(Math.max(parsedLimit, 1), 100) + const categoryId = searchParams.get('categoryId') || undefined + const ids = searchParams.getAll('id[]') // curated ID list from useFeaturedProducts + + // Service-type routing: NEXT_PUBLIC_MEDUSA_SERVICE_TYPE (from .env.local) selects the correct + // internal Medusa container. Each container is on a different Docker DNS name + same internal port. + const serviceType = process.env.NEXT_PUBLIC_MEDUSA_SERVICE_TYPE || 'ecommerce' + const internalUrlMap: 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 backendUrl = internalUrlMap[serviceType] || internalUrlMap.ecommerce + const publishableKey = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || '' + + try { + // When specific IDs are requested (e.g. from useFeaturedProducts), forward them directly + // and use a limit that covers all requested IDs. + const effectiveLimit = ids.length > 0 ? ids.length : limit + const params = new URLSearchParams({ limit: String(effectiveLimit) }) + if (categoryId) params.set('category_id[]', categoryId) + ids.forEach(id => params.append('id[]', id)) + + const regionId = await getRegionId(backendUrl, publishableKey) + if (regionId) { + // With region: get calculated_price (tax-aware) + raw prices fallback + categories for filter tabs + params.set('region_id', regionId) + params.set('fields', '+variants.calculated_price,+variants.prices.amount,+variants.prices.currency_code,+categories') + } else { + // No region yet: request raw prices so price display still works + params.set('fields', '+variants.prices.amount,+variants.prices.currency_code') + } + const response = await fetch( + `${backendUrl}/store/products?${params}`, + { + headers: { 'x-publishable-api-key': publishableKey }, + cache: 'no-store', + signal: AbortSignal.timeout(10000), + } + ) + if (!response.ok) { + return NextResponse.json({ products: [] }) + } + const data = await response.json() + return NextResponse.json(data) + } catch { + return NextResponse.json({ products: [] }) + } +}