fix: harden Shopify tracking sync for closed fulfillments
Make supplier-side Shopify sync update tracking on existing fulfillments when fulfillment orders are closed, and make EC2 deploy extraction deletion-safe to prevent stale files from breaking builds. Made-with: Cursor
This commit is contained in:
parent
b67ef3190c
commit
bcd479b76a
13
.github/workflows/deploy-ec2.yml
vendored
13
.github/workflows/deploy-ec2.yml
vendored
@ -53,8 +53,17 @@ jobs:
|
|||||||
rm -rf "./prisma/init-pgvector.sql"
|
rm -rf "./prisma/init-pgvector.sql"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Unpack latest code snapshot over deploy directory
|
# Clean, deletion-safe deploy:
|
||||||
unzip -oq deploy.zip -d .
|
# extract to temp dir, then rsync with --delete so removed files are purged.
|
||||||
|
rm -rf .deploy_extract_tmp
|
||||||
|
mkdir -p .deploy_extract_tmp
|
||||||
|
unzip -oq deploy.zip -d .deploy_extract_tmp
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude ".env" \
|
||||||
|
--exclude "deploy.zip" \
|
||||||
|
--exclude ".deploy_extract_tmp" \
|
||||||
|
.deploy_extract_tmp/ ./
|
||||||
|
rm -rf .deploy_extract_tmp
|
||||||
rm -f deploy.zip
|
rm -f deploy.zip
|
||||||
|
|
||||||
# Recreate .env from secret for runtime values
|
# Recreate .env from secret for runtime values
|
||||||
|
|||||||
@ -96,6 +96,64 @@ async function syncOrderToShopify(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateTrackingOnExistingFulfillment = async (): Promise<{ ok: boolean; error?: string }> => {
|
||||||
|
const listRes = await fetch(
|
||||||
|
`https://${cleanDomain}/admin/api/2024-01/orders/${shopifyOrderId}/fulfillments.json`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-Shopify-Access-Token": store.shopifyAccessToken,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (!listRes.ok) {
|
||||||
|
const details = await listRes.text().catch(() => "Unknown Shopify response")
|
||||||
|
return { ok: false, error: `Read existing fulfillments failed: ${details}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const listPayload = (await listRes.json()) as {
|
||||||
|
fulfillments?: Array<{ id: number; created_at?: string }>
|
||||||
|
}
|
||||||
|
const fulfillments = listPayload.fulfillments || []
|
||||||
|
if (fulfillments.length === 0) {
|
||||||
|
return { ok: false, error: "No existing fulfillment found to update tracking." }
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = [...fulfillments].sort((a, b) => {
|
||||||
|
const aTs = a.created_at ? new Date(a.created_at).getTime() : 0
|
||||||
|
const bTs = b.created_at ? new Date(b.created_at).getTime() : 0
|
||||||
|
return bTs - aTs
|
||||||
|
})
|
||||||
|
const latest = sorted[0]
|
||||||
|
|
||||||
|
const updateRes = await fetch(
|
||||||
|
`https://${cleanDomain}/admin/api/2024-01/fulfillments/${latest.id}/update_tracking.json`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"X-Shopify-Access-Token": store.shopifyAccessToken,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
fulfillment: {
|
||||||
|
notify_customer: false,
|
||||||
|
tracking_info: {
|
||||||
|
number: trackingNumber,
|
||||||
|
company: trackingCompany,
|
||||||
|
url: trackingUrl || undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (!updateRes.ok) {
|
||||||
|
const details = await updateRes.text().catch(() => "Unknown Shopify response")
|
||||||
|
return { ok: false, error: `Update tracking on existing fulfillment failed: ${details}` }
|
||||||
|
}
|
||||||
|
return { ok: true }
|
||||||
|
}
|
||||||
|
|
||||||
const createLegacyFulfillmentFallback = async (): Promise<{ ok: boolean; error?: string }> => {
|
const createLegacyFulfillmentFallback = async (): Promise<{ ok: boolean; error?: string }> => {
|
||||||
const orderRes = await fetch(
|
const orderRes = await fetch(
|
||||||
`https://${cleanDomain}/admin/api/2024-01/orders/${shopifyOrderId}.json?status=any`,
|
`https://${cleanDomain}/admin/api/2024-01/orders/${shopifyOrderId}.json?status=any`,
|
||||||
@ -173,10 +231,24 @@ async function syncOrderToShopify(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const foJson = (await foRes.json()) as { fulfillment_orders?: Array<{ id: number }> }
|
const foJson = (await foRes.json()) as {
|
||||||
const fulfillmentOrders = foJson.fulfillment_orders || []
|
fulfillment_orders?: Array<{ id: number; status?: string; request_status?: string }>
|
||||||
|
}
|
||||||
|
const allFulfillmentOrders = foJson.fulfillment_orders || []
|
||||||
|
const fulfillmentOrders = allFulfillmentOrders.filter((fo) => {
|
||||||
|
const status = String(fo.status || "").toLowerCase()
|
||||||
|
// Only try fulfillment orders that are still actionable.
|
||||||
|
return status && status !== "closed" && status !== "cancelled" && status !== "incomplete"
|
||||||
|
})
|
||||||
if (fulfillmentOrders.length === 0) {
|
if (fulfillmentOrders.length === 0) {
|
||||||
return { ok: false, error: "No fulfillment orders available for this order." }
|
const closedCount = allFulfillmentOrders.filter((fo) => String(fo.status || "").toLowerCase() === "closed").length
|
||||||
|
if (closedCount > 0) {
|
||||||
|
// Closed fulfillment orders are usually already fulfilled: update tracking on existing fulfillment.
|
||||||
|
const updated = await updateTrackingOnExistingFulfillment()
|
||||||
|
if (updated.ok) return updated
|
||||||
|
return { ok: false, error: `All fulfillment orders are closed. ${updated.error}` }
|
||||||
|
}
|
||||||
|
return { ok: false, error: "No actionable fulfillment orders available for this order." }
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
@ -203,12 +275,14 @@ async function syncOrderToShopify(
|
|||||||
})
|
})
|
||||||
if (!syncRes.ok) {
|
if (!syncRes.ok) {
|
||||||
const details = await syncRes.text().catch(() => "Unknown Shopify response")
|
const details = await syncRes.text().catch(() => "Unknown Shopify response")
|
||||||
|
const updated = await updateTrackingOnExistingFulfillment()
|
||||||
|
if (updated.ok) return updated
|
||||||
const fallback = await createLegacyFulfillmentFallback()
|
const fallback = await createLegacyFulfillmentFallback()
|
||||||
if (fallback.ok) return fallback
|
if (fallback.ok) return fallback
|
||||||
const scopes = await fetchScopes()
|
const scopes = await fetchScopes()
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: `Shopify fulfillment sync failed: ${details}. Fallback also failed: ${fallback.error}. Token scopes: [${scopes.join(", ")}]. Re-install/reconnect app after scope changes.`,
|
error: `Shopify fulfillment sync failed: ${details}. Existing fulfillment tracking update failed: ${updated.error}. Fallback also failed: ${fallback.error}. Token scopes: [${scopes.join(", ")}]. Re-install/reconnect app after scope changes.`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user