This commit is contained in:
Vula Builder
2026-07-27 17:10:27 +00:00
parent 6241190e7d
commit 7c31801533
+81
View File
@@ -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<string | null> {
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 1100.
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<string, string> = {
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: [] })
}
}