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 all 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
25 changes: 19 additions & 6 deletions src/simpleSchemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ const ShippoShippingMethod = new SimpleSchema({
* @type {SimpleSchema}
* @property {String} _id Shipment method Id
* @property {String} name Method name
* @property {String} fulfillmentMethod Method name common identifier
* @property {String} label Public label
* @property {String} group Group, allowed values: `Ground`, `Priority`, `One Day`, `Free`
* @property {Number} cost optional
Expand Down Expand Up @@ -225,6 +226,11 @@ const ShippingMethod = new SimpleSchema({
type: String,
label: "Public Label"
},
"fulfillmentMethod": {
type: String,
optional: true,
label: "Method name Common identifier"
},
"group": {
type: String,
label: "Group",
Expand Down Expand Up @@ -532,7 +538,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 +576,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 +668,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 +741,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 = context.allRegisteredFulfillmentTypes?.registeredFulfillmentTypes;

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