diff --git a/web/api-modules/products/use-cases/get-products.js b/web/api-modules/products/use-cases/get-products.js index 9b9991e..6480678 100644 --- a/web/api-modules/products/use-cases/get-products.js +++ b/web/api-modules/products/use-cases/get-products.js @@ -14,7 +14,7 @@ const toProduct = (product) => ({ async function findProducts(client) { const response = await client.request(` { - products(first: 250) { + products(first: 250, sortKey: TITLE) { edges { node { id diff --git a/web/connector/productUtils.js b/web/connector/productUtils.js index 6b8c9eb..d3edb11 100644 --- a/web/connector/productUtils.js +++ b/web/connector/productUtils.js @@ -2,6 +2,7 @@ import { throwError } from '../utils/index.js'; import loadConnectorWithResources from './index.js'; import loadProductTypes from './mappedProductTypes.js'; import config from '../config.js'; +import currencyMeasureFor from '../utils/currencyMeasureFor.js'; const createQuantitativeValue = (connector, value, unit) => connector.createQuantity({ @@ -45,7 +46,6 @@ async function createVariantSuppliedProduct( try { const connector = await loadConnectorWithResources(); const kilogram = connector.MEASURES.UNIT.QUANTITYUNIT.KILOGRAM; - const euro = connector.MEASURES.UNIT.CURRENCYUNIT.EURO; const semanticBase = `${config.PRODUCER_SHOP_URL}api/dfc/Enterprises/${enterpriseName}/SuppliedProducts/${variant.id}`; @@ -55,7 +55,12 @@ async function createVariantSuppliedProduct( kilogram ); const hasVat = variant.taxable ? 1.0 : 0.0; // TODO check how the vat rate can be added - const price = createPrice(connector, variant.price, euro, hasVat); + const price = createPrice( + connector, + variant.price, + currencyMeasureFor(connector, variant.currencyCode), + hasVat + ); const offer = createOffer(connector, semanticBase, price); const inventoryQuantity = variant.inventoryPolicy === 'CONTINUE' ? -1 : variant.inventoryQuantity; diff --git a/web/fdc-modules/orders/dfc/dfc-order.js b/web/fdc-modules/orders/dfc/dfc-order.js index 261071c..60dbad1 100644 --- a/web/fdc-modules/orders/dfc/dfc-order.js +++ b/web/fdc-modules/orders/dfc/dfc-order.js @@ -1,218 +1,298 @@ -import loadConnectorWithResources from '../../../connector/index.js'; import { OrderLine, Order, SaleSession } from '@datafoodconsortium/connector'; -import * as ids from '../controllers/shopify/ids.js' +import loadConnectorWithResources from '../../../connector/index.js'; +import * as ids from '../controllers/shopify/ids.js'; import config from '../../../config.js'; +import currencyMeasureFor from '../../../utils/currencyMeasureFor.js'; export async function extractOrderLine(payload) { - const connector = await loadConnectorWithResources(); - + const connector = await loadConnectorWithResources(); - const orderLines = (await connector.import(payload)).filter(item => item instanceof OrderLine); + const orderLines = (await connector.import(payload)).filter( + (item) => item instanceof OrderLine + ); - if (orderLines.length !== 1) { - throw Error('Single OrderLine not present in graph'); - } + if (orderLines.length !== 1) { + throw Error('Single OrderLine not present in graph'); + } - return orderLines[0]; + return orderLines[0]; } export async function extractOrderAndLinesAndSalesSession(payload) { - return await extract(payload, true); + return await extract(payload, true); } export async function extractOrderAndLines(payload) { - return await extract(payload, false); + return await extract(payload, false); } async function extract(payload, requireSalesSession) { - const connector = await loadConnectorWithResources(); + const connector = await loadConnectorWithResources(); - const deserialised = await connector.import(payload); + const deserialised = await connector.import(payload); - const orders = deserialised.filter( - (item) => item instanceof Order - ); + const orders = deserialised.filter((item) => item instanceof Order); - const lines = deserialised.filter( - (item) => item instanceof OrderLine - ); + const lines = deserialised.filter((item) => item instanceof OrderLine); - const saleSessions = deserialised.filter( - (item) => item instanceof SaleSession - ); + const saleSessions = deserialised.filter( + (item) => item instanceof SaleSession + ); - if (orders.length !== 1) { - throw Error('Order missing'); - } + if (orders.length !== 1) { + throw Error('Order missing'); + } - const order = orders[0]; + const order = orders[0]; - if ((await order.getLines()).length !== lines.length) { - throw Error('Graph is missing OrderLine'); - } + if ((await order.getLines()).length !== lines.length) { + throw Error('Graph is missing OrderLine'); + } - if (!requireSalesSession) { - return order; - } else { - if (saleSessions.length !== 1) { - throw Error('Graph must contain single SalesSession'); - } - return { order, saleSession: saleSessions[0] }; + if (!requireSalesSession) { + return order; + } else { + if (saleSessions.length !== 1) { + throw Error('Graph must contain single SalesSession'); } + return { order, saleSession: saleSessions[0] }; + } } -function createOrderLine(connector, line, lineIdMappings, enterpriseName, orderId) { - const suppliedProduct = connector.createSuppliedProduct({ - semanticId: `${config.PRODUCER_SHOP_URL}api/dfc/Enterprises/${enterpriseName}/SuppliedProducts/${ids.extract(line.variant.id)}` - }); - - const mapping = lineIdMappings.find(({shopifyId}) => shopifyId.toString() === ids.extract(line.id)); - if (!mapping) { - throw new Error(`Need to do something here when the draft order contains a non dfc line.... ${line.id}`); - } - - const madeUpIdForTheOfferSoTheConnectorWorks = `${config.PRODUCER_SHOP_URL}api/dfc/Enterprises/${enterpriseName}/Offers/${ids.extract(line.variant.id)}` - - const offer = connector.createOffer({ - semanticId: madeUpIdForTheOfferSoTheConnectorWorks, - offeredItem: suppliedProduct - }); - - const { amount, currencyCode } = line.originalUnitPriceSet.shopMoney; - - const price = connector.createPrice({ - value: amount, - unit: currencyMeasureFor(connector, currencyCode) - }); - - return [ - suppliedProduct, - offer, - connector.createOrderLine({ - semanticId: `${config.PRODUCER_SHOP_URL}api/dfc/Enterprises/${enterpriseName}/Orders/${orderId}/orderLines/${mapping.externalId.toString()}`, - offer: offer, - price: price, - quantity: line.quantity - })]; -} - -function createOrderLines(connector, shopifyDraftOrderResponse, lineIdMappings, enterpriseName, orderId) { - const shopifyLineItems = shopifyDraftOrderResponse.lineItems; - return shopifyLineItems - .filter(({custom}) => !custom) - .flatMap((line) => { - return createOrderLine(connector, line, lineIdMappings, enterpriseName, orderId); - }) +function createOrderLine( + connector, + line, + lineIdMappings, + enterpriseName, + orderId +) { + const suppliedProduct = connector.createSuppliedProduct({ + semanticId: `${ + config.PRODUCER_SHOP_URL + }api/dfc/Enterprises/${enterpriseName}/SuppliedProducts/${ids.extract( + line.variant.id + )}` + }); + + const mapping = lineIdMappings.find( + ({ shopifyId }) => shopifyId.toString() === ids.extract(line.id) + ); + if (!mapping) { + throw new Error( + `Need to do something here when the draft order contains a non dfc line.... ${line.id}` + ); + } + + const madeUpIdForTheOfferSoTheConnectorWorks = `${ + config.PRODUCER_SHOP_URL + }api/dfc/Enterprises/${enterpriseName}/Offers/${ids.extract( + line.variant.id + )}`; + + const offer = connector.createOffer({ + semanticId: madeUpIdForTheOfferSoTheConnectorWorks, + offeredItem: suppliedProduct + }); + + const { amount, currencyCode } = line.originalUnitPriceSet.shopMoney; + + const price = connector.createPrice({ + value: amount, + unit: currencyMeasureFor(connector, currencyCode) + }); + + return [ + suppliedProduct, + offer, + connector.createOrderLine({ + semanticId: `${ + config.PRODUCER_SHOP_URL + }api/dfc/Enterprises/${enterpriseName}/Orders/${orderId}/orderLines/${mapping.externalId.toString()}`, + offer: offer, + price: price, + quantity: line.quantity + }) + ]; } -async function createUnexportedDfcOrderFromShopify(shopifyDraftOrderResponse, lineIdMappings, enterpriseName) { - const connector = await loadConnectorWithResources(); - - const orderId = ids.extract(shopifyDraftOrderResponse.id) - - const dfcOrderLinesGraph = createOrderLines(connector, shopifyDraftOrderResponse, lineIdMappings, enterpriseName, orderId); - - const order = connector.createOrder({ - semanticId: `${config.PRODUCER_SHOP_URL}api/dfc/Enterprises/${enterpriseName}/Orders/${orderId}`, - lines: dfcOrderLinesGraph.filter((item) => item instanceof OrderLine), - orderStatus: orderStatusFor(connector, shopifyDraftOrderResponse.status), - fulfilmentStatus: fulfilmentStatusFor(connector, shopifyDraftOrderResponse.order) +function createOrderLines( + connector, + shopifyDraftOrderResponse, + lineIdMappings, + enterpriseName, + orderId +) { + const shopifyLineItems = shopifyDraftOrderResponse.lineItems; + return shopifyLineItems + .filter(({ custom }) => !custom) + .flatMap((line) => { + return createOrderLine( + connector, + line, + lineIdMappings, + enterpriseName, + orderId + ); }); - - return [order, ...dfcOrderLinesGraph]; } -export async function createDfcOrderFromShopify(shopifyDraftOrderResponse, lineIdMappings, enterpriseName) { - const connector = await loadConnectorWithResources(); - const graph = await createUnexportedDfcOrderFromShopify(shopifyDraftOrderResponse, lineIdMappings, enterpriseName); - return await connector.export(graph); +async function createUnexportedDfcOrderFromShopify( + shopifyDraftOrderResponse, + lineIdMappings, + enterpriseName +) { + const connector = await loadConnectorWithResources(); + + const orderId = ids.extract(shopifyDraftOrderResponse.id); + + const dfcOrderLinesGraph = createOrderLines( + connector, + shopifyDraftOrderResponse, + lineIdMappings, + enterpriseName, + orderId + ); + + const order = connector.createOrder({ + semanticId: `${config.PRODUCER_SHOP_URL}api/dfc/Enterprises/${enterpriseName}/Orders/${orderId}`, + lines: dfcOrderLinesGraph.filter((item) => item instanceof OrderLine), + orderStatus: orderStatusFor(connector, shopifyDraftOrderResponse.status), + fulfilmentStatus: fulfilmentStatusFor( + connector, + shopifyDraftOrderResponse.order + ) + }); + + return [order, ...dfcOrderLinesGraph]; } -export async function createBulkDfcOrderFromShopify(shopifyDraftOrderResponses, lineIdMappingsByDraftId, enterpriseName) { - const connector = await loadConnectorWithResources(); - const megaGraph = await (Promise.all(shopifyDraftOrderResponses.map(async draftOrderResponse => { - const lineItemIdMapping = lineIdMappingsByDraftId.find(({ draftOrderId }) => draftOrderId === ids.extract(draftOrderResponse.id)); - - if (!lineItemIdMapping) { - return []; - } - - return await createUnexportedDfcOrderFromShopify(draftOrderResponse, lineItemIdMapping.lineItems, enterpriseName) - }))); - - return await connector.export(megaGraph.flat()); +export async function createDfcOrderFromShopify( + shopifyDraftOrderResponse, + lineIdMappings, + enterpriseName +) { + const connector = await loadConnectorWithResources(); + const graph = await createUnexportedDfcOrderFromShopify( + shopifyDraftOrderResponse, + lineIdMappings, + enterpriseName + ); + return await connector.export(graph); } -export async function createDfcOrderLinesFromShopify(shopifyDraftOrderResponse, lineIdMappings, enterpriseName, orderId) { - const connector = await loadConnectorWithResources(); - - const dfcOrderLines = createOrderLines(connector, shopifyDraftOrderResponse, lineIdMappings, enterpriseName, orderId); - - return await connector.export(dfcOrderLines); +export async function createBulkDfcOrderFromShopify( + shopifyDraftOrderResponses, + lineIdMappingsByDraftId, + enterpriseName +) { + const connector = await loadConnectorWithResources(); + const megaGraph = await Promise.all( + shopifyDraftOrderResponses.map(async (draftOrderResponse) => { + const lineItemIdMapping = lineIdMappingsByDraftId.find( + ({ draftOrderId }) => + draftOrderId === ids.extract(draftOrderResponse.id) + ); + + if (!lineItemIdMapping) { + return []; + } + + return await createUnexportedDfcOrderFromShopify( + draftOrderResponse, + lineItemIdMapping.lineItems, + enterpriseName + ); + }) + ); + + return await connector.export(megaGraph.flat()); } -export async function createDfcOrderLineFromShopify(shopifyDraftOrderResponse, externalLineId, lineIdMappings, enterpriseName, orderId) { - const connector = await loadConnectorWithResources(); - - const shopifyLineId = lineIdMappings.find(({externalId}) => externalId.toString() === externalLineId.toString())?.shopifyId; - - if (!shopifyLineId) { - return null; - } - - const line = shopifyDraftOrderResponse.lineItems.find((line) => ids.extract(line.id) === shopifyLineId) - - return await connector.export(createOrderLine(connector, line, lineIdMappings, enterpriseName, orderId)); +export async function createDfcOrderLinesFromShopify( + shopifyDraftOrderResponse, + lineIdMappings, + enterpriseName, + orderId +) { + const connector = await loadConnectorWithResources(); + + const dfcOrderLines = createOrderLines( + connector, + shopifyDraftOrderResponse, + lineIdMappings, + enterpriseName, + orderId + ); + + return await connector.export(dfcOrderLines); } -function currencyMeasureFor(connector, currencyCode) { - const measure = { - 'EUR': connector.MEASURES.UNIT.CURRENCYUNIT.EURO, - 'GBP': connector.MEASURES.UNIT.CURRENCYUNIT.POUNDSTERLING, - 'USD': connector.MEASURES.UNIT.CURRENCYUNIT.USDOLLAR - }[currencyCode]; - - if (!measure) { - throw new Error(`Unknown connector currency mapping for currenct code ${currencyCode}`); - } - - return measure; +export async function createDfcOrderLineFromShopify( + shopifyDraftOrderResponse, + externalLineId, + lineIdMappings, + enterpriseName, + orderId +) { + const connector = await loadConnectorWithResources(); + + const shopifyLineId = lineIdMappings.find( + ({ externalId }) => externalId.toString() === externalLineId.toString() + )?.shopifyId; + + if (!shopifyLineId) { + return null; + } + + const line = shopifyDraftOrderResponse.lineItems.find( + (line) => ids.extract(line.id) === shopifyLineId + ); + + return await connector.export( + createOrderLine(connector, line, lineIdMappings, enterpriseName, orderId) + ); } function orderStatusFor(connector, shopifyDraftOrderStatus) { - const status = { - 'OPEN': connector.VOCABULARY.STATES.ORDERSTATE.HELD, - 'INVOICE_SENT': connector.VOCABULARY.STATES.ORDERSTATE.HELD, - 'COMPLETED': connector.VOCABULARY.STATES.ORDERSTATE.COMPLETE, - }[shopifyDraftOrderStatus]; - - if (!status) { - throw new Error(`Unknown connector order status mapping for ${shopifyDraftOrderStatus}`); - } + const status = { + OPEN: connector.VOCABULARY.STATES.ORDERSTATE.HELD, + INVOICE_SENT: connector.VOCABULARY.STATES.ORDERSTATE.HELD, + COMPLETED: connector.VOCABULARY.STATES.ORDERSTATE.COMPLETE + }[shopifyDraftOrderStatus]; + + if (!status) { + throw new Error( + `Unknown connector order status mapping for ${shopifyDraftOrderStatus}` + ); + } - return status; + return status; } function fulfilmentStatusFor(connector, order) { - if (!order || !order.displayFulfillmentStatus) { - return null; - } - - const status = { - 'FULFILLED': connector.VOCABULARY.STATES.FULFILMENTSTATE.FULFILLED, - 'IN_PROGRESS': connector.VOCABULARY.STATES.FULFILMENTSTATE.UNFULFILLED, - 'ON_HOLD': connector.VOCABULARY.STATES.FULFILMENTSTATE.HELD, - 'OPEN': connector.VOCABULARY.STATES.FULFILMENTSTATE.UNFULFILLED, - 'PARTIALLY_FULFILLED': connector.VOCABULARY.STATES.FULFILMENTSTATE.UNFULFILLED, - 'PENDING_FULFILLMENT': connector.VOCABULARY.STATES.FULFILMENTSTATE.UNFULFILLED, - 'RESTOCKED': connector.VOCABULARY.STATES.FULFILMENTSTATE.UNFULFILLED, - 'SCHEDULED': connector.VOCABULARY.STATES.FULFILMENTSTATE.HELD, - 'UNFULFILLED': connector.VOCABULARY.STATES.FULFILMENTSTATE.UNFULFILLED, - }[order.displayFulfillmentStatus]; - - if (!status) { - throw new Error(`Unknown connector fulfilment status mapping for ${order.displayFulfillmentStatus}`); - } + if (!order || !order.displayFulfillmentStatus) { + return null; + } + + const status = { + FULFILLED: connector.VOCABULARY.STATES.FULFILMENTSTATE.FULFILLED, + IN_PROGRESS: connector.VOCABULARY.STATES.FULFILMENTSTATE.UNFULFILLED, + ON_HOLD: connector.VOCABULARY.STATES.FULFILMENTSTATE.HELD, + OPEN: connector.VOCABULARY.STATES.FULFILMENTSTATE.UNFULFILLED, + PARTIALLY_FULFILLED: + connector.VOCABULARY.STATES.FULFILMENTSTATE.UNFULFILLED, + PENDING_FULFILLMENT: + connector.VOCABULARY.STATES.FULFILMENTSTATE.UNFULFILLED, + RESTOCKED: connector.VOCABULARY.STATES.FULFILMENTSTATE.UNFULFILLED, + SCHEDULED: connector.VOCABULARY.STATES.FULFILMENTSTATE.HELD, + UNFULFILLED: connector.VOCABULARY.STATES.FULFILMENTSTATE.UNFULFILLED + }[order.displayFulfillmentStatus]; + + if (!status) { + throw new Error( + `Unknown connector fulfilment status mapping for ${order.displayFulfillmentStatus}` + ); + } - return status; -} \ No newline at end of file + return status; +} diff --git a/web/fdc-modules/products/controllers/shopify/products.js b/web/fdc-modules/products/controllers/shopify/products.js index 76b1edc..22530d3 100644 --- a/web/fdc-modules/products/controllers/shopify/products.js +++ b/web/fdc-modules/products/controllers/shopify/products.js @@ -6,6 +6,9 @@ import { } from '../../../../database/variants/variants.js'; const query = `query findProducts($ids: [ID!]!) { + shop { + currencyCode + } products: nodes(ids: $ids) { ... on Product { id @@ -71,7 +74,7 @@ export async function getFdcVariantsByProductIdFromDB(productId) { return mappedVariantsByProductId; } -const toFdcProduct = (product) => ({ +const toFdcProduct = (product, currencyCode) => ({ ...product, id: getShopifyIdSubstring(product?.id), images: product?.images?.edges?.map(({ node }) => ({ @@ -81,6 +84,7 @@ const toFdcProduct = (product) => ({ variants: product.variants.edges.map(({ node: variant }) => ({ ...variant, id: getShopifyIdSubstring(variant.id), + currencyCode, image: variant.image && { ...variant.image, id: getShopifyIdSubstring(variant.image.id) @@ -92,15 +96,15 @@ export async function findProductsByIds(client, ids) { const response = await client.request(query, { variables: { ids: ids.map((id) => `gid://shopify/Product/${id}`) } }); - if (response.errors) { throw new Error('Failed to load Products'); } const products = response.data.products?.filter((product) => product !== null) || []; + const currencyCode = response.data?.shop?.currencyCode || 'GBP'; if (products.length > 0) { - return products.map((product) => toFdcProduct(product)); + return products.map((product) => toFdcProduct(product, currencyCode)); } return []; } diff --git a/web/utils/currencyMeasureFor.js b/web/utils/currencyMeasureFor.js new file mode 100644 index 0000000..6b3a04d --- /dev/null +++ b/web/utils/currencyMeasureFor.js @@ -0,0 +1,17 @@ +function currencyMeasureFor(connector, currencyCode) { + const measure = { + EUR: connector.MEASURES.UNIT.CURRENCYUNIT.EURO, + GBP: connector.MEASURES.UNIT.CURRENCYUNIT.POUNDSTERLING, + USD: connector.MEASURES.UNIT.CURRENCYUNIT.USDOLLAR + }[currencyCode]; + + if (!measure) { + throw new Error( + `Unknown connector currency mapping for currenct code ${currencyCode}` + ); + } + + return measure; +} + +export default currencyMeasureFor;