diff --git a/prisma/migrations/20260331143000_cached_order_vendor_sync_fields/migration.sql b/prisma/migrations/20260331143000_cached_order_vendor_sync_fields/migration.sql new file mode 100644 index 0000000..df746eb --- /dev/null +++ b/prisma/migrations/20260331143000_cached_order_vendor_sync_fields/migration.sql @@ -0,0 +1,7 @@ +ALTER TABLE "CachedOrder" + ADD COLUMN IF NOT EXISTS "pendingVendorSync" BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS "lastSupplierUpdateAt" TIMESTAMP(3), + ADD COLUMN IF NOT EXISTS "lastSupplierUpdatedBy" TEXT, + ADD COLUMN IF NOT EXISTS "lastShopifySyncAt" TIMESTAMP(3), + ADD COLUMN IF NOT EXISTS "lastShopifySyncError" TEXT; + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e0b16e1..c214647 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -157,6 +157,11 @@ model CachedOrder { trackingNumber String? trackingCompany String? trackingUrl String? + pendingVendorSync Boolean @default(false) + lastSupplierUpdateAt DateTime? + lastSupplierUpdatedBy String? + lastShopifySyncAt DateTime? + lastShopifySyncError String? lineItems Json // Store line items as JSON customer Json? // Store customer info as JSON createdAt DateTime @default(now()) diff --git a/src/app/api/shopify/orders/[orderId]/route.ts b/src/app/api/shopify/orders/[orderId]/route.ts index 13f4a0c..087c4fb 100644 --- a/src/app/api/shopify/orders/[orderId]/route.ts +++ b/src/app/api/shopify/orders/[orderId]/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server" import { auth } from "@/auth" import { prisma } from "@/lib/prisma" import { handleApiError } from "@/lib/errors" +import { normalizeFulfillmentStatus, toVendorDisplayStatus } from "@/lib/orders/fulfillmentStatus" export async function GET( request: Request, @@ -69,7 +70,18 @@ export async function GET( storeId: store.id, }, }, - select: { email: true, customer: true }, + select: { + email: true, + customer: true, + fulfillmentStatus: true, + trackingNumber: true, + trackingCompany: true, + trackingUrl: true, + pendingVendorSync: true, + lastSupplierUpdateAt: true, + lastShopifySyncAt: true, + lastShopifySyncError: true, + }, }) if (cached) { @@ -96,6 +108,16 @@ export async function GET( order.customer = { ...cust, email: cachedEmail } } } + + order.fulfillment_status = toVendorDisplayStatus(cached.fulfillmentStatus) + order.fulfillment_status_canonical = normalizeFulfillmentStatus(cached.fulfillmentStatus) + order.tracking_number = cached.trackingNumber || null + order.tracking_company = cached.trackingCompany || null + order.tracking_url = cached.trackingUrl || null + order.pending_vendor_sync = cached.pendingVendorSync + order.last_supplier_update_at = cached.lastSupplierUpdateAt?.toISOString() || null + order.last_shopify_sync_at = cached.lastShopifySyncAt?.toISOString() || null + order.last_shopify_sync_error = cached.lastShopifySyncError || null } return NextResponse.json({ order }) diff --git a/src/app/api/shopify/orders/route.ts b/src/app/api/shopify/orders/route.ts index 729e7c2..b8a8fa9 100644 --- a/src/app/api/shopify/orders/route.ts +++ b/src/app/api/shopify/orders/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server" import { auth } from "@/auth" import { prisma } from "@/lib/prisma" import { isCacheFresh, getCachedOrders, updateOrderCache } from "@/lib/cache" +import { normalizeFulfillmentStatus, toVendorDisplayStatus } from "@/lib/orders/fulfillmentStatus" export async function GET() { try { @@ -40,9 +41,17 @@ export async function GET() { total_price: order.totalPrice, currency: order.currency, financial_status: order.financialStatus, - fulfillment_status: order.fulfillmentStatus, + fulfillment_status: toVendorDisplayStatus(order.fulfillmentStatus), + fulfillment_status_canonical: normalizeFulfillmentStatus(order.fulfillmentStatus), customer: order.customer, - line_items: order.lineItems + line_items: order.lineItems, + tracking_number: order.trackingNumber || null, + tracking_company: order.trackingCompany || null, + tracking_url: order.trackingUrl || null, + pending_vendor_sync: order.pendingVendorSync || false, + last_supplier_update_at: order.lastSupplierUpdateAt?.toISOString() || null, + last_shopify_sync_at: order.lastShopifySyncAt?.toISOString() || null, + last_shopify_sync_error: order.lastShopifySyncError || null, })) return NextResponse.json({ orders: transformedOrders, @@ -87,7 +96,39 @@ export async function GET() { }) } - return NextResponse.json({ ...data, cached: false }) + const cachedOrders = await prisma.cachedOrder.findMany({ + where: { storeId: store.id }, + select: { + shopifyOrderId: true, + fulfillmentStatus: true, + trackingNumber: true, + trackingCompany: true, + trackingUrl: true, + pendingVendorSync: true, + lastSupplierUpdateAt: true, + lastShopifySyncAt: true, + lastShopifySyncError: true, + }, + }) + const byId = new Map(cachedOrders.map((o) => [o.shopifyOrderId, o])) + const mergedOrders = (data.orders || []).map((o: Record) => { + const local = byId.get(String(o.id)) + if (!local) return o + return { + ...o, + fulfillment_status: toVendorDisplayStatus(local.fulfillmentStatus), + fulfillment_status_canonical: normalizeFulfillmentStatus(local.fulfillmentStatus), + tracking_number: local.trackingNumber || null, + tracking_company: local.trackingCompany || null, + tracking_url: local.trackingUrl || null, + pending_vendor_sync: local.pendingVendorSync || false, + last_supplier_update_at: local.lastSupplierUpdateAt?.toISOString() || null, + last_shopify_sync_at: local.lastShopifySyncAt?.toISOString() || null, + last_shopify_sync_error: local.lastShopifySyncError || null, + } + }) + + return NextResponse.json({ ...data, orders: mergedOrders, cached: false }) } catch (fetchError: unknown) { clearTimeout(timeoutId) if (fetchError instanceof Error && fetchError.name === "AbortError") { diff --git a/src/app/api/supplier/orders/[orderId]/route.ts b/src/app/api/supplier/orders/[orderId]/route.ts index a1ba7ca..7c8f13b 100644 --- a/src/app/api/supplier/orders/[orderId]/route.ts +++ b/src/app/api/supplier/orders/[orderId]/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server" import { auth } from "@/auth" import { prisma } from "@/lib/prisma" +import { normalizeFulfillmentStatus, toVendorDisplayStatus } from "@/lib/orders/fulfillmentStatus" async function getSupplierContext(userId: string) { const profile = await prisma.supplierProfile.findUnique({ @@ -52,6 +53,168 @@ function isSupplierAllowedForOrder( }) } +async function syncOrderToShopify( + storeId: string, + shopifyOrderId: string, + fulfillmentStatus: string, + trackingNumber: string | null, + trackingCompany: string | null, + trackingUrl: string | null, +): Promise<{ ok: boolean; error?: string }> { + const canonical = normalizeFulfillmentStatus(fulfillmentStatus) + if (canonical !== "SHIPPED" && canonical !== "FULFILLED") { + // No fulfillment push needed for non-shipped statuses. + return { ok: true } + } + if (!trackingNumber || !trackingCompany) { + return { ok: false, error: "Tracking platform and tracking number are required for shipped/fulfilled sync." } + } + + const store = await prisma.store.findUnique({ + where: { id: storeId }, + select: { shopifyDomain: true, shopifyAccessToken: true }, + }) + if (!store?.shopifyDomain || !store?.shopifyAccessToken) { + return { ok: false, error: "Shopify store not connected." } + } + const cleanDomain = store.shopifyDomain.trim().replace(/^https?:\/\//, "").replace(/\/$/, "") + + const fetchScopes = async (): Promise => { + try { + const res = await fetch(`https://${cleanDomain}/admin/oauth/access_scopes.json`, { + method: "GET", + headers: { + "X-Shopify-Access-Token": store.shopifyAccessToken, + "Content-Type": "application/json", + }, + }) + if (!res.ok) return [] + const payload = (await res.json()) as { access_scopes?: Array<{ handle?: string }> } + return (payload.access_scopes || []).map((s) => s.handle || "").filter(Boolean) + } catch { + return [] + } + } + + const createLegacyFulfillmentFallback = async (): Promise<{ ok: boolean; error?: string }> => { + const orderRes = await fetch( + `https://${cleanDomain}/admin/api/2024-01/orders/${shopifyOrderId}.json?status=any`, + { + method: "GET", + headers: { + "X-Shopify-Access-Token": store.shopifyAccessToken, + "Content-Type": "application/json", + }, + }, + ) + if (!orderRes.ok) { + const details = await orderRes.text().catch(() => "Unknown Shopify response") + return { ok: false, error: `Fallback fetch order failed: ${details}` } + } + + const orderPayload = (await orderRes.json()) as { + order?: { + line_items?: Array<{ id: number; fulfillable_quantity?: number; quantity?: number }> + } + } + const lineItems = (orderPayload.order?.line_items || []) + .filter((li) => (li.fulfillable_quantity ?? li.quantity ?? 0) > 0) + .map((li) => ({ + id: li.id, + quantity: li.fulfillable_quantity ?? li.quantity ?? 1, + })) + + const legacyPayload = { + fulfillment: { + notify_customer: false, + tracking_number: trackingNumber, + tracking_company: trackingCompany, + tracking_urls: trackingUrl ? [trackingUrl] : undefined, + line_items: lineItems.length > 0 ? lineItems : undefined, + }, + } + + const legacyRes = await fetch( + `https://${cleanDomain}/admin/api/2024-01/orders/${shopifyOrderId}/fulfillments.json`, + { + method: "POST", + headers: { + "X-Shopify-Access-Token": store.shopifyAccessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(legacyPayload), + }, + ) + if (!legacyRes.ok) { + const details = await legacyRes.text().catch(() => "Unknown Shopify response") + return { ok: false, error: `Legacy fulfillment failed: ${details}` } + } + return { ok: true } + } + + const foRes = await fetch( + `https://${cleanDomain}/admin/api/2024-01/orders/${shopifyOrderId}/fulfillment_orders.json`, + { + method: "GET", + headers: { + "X-Shopify-Access-Token": store.shopifyAccessToken, + "Content-Type": "application/json", + }, + }, + ) + if (!foRes.ok) { + const details = await foRes.text().catch(() => "Unknown Shopify response") + const fallback = await createLegacyFulfillmentFallback() + if (fallback.ok) return fallback + const scopes = await fetchScopes() + return { + ok: false, + error: `Failed to read fulfillment orders: ${details}. Fallback also failed: ${fallback.error}. Token scopes: [${scopes.join(", ")}]. Re-install/reconnect app after scope changes.`, + } + } + + const foJson = (await foRes.json()) as { fulfillment_orders?: Array<{ id: number }> } + const fulfillmentOrders = foJson.fulfillment_orders || [] + if (fulfillmentOrders.length === 0) { + return { ok: false, error: "No fulfillment orders available for this order." } + } + + const payload = { + fulfillment: { + notify_customer: false, + tracking_info: { + number: trackingNumber, + company: trackingCompany, + url: trackingUrl || undefined, + }, + line_items_by_fulfillment_order: fulfillmentOrders.map((f) => ({ + fulfillment_order_id: f.id, + })), + }, + } + + const syncRes = await fetch(`https://${cleanDomain}/admin/api/2024-01/fulfillments.json`, { + method: "POST", + headers: { + "X-Shopify-Access-Token": store.shopifyAccessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }) + if (!syncRes.ok) { + const details = await syncRes.text().catch(() => "Unknown Shopify response") + const fallback = await createLegacyFulfillmentFallback() + if (fallback.ok) return fallback + const scopes = await fetchScopes() + return { + ok: false, + error: `Shopify fulfillment sync failed: ${details}. Fallback also failed: ${fallback.error}. Token scopes: [${scopes.join(", ")}]. Re-install/reconnect app after scope changes.`, + } + } + + return { ok: true } +} + export async function GET( _req: Request, { params }: { params: Promise<{ orderId: string }> }, @@ -132,7 +295,8 @@ export async function GET( totalPrice: order.totalPrice, currency: order.currency, financialStatus: order.financialStatus, - fulfillmentStatus: order.fulfillmentStatus || "UNFULFILLED", + fulfillmentStatus: normalizeFulfillmentStatus(order.fulfillmentStatus), + fulfillmentStatusDisplay: toVendorDisplayStatus(order.fulfillmentStatus), trackingNumber: (order as unknown as { trackingNumber?: string | null }).trackingNumber, trackingCompany: (order as unknown as { trackingCompany?: string | null }).trackingCompany, trackingUrl: (order as unknown as { trackingUrl?: string | null }).trackingUrl, @@ -162,11 +326,7 @@ export async function PATCH( const { orderId } = await params const body = await req.json() - const allowedStatuses = new Set(["UNFULFILLED", "IN_PROGRESS", "SHIPPED", "FULFILLED", "CANCELLED"]) - const fulfillmentStatus = String(body.fulfillmentStatus || "").toUpperCase() - if (!allowedStatuses.has(fulfillmentStatus)) { - return NextResponse.json({ message: "Invalid fulfillment status" }, { status: 400 }) - } + const fulfillmentStatus = normalizeFulfillmentStatus(body.fulfillmentStatus) const trackingNumber = String(body.trackingNumber || "").trim() || null const trackingCompany = String(body.trackingCompany || "").trim() || null @@ -193,6 +353,10 @@ export async function PATCH( trackingNumber, trackingCompany, trackingUrl, + pendingVendorSync: true, + lastSupplierUpdateAt: new Date(), + lastSupplierUpdatedBy: session.user.id, + lastShopifySyncError: null, } as unknown as Record, select: { shopifyOrderId: true, @@ -200,12 +364,43 @@ export async function PATCH( }, }) + const shopifySync = await syncOrderToShopify( + order.storeId, + orderId, + fulfillmentStatus, + trackingNumber, + trackingCompany, + trackingUrl, + ) + + if (shopifySync.ok) { + await prisma.cachedOrder.update({ + where: { id: order.id }, + data: { + pendingVendorSync: false, + lastShopifySyncAt: new Date(), + lastShopifySyncError: null, + } as unknown as Record, + }) + } else { + await prisma.cachedOrder.update({ + where: { id: order.id }, + data: { + pendingVendorSync: true, + lastShopifySyncError: shopifySync.error || "Unknown sync error", + } as unknown as Record, + }) + } + return NextResponse.json({ order: { ...updated, trackingNumber, trackingCompany, trackingUrl, + pendingVendorSync: !shopifySync.ok, + lastShopifySyncError: shopifySync.ok ? null : shopifySync.error || "Unknown sync error", + fulfillmentStatusDisplay: toVendorDisplayStatus(updated.fulfillmentStatus), }, }) } catch (error) { diff --git a/src/app/api/supplier/orders/route.ts b/src/app/api/supplier/orders/route.ts index ac1ff37..85d5cf8 100644 --- a/src/app/api/supplier/orders/route.ts +++ b/src/app/api/supplier/orders/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server" import { auth } from "@/auth" import { prisma } from "@/lib/prisma" +import { normalizeFulfillmentStatus, toVendorDisplayStatus } from "@/lib/orders/fulfillmentStatus" /** * GET /api/supplier/orders @@ -154,11 +155,13 @@ export async function GET() { total: Number.parseFloat(order.totalPrice || "0"), currency: order.currency, financialStatus: order.financialStatus, - fulfillmentStatus: order.fulfillmentStatus, - status: order.fulfillmentStatus || "UNFULFILLED", + fulfillmentStatus: normalizeFulfillmentStatus(order.fulfillmentStatus), + status: normalizeFulfillmentStatus(order.fulfillmentStatus), + fulfillmentStatusDisplay: toVendorDisplayStatus(order.fulfillmentStatus), trackingNumber: (order as unknown as { trackingNumber?: string | null }).trackingNumber, trackingCompany: (order as unknown as { trackingCompany?: string | null }).trackingCompany, trackingUrl: (order as unknown as { trackingUrl?: string | null }).trackingUrl, + pendingVendorSync: (order as unknown as { pendingVendorSync?: boolean }).pendingVendorSync ?? false, vendorName: order.store.businessName || order.store.shopifyStoreName, createdAt: order.shopifyCreatedAt || order.createdAt, store: order.store, diff --git a/src/app/dashboard/orders/[orderId]/page.tsx b/src/app/dashboard/orders/[orderId]/page.tsx index 9bb47f0..71e63ef 100644 --- a/src/app/dashboard/orders/[orderId]/page.tsx +++ b/src/app/dashboard/orders/[orderId]/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react" import { useRouter, useParams } from "next/navigation" -import { ArrowLeft, ShoppingBag, User, Calendar, DollarSign, Package, MapPin, Phone, Mail, CheckCircle2, Clock, Loader2, AlertCircle } from "lucide-react" +import { ArrowLeft, ShoppingBag, User, Calendar, DollarSign, Package, MapPin, Phone, Mail, CheckCircle2, Clock, Loader2, AlertCircle, Truck } from "lucide-react" interface LineItem { id: number @@ -58,6 +58,11 @@ interface OrderDetails { line_items: LineItem[] note?: string tags?: string + tracking_number?: string | null + tracking_company?: string | null + tracking_url?: string | null + pending_vendor_sync?: boolean + last_shopify_sync_error?: string | null } export default function OrderDetailsPage() { @@ -234,6 +239,35 @@ export default function OrderDetailsPage() { {/* Main Content Grid */} +
+
+

+ + Fulfillment & Tracking +

+
+
+
+ {order.fulfillment_status || "unfulfilled"} +
+
+ {order.tracking_company || "Tracking platform not set"} +
+
+ {order.tracking_number || "Tracking number not set"} +
+
+ {order.tracking_url || "Tracking URL not set"} +
+
+ {order.pending_vendor_sync && ( +

Supplier update is syncing to Shopify...

+ )} + {order.last_shopify_sync_error && ( +

{order.last_shopify_sync_error}

+ )} +
+
{/* Left Column - Customer & Shipping */}
diff --git a/src/app/dashboard/orders/page.tsx b/src/app/dashboard/orders/page.tsx index 6ec525a..efa24b5 100644 --- a/src/app/dashboard/orders/page.tsx +++ b/src/app/dashboard/orders/page.tsx @@ -32,6 +32,7 @@ interface ShopifyOrder { currency: string financial_status: string fulfillment_status: string | null + pending_vendor_sync?: boolean customer: { first_name: string last_name: string @@ -342,6 +343,11 @@ export default function OrdersPage() { {order.fulfillment_status === "fulfilled" ? : } {order.fulfillment_status || "Unfulfilled"} + {order.pending_vendor_sync && ( + + Pending Sync + + )}
diff --git a/src/app/supplier/orders/[orderId]/page.tsx b/src/app/supplier/orders/[orderId]/page.tsx index b399487..9f0c76e 100644 --- a/src/app/supplier/orders/[orderId]/page.tsx +++ b/src/app/supplier/orders/[orderId]/page.tsx @@ -171,9 +171,9 @@ export default function SupplierOrderDetailsPage() { -
+
-
+

Order Snapshot @@ -195,7 +195,7 @@ export default function SupplierOrderDetailsPage() {

-
+

Fulfillment Update @@ -258,8 +258,8 @@ export default function SupplierOrderDetailsPage() {

-
-
+
+

Customer @@ -279,7 +279,7 @@ export default function SupplierOrderDetailsPage() {

-
+

Shipping Address @@ -299,7 +299,7 @@ export default function SupplierOrderDetailsPage() { )}

-
+

Billing Address diff --git a/src/lib/cache.ts b/src/lib/cache.ts index ce1d7d7..958d241 100644 --- a/src/lib/cache.ts +++ b/src/lib/cache.ts @@ -190,6 +190,21 @@ export async function updateProductCache(storeId: string, products: ShopifyProdu */ export async function updateOrderCache(storeId: string, orders: ShopifyOrderRecord[]) { try { + const orderIds = orders.map((order) => order.id.toString()) + const pendingRows = await prisma.$queryRaw>` + SELECT "shopifyOrderId" + FROM "CachedOrder" + WHERE "storeId" = ${storeId} + AND "pendingVendorSync" = true + AND "shopifyOrderId" = ANY(${orderIds}::text[]) + ` + const pendingIds = new Set(pendingRows.map((r) => r.shopifyOrderId)) + const existingRows = await prisma.cachedOrder.findMany({ + where: { storeId, shopifyOrderId: { in: orderIds } }, + select: { shopifyOrderId: true, fulfillmentStatus: true }, + }) + const existingById = new Map(existingRows.map((r) => [r.shopifyOrderId, r.fulfillmentStatus])) + // Use upsert for each order to handle duplicates gracefully // This ensures webhooks can update orders concurrently without conflicts const upsertPromises = orders.map((order) => @@ -219,7 +234,9 @@ export async function updateOrderCache(storeId: string, orders: ShopifyOrderReco totalPrice: order.total_price || "0", currency: order.currency || null, financialStatus: order.financial_status || null, - fulfillmentStatus: order.fulfillment_status || null, + fulfillmentStatus: pendingIds.has(order.id.toString()) + ? existingById.get(order.id.toString()) || null + : order.fulfillment_status || null, lineItems: (order.line_items || []) as Prisma.InputJsonValue, customer: (order.customer ?? undefined) as Prisma.InputJsonValue | undefined, shopifyCreatedAt: order.created_at ? new Date(order.created_at) : null, @@ -397,6 +414,15 @@ export async function updateSingleProduct(storeId: string, product: ShopifyProdu * Update a single order in cache (for webhook updates) */ export async function updateSingleOrder(storeId: string, order: ShopifyOrderRecord) { + const pendingRows = await prisma.$queryRaw>` + SELECT "pendingVendorSync", "fulfillmentStatus" + FROM "CachedOrder" + WHERE "storeId" = ${storeId} AND "shopifyOrderId" = ${order.id.toString()} + LIMIT 1 + ` + const shouldPreserve = pendingRows[0]?.pendingVendorSync === true + const preservedStatus = pendingRows[0]?.fulfillmentStatus ?? null + await prisma.cachedOrder.upsert({ where: { shopifyOrderId_storeId: { @@ -412,7 +438,7 @@ export async function updateSingleOrder(storeId: string, order: ShopifyOrderReco totalPrice: order.total_price || "0", currency: order.currency || null, financialStatus: order.financial_status || null, - fulfillmentStatus: order.fulfillment_status || null, + fulfillmentStatus: shouldPreserve ? preservedStatus : order.fulfillment_status || null, lineItems: (order.line_items || []) as Prisma.InputJsonValue, customer: (order.customer ?? undefined) as Prisma.InputJsonValue | undefined, shopifyCreatedAt: order.created_at ? new Date(order.created_at) : null, diff --git a/src/lib/orders/fulfillmentStatus.ts b/src/lib/orders/fulfillmentStatus.ts new file mode 100644 index 0000000..bf9968e --- /dev/null +++ b/src/lib/orders/fulfillmentStatus.ts @@ -0,0 +1,38 @@ +export type CanonicalFulfillmentStatus = + | "UNFULFILLED" + | "IN_PROGRESS" + | "SHIPPED" + | "FULFILLED" + | "CANCELLED" + +const SHOPIFY_FULFILLMENT_MAP: Record = { + UNFULFILLED: "unfulfilled", + IN_PROGRESS: "in_progress", + SHIPPED: "shipped", + FULFILLED: "fulfilled", + CANCELLED: "cancelled", +} + +export function normalizeFulfillmentStatus(input: string | null | undefined): CanonicalFulfillmentStatus { + const raw = String(input ?? "") + .trim() + .replace(/[\s-]+/g, "_") + .toUpperCase() + + if (raw === "FULFILLED") return "FULFILLED" + if (raw === "SHIPPED") return "SHIPPED" + if (raw === "IN_PROGRESS" || raw === "INPROGRESS" || raw === "PARTIAL" || raw === "PARTIALLY_FULFILLED") { + return "IN_PROGRESS" + } + if (raw === "CANCELLED" || raw === "CANCELED") return "CANCELLED" + return "UNFULFILLED" +} + +export function toVendorDisplayStatus(input: string | null | undefined): string { + return SHOPIFY_FULFILLMENT_MAP[normalizeFulfillmentStatus(input)] +} + +export function isFulfilledLike(input: string | null | undefined): boolean { + return normalizeFulfillmentStatus(input) === "FULFILLED" +} +