Skip to content
This repository has been archived by the owner on Sep 21, 2022. It is now read-only.

Accept User input for Fulfillment type in PDP & Fulfillment Grouping #45

Open
wants to merge 5 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: 2
jobs:
build:
docker:
- image: node:12.14.1
- image: node:14.18.1

dependencies:
pre:
Expand All @@ -20,7 +20,7 @@ jobs:

deploy:
docker:
- image: node:12.14.1
- image: node:14.18.1

steps:
- checkout
Expand All @@ -35,7 +35,7 @@ jobs:

lint:
docker:
- image: node:12.14.1
- image: node:14.18.1

steps:
- checkout
Expand All @@ -50,7 +50,7 @@ jobs:

test:
docker:
- image: node:12.14.1
- image: node:14.18.1

steps:
- checkout
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
12.14.1
14.18.1
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "index.js",
"type": "module",
"engines": {
"node": ">=12.14.1"
"node": ">=14.18.1"
},
"homepage": "https://github.com/reactioncommerce/api-plugin-carts",
"url": "https://github.com/reactioncommerce/api-plugin-carts",
Expand Down
5 changes: 3 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import queries from "./queries/index.js";
import resolvers from "./resolvers/index.js";
import schemas from "./schemas/index.js";
import { registerPluginHandlerForCart } from "./registration.js";
import { Cart, CartItem } from "./simpleSchemas.js";
import { Cart, CartItem, Shipment } from "./simpleSchemas.js";
import startup from "./startup.js";

