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)
});