374 lines
10 KiB
TypeScript
374 lines
10 KiB
TypeScript
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<string, unknown>;
|
|
};
|
|
|
|
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<string, string> {
|
|
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<MedusaProduct[]> {
|
|
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<MedusaProduct | null> {
|
|
try {
|
|
const products = await getAllProducts();
|
|
return products.find((p) => p.metadata?.ean === ean) ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function getProductByCatalogNo(catalogNo: string): Promise<MedusaProduct | null> {
|
|
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<MedusaCart> {
|
|
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<MedusaCart | null> {
|
|
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<MedusaCart> {
|
|
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<MedusaCart> {
|
|
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<MedusaCart> {
|
|
// 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<MedusaCart> {
|
|
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<MedusaCart> {
|
|
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<MedusaCart> {
|
|
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<MedusaShippingOption[]> {
|
|
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<MedusaCart> {
|
|
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<MedusaPaymentCollection> {
|
|
// 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<string, unknown>,
|
|
): Promise<MedusaPaymentCollection> {
|
|
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<MedusaOrder | null> {
|
|
try {
|
|
const data = await medusa.store.order.retrieve(orderId);
|
|
return data.order as unknown as MedusaOrder;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|