import Medusa from "@medusajs/js-sdk"; const MEDUSA_URL = process.env.MEDUSA_URL ?? process.env.NEXT_PUBLIC_MEDUSA_URL ?? "http://localhost:9000"; const PUBLISHABLE_KEY = process.env.MEDUSA_PUBLISHABLE_KEY ?? process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY ?? ""; const REGION_ID = process.env.MEDUSA_REGION_ID ?? process.env.NEXT_PUBLIC_MEDUSA_REGION_ID ?? ""; // Create a single SDK instance at module scope. // auth type "session" is not needed for server-side usage; we pass tokens // via the ClientHeaders parameter where necessary. const medusa = new Medusa({ baseUrl: MEDUSA_URL, publishableKey: PUBLISHABLE_KEY, auth: { type: "jwt", jwtTokenStorageMethod: "nostore" }, }); // ── Types ────────────────────────────────────────────────────────── export type MedusaMoneyAmount = { amount: number; currency_code: string; }; export type MedusaVariant = { id: string; title: string; sku: string | null; calculated_price?: { calculated_amount: number; currency_code: string; }; prices?: MedusaMoneyAmount[]; }; export type MedusaProduct = { id: string; title: string; handle: string; metadata?: { ean?: string; catalogue_number?: string; [key: string]: unknown; }; variants: MedusaVariant[]; }; export type MedusaLineItem = { id: string; title: string; variant_id: string; variant: MedusaVariant; quantity: number; unit_price: number; total: number; product_title: string; product_handle?: string; thumbnail: string | null; }; export type MedusaCart = { id: string; items: MedusaLineItem[]; total: number; subtotal: number; shipping_total?: number; tax_total?: number; discount_total?: number; currency_code: string; email?: string; shipping_address?: MedusaAddress | null; billing_address?: MedusaAddress | null; shipping_methods?: { id: string; name: string; amount: number; shipping_option_id: string }[]; payment_collection?: MedusaPaymentCollection | null; promotions?: { id: string; code?: string }[]; }; export type MedusaAddress = { first_name: string; last_name: string; address_1: string; address_2?: string; city: string; province?: string; postal_code: string; country_code: string; phone?: string; }; export type MedusaShippingOption = { id: string; name: string; amount: number; price_type: string; }; export type MedusaPaymentSession = { id: string; provider_id: string; status: string; data?: Record; }; export type MedusaPaymentCollection = { id: string; status: string; payment_sessions?: MedusaPaymentSession[]; }; export type MedusaOrder = { id: string; display_id: number; email: string; items: MedusaLineItem[]; total: number; subtotal: number; shipping_total: number; tax_total: number; currency_code: string; shipping_address?: MedusaAddress | null; }; // ── Helpers ──────────────────────────────────────────────────────── /** Build an authorization header object when a JWT auth token is provided. */ function authHeaders(authToken?: string): Record { return authToken ? { Authorization: `Bearer ${authToken}` } : {}; } // ── Product queries ──────────────────────────────────────────────── const PRODUCT_FIELDS = "id,title,handle,metadata,*variants,*variants.calculated_price,variants.sku"; /** * Fetches all products from the Store API (paginated). * Medusa v2 Store API doesn't support metadata filtering, so we fetch * the full catalog and match server-side. The result is cached by * Next.js via `fetch` deduplication + ISR revalidation. */ export async function getAllProducts(): Promise { const all: MedusaProduct[] = []; let offset = 0; const limit = 100; // eslint-disable-next-line no-constant-condition while (true) { const data = await medusa.store.product.list({ fields: PRODUCT_FIELDS, region_id: REGION_ID, limit, offset, }); all.push(...(data.products as unknown as MedusaProduct[])); if (all.length >= data.count) break; offset += limit; } return all; } export async function getProductByEan(ean: string): Promise { try { const products = await getAllProducts(); return products.find((p) => p.metadata?.ean === ean) ?? null; } catch { return null; } } export async function getProductByCatalogNo(catalogNo: string): Promise { try { const products = await getAllProducts(); return ( products.find( (p) => p.metadata?.catalogue_number?.toUpperCase() === catalogNo.toUpperCase(), ) ?? null ); } catch { return null; } } // ── Cart operations ──────────────────────────────────────────────── const CART_FIELDS = "id,items,items.*,items.variant.*,items.thumbnail,total,subtotal,discount_total,currency_code,+promotions,+promotions.code"; export async function createCart(authToken?: string): Promise { const data = await medusa.store.cart.create( { region_id: REGION_ID }, {}, authHeaders(authToken), ); return data.cart as unknown as MedusaCart; } export async function getCart(cartId: string): Promise { try { const data = await medusa.store.cart.retrieve(cartId, { fields: CART_FIELDS, }); return data.cart as unknown as MedusaCart; } catch { return null; } } export async function addToCart( cartId: string, variantId: string, quantity = 1, ): Promise { const data = await medusa.store.cart.createLineItem( cartId, { variant_id: variantId, quantity }, { fields: CART_FIELDS }, ); return data.cart as unknown as MedusaCart; } export async function updateCartItem( cartId: string, lineItemId: string, quantity: number, ): Promise { const data = await medusa.store.cart.updateLineItem( cartId, lineItemId, { quantity }, { fields: CART_FIELDS }, ); return data.cart as unknown as MedusaCart; } export async function removeFromCart( cartId: string, lineItemId: string, ): Promise { // The SDK's deleteLineItem returns { parent: cart }. await medusa.store.cart.deleteLineItem(cartId, lineItemId); const cart = await getCart(cartId); if (!cart) throw new Error("Cart not found after removing item"); return cart; } // ── Promotion operations ──────────────────────────────────────────── export async function applyPromoCode( cartId: string, code: string, ): Promise { await medusa.client.fetch( `/store/carts/${cartId}/promotions`, { method: "POST", body: { promo_codes: [code] }, }, ); // Re-fetch with our field selection so the shape is consistent const cart = await getCart(cartId); if (!cart) throw new Error("Cart not found after applying promo"); return cart; } export async function removePromoCode( cartId: string, code: string, ): Promise { await medusa.client.fetch( `/store/carts/${cartId}/promotions`, { method: "DELETE", body: { promo_codes: [code] }, }, ); const cart = await getCart(cartId); if (!cart) throw new Error("Cart not found after removing promo"); return cart; } // ── Checkout operations ───────────────────────────────────────────── export async function updateCart( cartId: string, data: { email?: string; shipping_address?: MedusaAddress; billing_address?: MedusaAddress; }, authToken?: string, ): Promise { const res = await medusa.store.cart.update( cartId, data, {}, authHeaders(authToken), ); return res.cart as unknown as MedusaCart; } export async function getShippingOptions( cartId: string, ): Promise { const data = await medusa.store.fulfillment.listCartOptions({ cart_id: cartId, }); return data.shipping_options as unknown as MedusaShippingOption[]; } export async function addShippingMethod( cartId: string, optionId: string, ): Promise { const data = await medusa.store.cart.addShippingMethod(cartId, { option_id: optionId, }); return data.cart as unknown as MedusaCart; } export async function createPaymentCollection( cartId: string, ): Promise { // The SDK's store.payment.initiatePaymentSession handles this automatically, // but we need to keep the two-step flow for callers. Use client.fetch for // the explicit payment-collection creation endpoint. const data = await medusa.client.fetch<{ payment_collection: MedusaPaymentCollection }>( "/store/payment-collections", { method: "POST", body: { cart_id: cartId }, }, ); return data.payment_collection; } export async function initPaymentSession( paymentCollectionId: string, providerId: string, sessionData?: Record, ): Promise { const data = await medusa.client.fetch<{ payment_collection: MedusaPaymentCollection }>( `/store/payment-collections/${paymentCollectionId}/payment-sessions`, { method: "POST", body: { provider_id: providerId, ...(sessionData ? { data: sessionData } : {}), }, }, ); return data.payment_collection; } export async function completeCart( cartId: string, authToken?: string, ): Promise<{ type: string; order?: MedusaOrder; cart?: MedusaCart }> { const data = await medusa.store.cart.complete( cartId, {}, authHeaders(authToken), ); return data as unknown as { type: string; order?: MedusaOrder; cart?: MedusaCart }; } export async function getOrder(orderId: string): Promise { try { const data = await medusa.store.order.retrieve(orderId); return data.order as unknown as MedusaOrder; } catch { return null; } }