diff --git a/web/connector/mocks.js b/web/connector/mocks.js index bd933a2..493661a 100644 --- a/web/connector/mocks.js +++ b/web/connector/mocks.js @@ -108,6 +108,120 @@ export function exportedDFCProducerProducts(stockLimitation = 55) { }`; } +export const exportedSingleTransformlessProducts = `{ + "@context": "https://www.datafoodconsortium.org", + "@graph": [ + { + "@id": "_:b228", + "@type": "dfc-b:QuantitativeValue", + "dfc-b:hasUnit": "dfc-m:Kilogram", + "dfc-b:value": "1" + }, + { + "@id": "_:b229", + "@type": "dfc-b:Price", + "dfc-b:VATrate": "0", + "dfc-b:hasUnit": "dfc-m:Euro", + "dfc-b:value": "5.99" + }, + { + "@id": "_:b230", + "@type": "dfc-b:QuantitativeValue", + "dfc-b:hasUnit": "dfc-m:Kilogram", + "dfc-b:value": "6" + }, + { + "@id": "_:b231", + "@type": "dfc-b:Price", + "dfc-b:VATrate": "0", + "dfc-b:hasUnit": "dfc-m:Euro", + "dfc-b:value": "27.00" + }, + { + "@id": "_:b232", + "@type": "dfc-b:QuantitativeValue", + "dfc-b:hasUnit": "dfc-m:Kilogram", + "dfc-b:value": "3" + }, + { + "@id": "_:b233", + "@type": "dfc-b:Price", + "dfc-b:VATrate": "0", + "dfc-b:hasUnit": "dfc-m:Euro", + "dfc-b:value": "15.99" + }, + { + "@id": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236913913", + "@type": "dfc-b:SuppliedProduct", + "dfc-b:description": "hello", + "dfc-b:hasQuantity": "_:b228", + "dfc-b:image": "https://cdn.shopify.com/s/files/1/0694/6491/6217/products/image_5611ad7e-65bf-4c16-b54f-9086acff0892.jpg?v=1710254054", + "dfc-b:name": "Botanical Flour, #2 Meadow Blend - Catering, kilo, 1kg", + "dfc-b:referencedBy": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236913913/CatalogItem" + }, + { + "@id": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236913913/CatalogItem", + "@type": "dfc-b:CatalogItem", + "dfc-b:offeredThrough": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236913913/Offer", + "dfc-b:sku": "NFB2/1K", + "dfc-b:stockLimitation": "-3" + }, + { + "@id": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236913913/Offer", + "@type": "dfc-b:Offer", + "dfc-b:hasPrice": { + "@id": "_:b229" + } + }, + { + "@id": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236946681", + "@type": "dfc-b:SuppliedProduct", + "dfc-b:description": "hello", + "dfc-b:hasQuantity": "_:b230", + "dfc-b:image": "https://cdn.shopify.com/s/files/1/0694/6491/6217/products/image_5611ad7e-65bf-4c16-b54f-9086acff0892.jpg?v=1710254054", + "dfc-b:name": "Botanical Flour, #2 Meadow Blend - Case, 6 x 1kg", + "dfc-b:referencedBy": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236946681/CatalogItem" + }, + { + "@id": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236946681/CatalogItem", + "@type": "dfc-b:CatalogItem", + "dfc-b:offeredThrough": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236946681/Offer", + "dfc-b:sku": "NFB2/C6", + "dfc-b:stockLimitation": "-2" + }, + { + "@id": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236946681/Offer", + "@type": "dfc-b:Offer", + "dfc-b:hasPrice": { + "@id": "_:b231" + } + }, + { + "@id": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236979449", + "@type": "dfc-b:SuppliedProduct", + "dfc-b:description": "hello", + "dfc-b:hasQuantity": "_:b232", + "dfc-b:image": "https://cdn.shopify.com/s/files/1/0694/6491/6217/products/image_5611ad7e-65bf-4c16-b54f-9086acff0892.jpg?v=1710254054", + "dfc-b:name": "Botanical Flour, #2 Meadow Blend - Catering, small, 3kg", + "dfc-b:referencedBy": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236979449/CatalogItem" + }, + { + "@id": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236979449/CatalogItem", + "@type": "dfc-b:CatalogItem", + "dfc-b:offeredThrough": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236979449/Offer", + "dfc-b:sku": "NFB2/3K", + "dfc-b:stockLimitation": "-7" + }, + { + "@id": "http://localhost:3629/api/dfc/Enterprises/alex-fdc-producer/SuppliedProducts/44518236979449/Offer", + "@type": "dfc-b:Offer", + "dfc-b:hasPrice": { + "@id": "_:b233" + } + } + ] +}`; + export const importedShopifyProductsFromDFC = [ { parentProduct: { diff --git a/web/connector/productUtils.js b/web/connector/productUtils.js index 5a570ad..cfb04e9 100644 --- a/web/connector/productUtils.js +++ b/web/connector/productUtils.js @@ -1,6 +1,6 @@ import { getTargetStringFromSemanticId, throwError } from '../utils/index.js'; -import { loadConnectorWithResources } from './index.js'; +import { loadConnectorWithResources, SuppliedProduct } from './index.js'; import { loadProductTypes, loadQuantityUnits } from './mappings.js'; async function getSingleSuppliedProduct(suppliedProduct) { @@ -104,6 +104,12 @@ async function importSuppliedProducts(dfcProducts) { } } +function findUnmappedSuppliedProducts(graph, retailWholesaleProductIds) { + return graph.filter( + (item) => item instanceof SuppliedProduct && !retailWholesaleProductIds.has(getTargetStringFromSemanticId(item.getSemanticId(), 'SuppliedProducts')) + ); +} + async function getSuppliedProductDetailsFromImports(dfcExportsArray) { const dfcImports = await importSuppliedProducts(dfcExportsArray); @@ -111,10 +117,20 @@ async function getSuppliedProductDetailsFromImports(dfcExportsArray) { (item) => item.getSemanticType() === 'dfc-b:AsPlannedTransformation' ); - return await Promise.all(dfcRetailWholesalePairs.map(toShopifyProduct)); + const retailWholesalePairs = await Promise.all(dfcRetailWholesalePairs.map(retailWholesaleTransformationToShopifyProduct)); + + const retailWholesaleProductIds = new Set(retailWholesalePairs.flatMap(({ retailProduct, wholesaleProduct }) => ([retailProduct.id, wholesaleProduct.id]))); + + const unMappedProducts = await Promise.all(findUnmappedSuppliedProducts(dfcImports, retailWholesaleProductIds).map(suppliedProductToShopifyProduct)); + + return [...retailWholesalePairs, ...unMappedProducts]; +} + +async function suppliedProductToShopifyProduct(suppliedProduct) { + return await createResultObject(suppliedProduct, null, 1); } -async function toShopifyProduct(retailWholesalePair) { +async function retailWholesaleTransformationToShopifyProduct(retailWholesalePair) { const consumptionFlows = await retailWholesalePair.getPlannedConsumptionFlows(); const productionFlows = await retailWholesalePair.getPlannedProductionFlows(); @@ -129,25 +145,29 @@ async function toShopifyProduct(retailWholesalePair) { const wholesaleProduct = await productionFlows[0].getProducedProduct(); const retailProduct = await consumptionFlows[0].getConsumedProduct(); - const retailShopifyProduct = - await getSingleVariantSuppliedProduct(retailProduct); - const wholesaleShopifyProduct = - await getSingleVariantSuppliedProduct(wholesaleProduct); const itemsPerWholesaleVariant = await await consumptionFlows[0] .getQuantity() .getQuantityValue(); + return await createResultObject(retailProduct, wholesaleProduct, itemsPerWholesaleVariant); +} + +async function createResultObject(retailProduct, wholesaleProduct, itemsPerWholesaleVariant) { + const retailShopifyProduct = + await getSingleVariantSuppliedProduct(retailProduct); + const wholesaleShopifyProduct = wholesaleProduct ? await getSingleVariantSuppliedProduct(wholesaleProduct) : retailShopifyProduct; + const parentShopifyProduct = await getSingleSuppliedProduct(retailProduct); parentShopifyProduct.variants = [retailShopifyProduct]; parentShopifyProduct.images = retailShopifyProduct.image ? [ - createImageObject( - retailShopifyProduct.image, - retailShopifyProduct.id, - 1 - ) - ] + createImageObject( + retailShopifyProduct.image, + retailShopifyProduct.id, + 1 + ) + ] : []; return { diff --git a/web/connector/productUtils.test.js b/web/connector/productUtils.test.js index a97d702..9d75cdb 100644 --- a/web/connector/productUtils.test.js +++ b/web/connector/productUtils.test.js @@ -1,6 +1,7 @@ import { exportedDFCProducerProducts, - importedShopifyProductsFromDFC + importedShopifyProductsFromDFC, + exportedSingleTransformlessProducts } from './mocks'; import { generateShopifyFDCProducts } from './productUtils'; @@ -24,4 +25,17 @@ describe('generateShopifyFDCProducts', () => { expect(variant.inventoryQuantity).toEqual(0); expect(variant.inventoryPolicy).toEqual('continue'); }); + + it('Can import single products that dont appear on transformations', async () => { + const result = await generateShopifyFDCProducts(exportedSingleTransformlessProducts); + + expect(result).toHaveLength(3); + + expect(result[0].retailProduct.title).toEqual('Botanical Flour, #2 Meadow Blend - Catering, kilo, 1kg'); + expect(result[0].wholesaleProduct.title).toEqual('Botanical Flour, #2 Meadow Blend - Catering, kilo, 1kg'); + expect(result[1].retailProduct.title).toEqual('Botanical Flour, #2 Meadow Blend - Case, 6 x 1kg'); + expect(result[1].wholesaleProduct.title).toEqual('Botanical Flour, #2 Meadow Blend - Case, 6 x 1kg'); + expect(result[2].retailProduct.title).toEqual('Botanical Flour, #2 Meadow Blend - Catering, small, 3kg'); + expect(result[2].wholesaleProduct.title).toEqual('Botanical Flour, #2 Meadow Blend - Catering, small, 3kg'); + }, 15000) });