diff --git a/fulfillmentService.js b/fulfillmentService.js index 6808a73..c06b3df 100755 --- a/fulfillmentService.js +++ b/fulfillmentService.js @@ -1,96 +1,12 @@ const axios = require('axios'); const { log } = require('./logger'); -/** - * Logs messages with a shop context. - * @param {string} shop - Shopify store name. - * @param {string} message - Log message. - */ - - -const getLocationQuery = ` - query { - - locations(first: 1, query: "name:'Shop location'" ) { - nodes { - id - name - address { - address1 - address2 - city - province - provinceCode - country - countryCode - zip - phone - } - } - } - } -`; - -async function getStoreAddress(client) { - const response = await client.post('', { query: getLocationQuery }); - console.log('Store locations response:', response.data); - const location = response.data.data.locations.nodes[0]; - return location.address; -} - -const createLocationMutation = (address) => ` - mutation { - locationAdd(input: { - name: "(App) Data4Autos Distribution API", - address: { - address1: "${address.address1 || ''}", - address2: "${address.address2 || ''}", - city: "${address.city}", - provinceCode: "${address.phone || 'ON'}", - countryCode: ${address.countryCode}, - zip: "${address.zip || ''}", - phone: "${address.phone || ''}" - }, - fulfillsOnlineOrders: true - }) { - location { - id - name - address { - address1 - city - provinceCode - countryCode - zip - phone - } - } - userErrors { - code - field - message - } - } - } -`; - -async function createCustomLocation(address, client) { - const mutation = createLocationMutation(address); - - - const response = await client.post('', { query: mutation }); -// console.log('Location creation response:', response.data); - return response.data.data.locationAdd; -} - /** - * Create a Shopify fulfillment service. - * @param {string} shop - The Shopify store domain (e.g., myshop.myshopify.com). - * @param {string} accessToken - The Shopify Admin API access token. - * @returns {Promise} The created fulfillment service or error details. + * 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: { @@ -99,183 +15,128 @@ async function createFulfillmentService(shop, accessToken) { }, }); - const mutation = ` - mutation { - fulfillmentServiceCreate( - name: "Data4Autos Distribution", - callbackUrl: "https://backend.data4autos.com/fulfillment" - ) { - fulfillmentService { - id - serviceName - callbackUrl - handle - location { - id + 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 } } } - 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 + const serviceFallbackLocationId = fulfillmentService?.location?.id || null; + 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}`); } } - `; - // const mutation = ` - // mutation { - // fulfillmentServiceCreate( - // name: "Data4Autos Distribution", - // callbackUrl: "https://backend.data4autos.com/fulfillment", - // fulfillmentOrdersOptIn: true, - // inventoryManagement: true, - // trackingSupport: true, - // requiresShippingMethod: true - // - // ) { - // fulfillmentService { - // id - // serviceName - // callbackUrl - // handle - // location { - // id - // } - // } - // userErrors { - // field - // 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'})`); - try { - log(shop, `🚀 Creating fulfillment service...`); - const response = await client.post('', { query: mutation }); - const data = response.data.data.fulfillmentServiceCreate; + return { fulfillmentService, locationId }; - if (data.userErrors && data.userErrors.length > 0) { - log(shop, `❌ User errors: ${JSON.stringify(data.userErrors)}`); - return { success: false, errors: data.userErrors }; - } - - log(shop, `✅ Fulfillment Service created: ${JSON.stringify(data.fulfillmentService)}`); - - - - // Step 1: Get the store address - const address = await getStoreAddress(client); - - // Step 2: Create a new location with a custom name and the same address - // console.log(address) - const result = await createCustomLocation(address, client); - // console.log('Result from creating custom location:', result); - if (result.userErrors && result.userErrors.length > 0) { - console.error('Location creation errors:', result.userErrors); - } else { - console.log('Created location:', result.location); - } - - - var locationId = result.location ? result.location.id : null; - - - - return { success: true, fulfillmentService: data.fulfillmentService, locationId }; } catch (error) { - log(shop, `💥 Request failed: ${error.response ? JSON.stringify(error.response.data) : error.message}`); - return { success: false, error: error.response ? error.response.data : error.message }; + 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 }; - - - - - - - - - - - - - -// const axios = require('axios'); -// const { log } = require('./logger'); - -// /** -// * Create a Shopify fulfillment service. -// * @param {string} shop - The Shopify store domain (e.g., myshop.myshopify.com). -// * @param {string} accessToken - The Shopify Admin API access token. -// * @returns {Promise} The created fulfillment service or error details. -// */ -// async function createFulfillmentService(shop, accessToken) { -// const client = axios.create({ -// baseURL: `https://${shop}/admin/api/2024-01/graphql.json`, -// headers: { -// 'X-Shopify-Access-Token': accessToken, -// 'Content-Type': 'application/json', -// }, -// }); - -// const mutation = ` -// mutation { -// fulfillmentServiceCreate( -// name: "Data4Autos Distribution", -// callbackUrl: "https://backend.data4autos.com/fulfillment", -// fulfillmentOrdersOptIn: true, -// inventoryManagement: true, -// trackingSupport: true -// ) { -// fulfillmentService { -// id -// serviceName -// callbackUrl -// handle -// location { -// id -// } -// } -// userErrors { -// field -// message -// } -// } -// } -// `; - -// try { -// log(shop, `🚀 Creating fulfillment service...`); -// const response = await client.post('', { query: mutation }); -// console.log(`Response from Shopify: ${JSON.stringify(response.data, null, 2)}`); -// const data = response.data.data.fulfillmentServiceCreate; - -// if (data.userErrors && data.userErrors.length > 0) { -// console.log(shop, `❌ User errors: ${JSON.stringify(data.userErrors)}`); -// return { success: false, errors: data.userErrors }; -// } - -// log(shop, `✅ Fulfillment Service created: ${JSON.stringify(data.fulfillmentService)}`); -// return { success: true, fulfillmentService: data.fulfillmentService }; -// } catch (error) { -// log(shop, `💥 Request failed: ${error.response ? JSON.stringify(error.response.data) : error.message}`); -// return { success: false, error: error.response ? error.response.data : error.message }; -// } -// } - -// module.exports = { createFulfillmentService }; - -// // ------------------------------- -// // Test call (comment out later) -// // ------------------------------- -// // (async () => { -// // const shop = "veloxautomotive.myshopify.com"; // Replace with your shop domain -// // const accessToken = "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"; // Replace with your token - -// // const result = await createFulfillmentService(shop, accessToken); -// // console.log("Result:", result); -// // })(); diff --git a/server.js b/server.js index edfbebc..bd08738 100755 --- a/server.js +++ b/server.js @@ -11,10 +11,11 @@ const managepricing = require('./routes/managePricing'); const adminPanel = require('./routes/adminPanel'); const privacyLawWebhooks = require('./routes/privacyLawWebhooks'); -const { getToken, listTokens } = require('./tokenStore'); +const { getToken, saveToken, listTokens } = require('./tokenStore'); const { listJobs, getJob, cancelJob, getLatestJobForShop } = require('./jobStore'); const { isShopAllowed } = require('./freeAccessStore'); const { addMessage: addChatMessage, readChat } = require('./chatStore'); +const { createFulfillmentService } = require('./fulfillmentService'); const app = express(); const PORT = process.env.PORT || 3002; @@ -32,6 +33,44 @@ app.get('/free-access/:shop', (req, res) => { res.json({ shop, allowed }); }); +// ── BACKFILL: Re-run fulfillment service + locationId for shops that have null ─ +// Hit: GET /admin/fix-location → runs all shops with null locationId +// Hit: GET /admin/fix-location/:shop → runs one specific shop +app.get('/admin/fix-location/:shop?', async (req, res) => { + const { createFulfillmentService } = require('./fulfillmentService'); + const stores = listTokens(); + const targetShop = req.params.shop ? decodeURIComponent(req.params.shop).toLowerCase().trim() : null; + + const toFix = Object.entries(stores).filter(([shop, record]) => { + if (targetShop) return shop === targetShop; + return !record.locationId; // only null locationId shops + }); + + if (toFix.length === 0) { + return res.json({ message: 'No shops need fixing', checked: Object.keys(stores).length }); + } + + const results = []; + for (const [shop, record] of toFix) { + if (!record.accessToken) { + results.push({ shop, status: 'skipped', reason: 'no accessToken' }); + continue; + } + try { + log(shop, `🔧 [BACKFILL] Re-running fulfillment setup for ${shop}`); + const { fulfillmentService, locationId } = await createFulfillmentService(shop, record.accessToken); + saveToken(shop, record.accessToken, record.scope, fulfillmentService, locationId); + results.push({ shop, status: 'fixed', locationId, fulfillmentServiceId: fulfillmentService?.id || null }); + log(shop, `✅ [BACKFILL] locationId=${locationId}`); + } catch (err) { + results.push({ shop, status: 'error', error: err.message }); + log(shop, `❌ [BACKFILL] Error: ${err.message}`); + } + } + + res.json({ fixed: results.filter(r => r.status === 'fixed').length, total: toFix.length, results }); +}); + // ── PUBLIC CHAT ENDPOINTS (for customer widget) ────────────────────────────── // widget.js MUST be before /chat/:shop — otherwise Express treats "widget.js" as the :shop param app.get('/chat/widget.js', (req, res) => {