-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5018 from reactioncommerce/feat-aldeed-move-order…
…-items-mutation Add moveOrderItems mutation
- Loading branch information
Showing
17 changed files
with
1,098 additions
and
36 deletions.
There are no files selected for viewing
29 changes: 29 additions & 0 deletions
29
.../plugins/core/orders/server/no-meteor/mutations/__snapshots__/moveOrderItems.test.js.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"`; |
2 changes: 2 additions & 0 deletions
2
imports/plugins/core/orders/server/no-meteor/mutations/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
imports/plugins/core/orders/server/no-meteor/mutations/moveOrderItems.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
} |
Oops, something went wrong.