Data4Autos-Shopify-Backend/fulfillmentService.js

158 lines
6.7 KiB
JavaScript
Executable File

const axios = require('axios');
const { log } = require('./logger');
/**
* Create (or retrieve existing) fulfillment service + location for a shop.
* Returns { fulfillmentService, locationId } — locationId is NEVER null if a
* fulfillment service exists, because we fall back to the service's own location.
*/
async function createFulfillmentService(shop, accessToken) {
const client = axios.create({
baseURL: `https://${shop}/admin/api/2025-10/graphql.json`,
headers: {
'X-Shopify-Access-Token': accessToken,
'Content-Type': 'application/json',
},
});
try {
log(shop, `🚀 [FulfillmentService] Creating fulfillment service...`);
// ── Step 1: Create fulfillment service (or retrieve existing) ────────────
const createResp = await client.post('', {
query: `
mutation {
fulfillmentServiceCreate(
name: "Data4Autos Distribution",
callbackUrl: "https://backend.data4autos.com/fulfillment"
) {
fulfillmentService {
id serviceName callbackUrl handle
location { id name }
}
userErrors { field message }
}
}
`,
});
let fulfillmentService = createResp.data?.data?.fulfillmentServiceCreate?.fulfillmentService;
const createErrors = createResp.data?.data?.fulfillmentServiceCreate?.userErrors || [];
if (createErrors.length > 0) {
log(shop, `⚠️ [FulfillmentService] Create errors: ${JSON.stringify(createErrors)}`);
// Service likely already exists — query it instead
log(shop, `🔍 [FulfillmentService] Querying existing fulfillment service...`);
const existingResp = await client.post('', {
query: `
query {
fulfillmentServices(type: THIRD_PARTY) {
id serviceName callbackUrl handle
location { id name }
}
}
`,
});
const services = existingResp.data?.data?.fulfillmentServices || [];
fulfillmentService = services.find(s => s.handle === 'data4autos-distribution') || services[0] || null;
if (!fulfillmentService) {
log(shop, `❌ [FulfillmentService] Could not create or find existing service`);
return { fulfillmentService: null, locationId: null };
}
log(shop, `✅ [FulfillmentService] Found existing: ${fulfillmentService.id}`);
} else {
log(shop, `✅ [FulfillmentService] Created: ${fulfillmentService.id}`);
}
// The fulfillment service has its own Shopify-managed location — use it as fallback
// Shopify sometimes returns location: null from the create mutation — re-query if so
let serviceFallbackLocationId = fulfillmentService?.location?.id || null;
if (!serviceFallbackLocationId) {
try {
log(shop, `🔍 [FulfillmentService] location null from mutation — re-querying fulfillment services...`);
const reQueryResp = await client.post('', {
query: `query { fulfillmentServices(type: THIRD_PARTY) { id handle location { id name } } }`,
});
const svcs = reQueryResp.data?.data?.fulfillmentServices || [];
const matched = svcs.find(s => s.handle === 'data4autos-distribution') || svcs[0] || null;
serviceFallbackLocationId = matched?.location?.id || null;
log(shop, `📍 [FulfillmentService] Re-query location: ${serviceFallbackLocationId}`);
} catch (reQErr) {
log(shop, `⚠️ [FulfillmentService] Re-query failed: ${reQErr.message}`);
}
}
log(shop, `📍 [FulfillmentService] Service location (fallback): ${serviceFallbackLocationId}`);
// ── Step 2: Try to create a custom named location ────────────────────────
// First get the store's primary location address (search broadly, not by exact name)
let storeAddress = null;
try {
const locResp = await client.post('', {
query: `query { locations(first: 1) { nodes { id name address { address1 address2 city province provinceCode country countryCode zip phone } } } }`,
});
const locNode = locResp.data?.data?.locations?.nodes?.[0];
if (locNode) {
storeAddress = locNode.address;
log(shop, `📍 [FulfillmentService] Store address from "${locNode.name}": ${JSON.stringify(storeAddress)}`);
}
} catch (addrErr) {
log(shop, `⚠️ [FulfillmentService] Could not fetch store address: ${addrErr.message}`);
}
let customLocationId = null;
if (storeAddress) {
try {
const provinceCode = storeAddress.provinceCode || storeAddress.province || '';
const countryCode = storeAddress.countryCode || 'US';
const locAddResp = await client.post('', {
query: `
mutation {
locationAdd(input: {
name: "(App) Data4Autos Distribution API",
address: {
address1: ${JSON.stringify(storeAddress.address1 || '')},
address2: ${JSON.stringify(storeAddress.address2 || '')},
city: ${JSON.stringify(storeAddress.city || '')},
provinceCode: ${JSON.stringify(provinceCode)},
countryCode: ${countryCode},
zip: ${JSON.stringify(storeAddress.zip || '')},
phone: ${JSON.stringify(storeAddress.phone || '')}
},
fulfillsOnlineOrders: true
}) {
location { id name }
userErrors { code field message }
}
}
`,
});
const locAddData = locAddResp.data?.data?.locationAdd;
if (locAddData?.userErrors?.length > 0) {
log(shop, `⚠️ [FulfillmentService] locationAdd errors: ${JSON.stringify(locAddData.userErrors)}`);
} else if (locAddData?.location?.id) {
customLocationId = locAddData.location.id;
log(shop, `✅ [FulfillmentService] Custom location created: ${customLocationId}`);
}
} catch (locErr) {
log(shop, `⚠️ [FulfillmentService] locationAdd threw: ${locErr.message}`);
}
}
// Use custom location if created, otherwise fall back to fulfillment service location
const locationId = customLocationId || serviceFallbackLocationId;
log(shop, `📍 [FulfillmentService] Final locationId: ${locationId} (${customLocationId ? 'custom' : 'service fallback'})`);
return { fulfillmentService, locationId };
} catch (error) {
const errDetail = error.response ? JSON.stringify(error.response.data) : error.message;
log(shop, `💥 [FulfillmentService] Request failed: ${errDetail}`);
return { fulfillmentService: null, locationId: null };
}
}
module.exports = { createFulfillmentService };