datewise/app/services/catalog.server.ts
metatroncubeswdev e56182e0e3 chore: initial Datewise app (rebranded from SlotSure prototype)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:06:49 -04:00

119 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Cart enrichment: the storefront only knows variant ids and quantities, so we look up
* collections, tags, vendor, type and per-location inventory with the Admin API.
* Results are cached briefly per shop+variant to keep the widget fast.
*/
import type { Cart, CartItem } from "../engine/types";
import type { Inventory } from "../engine/locations";
type AdminGraphql = { graphql: (q: string, o?: { variables?: Record<string, unknown> }) => Promise<Response> };
export interface StorefrontLine {
variantId: string | number;
quantity: number;
price?: number; // minor units, from /cart.js
title?: string;
}
interface VariantInfo {
item: Omit<CartItem, "quantity" | "price"> & { price: number };
stock: Record<string, number>; // shopify location gid → available
tracked: boolean;
isFee: boolean;
}
const cache = new Map<string, { at: number; v: VariantInfo }>();
const TTL = 60_000;
const gid = (id: string | number) => (String(id).startsWith("gid://") ? String(id) : `gid://shopify/ProductVariant/${id}`);
export const FEE_TAG = "datewise-fee";
export async function enrichCart(
admin: AdminGraphql,
shop: string,
lines: StorefrontLine[],
shopifyToAppLocation: Record<string, string>,
): Promise<{ cart: Cart; inventory: Inventory; feeLines: StorefrontLine[] }> {
const ids = [...new Set(lines.map((l) => gid(l.variantId)))];
const missing = ids.filter((id) => {
const c = cache.get(`${shop}|${id}`);
return !c || Date.now() - c.at > TTL;
});
for (let i = 0; i < missing.length; i += 50) {
const res = await admin.graphql(
`#graphql
query DatewiseCart($ids: [ID!]!) {
nodes(ids: $ids) {
... on ProductVariant {
id title price
product {
id title vendor productType tags
collections(first: 50) { nodes { id } }
}
inventoryItem {
tracked
inventoryLevels(first: 30) {
nodes { location { id } quantities(names: ["available"]) { quantity } }
}
}
}
}
}`,
{ variables: { ids: missing.slice(i, i + 50) } },
);
const json = (await res.json()) as { data: { nodes: (RawVariant | null)[] } };
for (const n of json.data.nodes) {
if (!n) continue;
const tracked = n.inventoryItem?.tracked ?? false;
const stock: Record<string, number> = {};
for (const lvl of n.inventoryItem?.inventoryLevels.nodes ?? [])
stock[lvl.location.id] = lvl.quantities[0]?.quantity ?? 0;
cache.set(`${shop}|${n.id}`, {
at: Date.now(),
v: {
isFee: n.product.tags.includes(FEE_TAG),
tracked,
stock,
item: {
productId: n.product.id,
variantId: n.id,
title: n.product.title + (n.title && n.title !== "Default Title" ? ` – ${n.title}` : ""),
price: Math.round(Number(n.price) * 100),
collectionIds: n.product.collections.nodes.map((c) => c.id),
vendor: n.product.vendor,
productType: n.product.productType,
tags: n.product.tags,
},
},
});
}
}
const items: CartItem[] = [];
const inventory: Inventory = {};
const feeLines: StorefrontLine[] = [];
for (const l of lines) {
const info = cache.get(`${shop}|${gid(l.variantId)}`)?.v;
if (!info) continue;
if (info.isFee) {
feeLines.push(l);
continue;
}
items.push({ ...info.item, quantity: l.quantity, price: l.price ?? info.item.price });
// Every linked location gets an explicit figure; untracked items count as unlimited.
for (const [shopifyLoc, appLoc] of Object.entries(shopifyToAppLocation)) {
inventory[appLoc] ??= {};
inventory[appLoc][info.item.variantId] = info.tracked ? info.stock[shopifyLoc] ?? 0 : Number.MAX_SAFE_INTEGER;
}
}
return { cart: { items, subtotal: items.reduce((s, i) => s + i.price * i.quantity, 0) }, inventory, feeLines };
}
interface RawVariant {
id: string;
title: string;
price: string;
product: { id: string; title: string; vendor: string; productType: string; tags: string[]; collections: { nodes: { id: string }[] } };
inventoryItem: { tracked: boolean; inventoryLevels: { nodes: { location: { id: string }; quantities: { quantity: number }[] }[] } } | null;
}