/**
Expand Down Expand Up @@ -59,7 +59,8 @@ export default async function register(app) {
policies,
simpleSchemas: {
Cart,
CartItem
CartItem,
Shipment
}
});
}
6 changes: 6 additions & 0 deletions src/schemas/cart.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ type CartItem implements Node {
"A title for use in cart/orders that conveys the selected product's title + chosen options"
title: String!

"The fulfillment type of the item (if implemented in UI & selected by user) while adding item to cart"
selectedFulfillmentType: String

"The date and time at which this item was last updated"
updatedAt: DateTime!

Expand Down Expand Up @@ -314,6 +317,9 @@ input CartItemInput {

"The number of this item to add to the cart"
quantity: Int!

"The fulfillment type of the item (if implemented in UI & selected by user) while adding item to cart"
selectedFulfillmentType: String
}

"Input that defines a single configuration of a product"
Expand Down
19 changes: 13 additions & 6 deletions src/simpleSchemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ export const CartInvoice = new SimpleSchema({
* @property {String} customsLabelUrl For customs printable label
* @property {ShippoShipment} shippo For Shippo specific properties
*/
const Shipment = new SimpleSchema({
export const Shipment = new SimpleSchema({
"_id": {
type: String,
label: "Shipment Id"
Expand Down Expand Up @@ -570,11 +570,12 @@ const Shipment = new SimpleSchema({
type: String,
optional: true
},
"type": {
type: String,
allowedValues: ["shipping"],
defaultValue: "shipping"
},
// type extended in startup with dynamic allowedValues
// "type": {
// type: String,
// allowedValues: ["shipping"],
// defaultValue: "shipping"
// },
"parcel": {
type: ShippingParcel,
optional: true
Expand Down Expand Up @@ -661,6 +662,7 @@ const CartItemAttribute = new SimpleSchema({
* @property {String} title Cart Item title
* @property {Object} transaction Transaction associated with this item
* @property {String} updatedAt required
* @property {String} selectedFulfillmentType Fulfillment Type (if selected/passed from UI)
* @property {String} variantId required
* @property {String} variantTitle Title from the selected variant
*/
Expand Down Expand Up @@ -733,6 +735,11 @@ export const CartItem = new SimpleSchema({
blackbox: true
},
"updatedAt": Date,
"selectedFulfillmentType": {
type: String,
defaultValue: "",
optional: true
},
"variantId": {
type: String,
optional: true
Expand Down
37 changes: 37 additions & 0 deletions src/startup.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,41 @@ async function updateAllCartsForVariant({ Cart, context, variant }) {
return null;
}

/**
* @summary Extend the schema with updated allowedValues
* @param {Object} context Startup context
* @returns {undefined}
*/
async function extendSchemas(context) {
let allFulfillmentTypesArray = await context.queries.allFulfillmentTypes(context);

if (!allFulfillmentTypesArray || allFulfillmentTypesArray.length === 0) {
Logger.warn("No fulfillment types available, setting 'shipping' as default");
allFulfillmentTypesArray = ["shipping"];
}

const { simpleSchemas: { Shipment, CartItem } } = context;
const schemaShipmentExtension = {
type: {
type: String,
allowedValues: allFulfillmentTypesArray,
defaultValue: allFulfillmentTypesArray[0]
}
};
Shipment.extend(schemaShipmentExtension);

const schemaCartItemExtension = {
"supportedFulfillmentTypes": {
type: Array
},
"supportedFulfillmentTypes.$": {
type: String,
allowedValues: allFulfillmentTypesArray
}
};
CartItem.extend(schemaCartItemExtension);
}

/**
* @summary Called on startup
* @param {Object} context Startup context
Expand All @@ -98,6 +133,8 @@ export default async function cartStartup(context) {
const { appEvents, collections } = context;
const { Cart } = collections;

await extendSchemas(context);

// When an order is created, delete the source cart
appEvents.on("afterOrderCreate", async ({ order }) => {
const { cartId } = order;
Expand Down
8 changes: 7 additions & 1 deletion src/util/addCartItems.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const inputItemSchema = new SimpleSchema({
"price.amount": {
type: Number,
optional: true
},
"selectedFulfillmentType": {
type: String,
optional: true
}
});

Expand All @@ -47,7 +51,7 @@ export default async function addCartItems(context, currentItems, inputItems, op
const currentDateTime = new Date();

const promises = inputItems.map(async (inputItem) => {
const { metafields, productConfiguration, quantity, price } = inputItem;
const { metafields, productConfiguration, quantity, price, selectedFulfillmentType } = inputItem;
const { productId, productVariantId } = productConfiguration;

// Get the published product from the DB, in order to get variant title and check price.
Expand Down Expand Up @@ -111,6 +115,8 @@ export default async function addCartItems(context, currentItems, inputItems, op
compareAtPrice: null,
isTaxable: chosenVariant.isTaxable || false,
metafields,
supportedFulfillmentTypes: catalogProduct.supportedFulfillmentTypes,
selectedFulfillmentType,
optionTitle: chosenVariant.optionTitle,
parcel: chosenVariant.parcel,
// This one will be kept updated by event handler watching for
Expand Down
63 changes: 46 additions & 17 deletions src/util/updateCartFulfillmentGroups.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,32 @@ function determineInitialGroupForItem(currentGroups, supportedFulfillmentTypes,
return compatibleGroup || null;
}

/**
* @summary Check if the provided fulfillment type is present in any of the groups and adds if not
* @param {Object[]} currentGroups The current cart fulfillment groups array
* @param {String} fulfillmentType Specific fulfillment type to be checked
* @param {Object} item The item (product) object
* @returns {undefined}
*/
function checkAndAddToGroup(currentGroups, fulfillmentType, item) {
const group = determineInitialGroupForItem(currentGroups, [fulfillmentType], item.shopId);
if (!group) {
// If no compatible group, add one with initially just this item in it
currentGroups.push({
_id: Random.id(),
itemIds: [item._id],
shopId: item.shopId,
type: fulfillmentType
});
} else if (!group.itemIds) {
// If there is a compatible group but it has no items array, add one with just this item in it
group.itemIds = [item._id];
} else if (!group.itemIds.includes(item._id)) {
// If there is a compatible group with an items array but it is missing this item, add this item ID to the array
group.itemIds.push(item._id);
}
}

/**
* @summary Updates the `shipping` property on a `cart`
* @param {Object} context App context
Expand All @@ -23,37 +49,40 @@ export default function updateCartFulfillmentGroups(context, cart) {
// Every time the cart is updated, create any missing fulfillment groups as necessary.
// We need one group per type per shop, containing only the items from that shop.
// Also make sure that every item is assigned to a fulfillment group.
// Update: Refer MCOSS-52:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit test for this would be great.

// 1. If the selectedFulfillmentType is not provided for an item, then
// that item should be present in all groups corresponding to it's supportedFulfillmentTypes
// If selectedFulfillmentType is provided, we keep the item only in that group.

const currentGroups = cart.shipping || [];

(cart.items || []).forEach((item) => {
let { supportedFulfillmentTypes } = item;

// This is a new optional field that UI can pass in case the user selects fulfillment type
// for each item in the product details page instead of waiting till checkout
const { selectedFulfillmentType } = item;

// Do not re-allocate the item if it is already in the group. Otherwise difficult for other code
// to create and manage fulfillment groups
const itemAlreadyInTheGroup = currentGroups.find(({ itemIds }) => itemIds && itemIds.includes(item._id));
if (itemAlreadyInTheGroup) return;
// Commenting out the below check since the item should below to all supported groups,
// and not just one if the selectedFulfillmentType is not provided.
// const itemAlreadyInTheGroup = currentGroups.find(({ itemIds }) => itemIds && itemIds.includes(item._id));
// if (itemAlreadyInTheGroup) return;

if (!supportedFulfillmentTypes || supportedFulfillmentTypes.length === 0) {
supportedFulfillmentTypes = ["shipping"];
}

// Out of the current groups, returns the one that this item should be in by default, if it isn't
// already in a group
const group = determineInitialGroupForItem(currentGroups, supportedFulfillmentTypes, item.shopId);

if (!group) {
// If no compatible group, add one with initially just this item in it
currentGroups.push({
_id: Random.id(),
itemIds: [item._id],
shopId: item.shopId,
type: supportedFulfillmentTypes[0]
// If selectedFulfillmentType is provided, add the item only to that group, else add item to all supported groups
if (selectedFulfillmentType) {
checkAndAddToGroup(currentGroups, selectedFulfillmentType, item);
} else {
supportedFulfillmentTypes.forEach((ffType) => {
checkAndAddToGroup(currentGroups, ffType, item);
});
} else if (!group.itemIds) {
// If there is a compatible group but it has no items array, add one with just this item in it
group.itemIds = [item._id];
} else if (!group.itemIds.includes(item._id)) {
// If there is a compatible group with an items array but it is missing this item, add this item ID to the array
group.itemIds.push(item._id);
}
});

Expand Down
2 changes: 1 addition & 1 deletion src/xforms/xformCartCheckout.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function xformCartFulfillmentGroup(fulfillmentGroup, cart) {
shippingAddress: fulfillmentGroup.address,
shopId: fulfillmentGroup.shopId,
// For now, this is always shipping. Revisit when adding download, pickup, etc. types
type: "shipping"
type: fulfillmentGroup.type
};
}

Expand Down