Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add moveOrderItems mutation #5018

Merged
merged 14 commits into from
Mar 14, 2019
Merged
Show file tree
Hide file tree
Changes from 7 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`throws if an order item doesn't exist 1`] = `"Some order items not found"`;

exports[`throws if fromFulfillmentGroupId isn't supplied 1`] = `"From fulfillment group ID is required"`;

exports[`throws if itemIds is empty 1`] = `"You must specify at least 1 values"`;

exports[`throws if itemIds isn't supplied 1`] = `"Item ids is required"`;

exports[`throws if orderId isn't supplied 1`] = `"Order ID is required"`;

exports[`throws if permission check fails 1`] = `"Access Denied"`;

exports[`throws if the database update fails 1`] = `"queries.getFulfillmentMethodsWithQuotes is not a function"`;

exports[`throws if the from group would have no items remaining 1`] = `"move would result in group having no items"`;

exports[`throws if the fromFulfillmentGroup doesn't exist 1`] = `"Order fulfillment group (from) not found"`;

exports[`throws if the order doesn't exist 1`] = `"Order not found"`;

exports[`throws if the toFulfillmentGroup doesn't exist 1`] = `"Order fulfillment group (to) not found"`;

exports[`throws if toFulfillmentGroupId isn't supplied 1`] = `"To fulfillment group ID is required"`;

exports[`throws if user who placed order tries to move item at invalid current item status 1`] = `"Item status (processing) is not one of: new"`;

exports[`throws if user who placed order tries to move item at invalid current order status 1`] = `"Order status (processing) is not one of: new"`;
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import addOrderFulfillmentGroup from "./addOrderFulfillmentGroup";
import cancelOrderItem from "./cancelOrderItem";
import moveOrderItems from "./moveOrderItems";
import placeOrder from "./placeOrder";
import updateOrder from "./updateOrder";
import updateOrderFulfillmentGroup from "./updateOrderFulfillmentGroup";

export default {
addOrderFulfillmentGroup,
cancelOrderItem,
moveOrderItems,
placeOrder,
updateOrder,
updateOrderFulfillmentGroup
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import SimpleSchema from "simpl-schema";
import ReactionError from "@reactioncommerce/reaction-error";
import { Order as OrderSchema } from "/imports/collections/schemas";
import updateGroupStatusFromItemStatus from "../util/updateGroupStatusFromItemStatus";
import updateGroupTotals from "../util/updateGroupTotals";

// These should eventually be configurable in settings
const itemStatusesThatOrdererCanMove = ["new"];
const orderStatusesThatOrdererCanMove = ["new"];

const inputSchema = new SimpleSchema({
"fromFulfillmentGroupId": String,
"itemIds": {
type: Array,
minCount: 1
},
"itemIds.$": String,
"orderId": String,
"toFulfillmentGroupId": String
});

/**
* @method moveOrderItems
* @summary Use this mutation to move one or more items between existing order
* fulfillment groups.
* @param {Object} context - an object containing the per-request state
* @param {Object} input - Necessary input. See SimpleSchema
* @return {Promise<Object>} Object with `order` property containing the updated order
*/
export default async function moveOrderItems(context, input) {
inputSchema.validate(input);

const {
fromFulfillmentGroupId,
itemIds,
orderId,
toFulfillmentGroupId
} = input;

const { accountId, appEvents, collections, isInternalCall, userHasPermission, userId } = context;
const { Orders } = collections;

// First verify that this order actually exists
const order = await Orders.findOne({ _id: orderId });
if (!order) throw new ReactionError("not-found", "Order not found");

// Allow move if the account that placed the order is attempting to move
// or if the account has "orders" permission. When called internally by another
// plugin, context.isInternalCall can be set to `true` to disable this check.
if (
!isInternalCall &&
(!accountId || accountId !== order.accountId) &&
!userHasPermission(["orders"], order.shopId)
) {
throw new ReactionError("access-denied", "Access Denied");
}

// Is the account calling this mutation also the account that placed the order?
// We need this check in a couple places below, so we'll get it here.
const accountIsOrderer = (order.accountId && accountId === order.accountId);

// The orderer may only move items while the order status is still "new"
if (accountIsOrderer && !orderStatusesThatOrdererCanMove.includes(order.workflow.status)) {
throw new ReactionError("invalid", `Order status (${order.workflow.status}) is not one of: ${orderStatusesThatOrdererCanMove.join(", ")}`);
}

// Find the two fulfillment groups we're modifying
const fromGroup = order.shipping.find((group) => group._id === fromFulfillmentGroupId);
if (!fromGroup) throw new ReactionError("not-found", "Order fulfillment group (from) not found");

const toGroup = order.shipping.find((group) => group._id === toFulfillmentGroupId);
if (!toGroup) throw new ReactionError("not-found", "Order fulfillment group (to) not found");

// Pull out the item's we're moving
const foundItemIds = [];
const movedItems = fromGroup.items.reduce((list, item) => {
if (itemIds.includes(item._id)) {
// The orderer may only move while the order item status is still "new"
if (accountIsOrderer && !itemStatusesThatOrdererCanMove.includes(item.workflow.status)) {
throw new ReactionError("invalid", `Item status (${item.workflow.status}) is not one of: ${itemStatusesThatOrdererCanMove.join(", ")}`);
}

list.push(item);
foundItemIds.push(item._id);
}
return list;
}, []);

if (!itemIds.every((id) => foundItemIds.includes(id))) {
throw new ReactionError("not-found", "Some order items not found");
}

const { billingAddress, cartId, currencyCode } = order;

// Find and move the items
const orderSurcharges = [];
const updatedGroups = await Promise.all(order.shipping.map(async (group) => {
if (group._id !== fromFulfillmentGroupId && group._id !== toFulfillmentGroupId) return group;

let updatedItems;
if (group._id === fromFulfillmentGroupId) {
// Remove the moved items
updatedItems = group.items.filter((item) => !itemIds.includes(item._id));
} else {
// Add the moved items
updatedItems = [...group.items, ...movedItems];
}

if (updatedItems.length === 0) {
throw new ReactionError("invalid-param", "move would result in group having no items");
}

// Create an updated group
const updatedGroup = {
...group,
// There is a convenience itemIds prop, so update that, too
itemIds: updatedItems.map((item) => item._id),
items: updatedItems,
totalItemQuantity: updatedItems.reduce((sum, item) => sum + item.quantity, 0)
};

// Update group shipping, tax, totals, etc.
const { groupSurcharges } = await updateGroupTotals(context, {
billingAddress,
cartId,
currencyCode,
discountTotal: updatedGroup.invoice.discounts,
group: updatedGroup,
orderId,
selectedFulfillmentMethodId: updatedGroup.shipmentMethod._id
});

// Push all group surcharges to overall order surcharge array.
// Currently, we do not save surcharges per group
orderSurcharges.push(...groupSurcharges);

// Ensure proper group status
updateGroupStatusFromItemStatus(updatedGroup);

return updatedGroup;
}));

// We're now ready to actually update the database and emit events
const modifier = {
$set: {
shipping: updatedGroups,
updatedAt: new Date()
}
};

OrderSchema.validate(modifier, { modifier: true });

const { modifiedCount, value: updatedOrder } = await Orders.findOneAndUpdate(
{ _id: orderId },
modifier,
{ returnOriginal: false }
);
if (modifiedCount === 0 || !updatedOrder) throw new ReactionError("server-error", "Unable to update order");

await appEvents.emit("afterOrderUpdate", {
order: updatedOrder,
updatedBy: userId
});

return { order: updatedOrder };
}
Loading