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

Adjust inventory on shipment #1240

Merged
merged 16 commits into from
Aug 4, 2016
Merged
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
19 changes: 14 additions & 5 deletions imports/plugins/included/inventory/server/methods/inventory.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ Meteor.methods({
* @summary sets status from one status to a new status. Defaults to "new" to "reserved"
* @param {Array} cartItems array of objects of type Schemas.CartItems
* @param {String} status optional - sets the inventory workflow status, defaults to "reserved"
* @param {String} currentStatus
* @param {String} notFoundStatus
* @todo move this to bulkOp
* @return {undefined} returns undefined
* @return {Number} returns reservationCount
*/
"inventory/setStatus": function (cartItems, status, currentStatus, notFoundStatus) {
check(cartItems, [Schemas.CartItem]);
Expand All @@ -56,7 +58,8 @@ Meteor.methods({
const reservationStatus = status || "reserved"; // change status to options object
const defaultStatus = currentStatus || "new"; // default to the "new" status
const backorderStatus = notFoundStatus || "backorder"; // change status to options object
let reservationCount = 0;
let reservationCount;
Logger.info(`Moving Inventory items from ${defaultStatus} to ${reservationStatus}`);

// update inventory status for cartItems
for (let item of cartItems) {
Expand Down Expand Up @@ -105,9 +108,15 @@ Meteor.methods({
// if we have inventory available, only create additional required reservations
Logger.debug("existingReservationQty", existingReservationQty);
reservationCount = existingReservationQty;
let newReservedQty = totalRequiredQty - existingReservationQty + 1;
let i = 1;
let newReservedQty;
if (reservationStatus === "reserved" && defaultStatus === "new") {
newReservedQty = totalRequiredQty - existingReservationQty + 1;
} else {
// when moving from one "reserved" type status, we don't need to deal with existingReservationQty
newReservedQty = totalRequiredQty + 1;
}

let i = 1;
while (i < newReservedQty) {
// updated existing new inventory to be reserved
Logger.info(
Expand All @@ -119,7 +128,7 @@ Meteor.methods({
"productId": item.productId,
"variantId": item.variants._id,
"shopId": item.shopId,
"workflow.status": "new"
"workflow.status": defaultStatus
}, {
$set: {
"orderItemId": item._id,
Expand Down
21 changes: 15 additions & 6 deletions imports/plugins/included/inventory/server/methods/inventory2.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,20 @@ Meteor.methods({
*/
"inventory/shipped": function (cartItems) {
check(cartItems, [Schemas.CartItem]);
return Meteor.call("inventory/setStatus", cartItems, "shipped");
return Meteor.call("inventory/setStatus", cartItems, "shipped", "sold");
},
/**
* inventory/shipped
* inventory/sold
* mark inventory as sold
* @param {Array} cartItems array of objects Schemas.CartItem
* @return {undefined}
*/
"inventory/sold": function (cartItems) {
check(cartItems, [Schemas.CartItem]);
return Meteor.call("inventory/setStatus", cartItems, "sold", "reserved");
},
/**
* inventory/return
* mark inventory as returned
* @param {Array} cartItems array of objects Schemas.CartItem
* @return {undefined}
Expand All @@ -204,14 +214,13 @@ Meteor.methods({
return Meteor.call("inventory/setStatus", cartItems, "return");
},
/**
* inventory/shipped
* inventory/returnToStock
* mark inventory as return and available for sale
* @param {Array} cartItems array of objects Schemas.CartItem
* @return {undefined}
*/
"inventory/returnToStock": function (productId, variantId) {
check(productId, String);
check(variantId, String);
"inventory/returnToStock": function (cartItems) {
check(cartItems, [Schemas.CartItem]);
return Meteor.call("inventory/clearStatus", cartItems, "new", "return");
}
});
51 changes: 50 additions & 1 deletion imports/plugins/included/inventory/server/startup/hooks.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Cart, Products } from "/lib/collections";
import { Cart, Products, Orders } from "/lib/collections";
import { Logger } from "/server/api";

/**
Expand Down Expand Up @@ -89,3 +89,52 @@ Products.after.insert((userId, doc) => {
}
Meteor.call("inventory/register", doc);
});

function markInventoryShipped(doc) {
Copy link
Contributor

Choose a reason for hiding this comment

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

these functions could be exported and then tested independent of the hooks

const order = Orders.findOne(doc._id);
const orderItems = order.items;
let cartItems = [];
for (let orderItem of orderItems) {
let cartItem = {
_id: orderItem.cartItemId,
shopId: orderItem.shopId,
quantity: orderItem.quantity,
productId: orderItem.productId,
variants: orderItem.variants,
title: orderItem.title
};
cartItems.push(cartItem);
}
Meteor.call("inventory/shipped", cartItems);
}

function markInventorySold(doc) {
const orderItems = doc.items;
let cartItems = [];
for (let orderItem of orderItems) {
let cartItem = {
_id: orderItem.cartItemId,
shopId: orderItem.shopId,
quantity: orderItem.quantity,
productId: orderItem.productId,
variants: orderItem.variants,
title: orderItem.title
};
cartItems.push(cartItem);
}
Meteor.call("inventory/sold", cartItems);
}

Orders.after.insert((userId, doc) => {
Logger.debug("Inventory module handling Order insert");
markInventorySold(doc);
});

Orders.after.update((userId, doc, fieldnames, modifier) => {
Logger.debug("Inventory module handling Order update");
if (modifier.$addToSet) {
if (modifier.$addToSet["workflow.workflow"] === "coreOrderWorkflow/completed") {
markInventoryShipped(doc);
}
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/* eslint dot-notation: 0 */
import { Meteor } from "meteor/meteor";
import { Inventory, Orders } from "/lib/collections";
import { Reaction } from "/server/api";
import { expect } from "meteor/practicalmeteor:chai";
import { sinon } from "meteor/practicalmeteor:sinon";
import Fixtures from "/server/imports/fixtures";
import { getShop } from "/server/imports/fixtures/shops";

Fixtures();

describe("Inventory Hooks", function () {
let originals;
let sandbox;

before(function () {
originals = {
mergeCart: Meteor.server.method_handlers["cart/mergeCart"],
createCart: Meteor.server.method_handlers["cart/createCart"],
copyCartToOrder: Meteor.server.method_handlers["cart/copyCartToOrder"],
addToCart: Meteor.server.method_handlers["cart/addToCart"],
setShipmentAddress: Meteor.server.method_handlers["cart/setShipmentAddress"],
setPaymentAddress: Meteor.server.method_handlers["cart/setPaymentAddress"]
};
});

beforeEach(function () {
sandbox = sinon.sandbox.create();
});

afterEach(function () {
sandbox.restore();
});

function spyOnMethod(method, id) {
return sandbox.stub(Meteor.server.method_handlers, `cart/${method}`, function () {
check(arguments, [Match.Any]); // to prevent audit_arguments from complaining
this.userId = id;
return originals[method].apply(this, arguments);
});
}

it("should move allocated inventory to 'sold' when an order is created", function () {
this.timeout(50000);
Inventory.direct.remove({});
const cart = Factory.create("cartToOrder");
sandbox.stub(Reaction, "getShopId", function () {
return cart.shopId;
});
sandbox.stub(Meteor.server.method_handlers, "orders/sendNotification", function () {
check(arguments, [Match.Any]);
});
let shop = getShop();
let product = cart.items[0];
const inventoryItem = Inventory.insert({
productId: product.productId,
variantId: product.variants._id,
shopId: shop._id,
workflow: {
status: "reserved"
},
orderItemId: product._id
});
expect(inventoryItem).to.not.be.undefined;
Inventory.update(inventoryItem._id,
{
$set: {
"workflow.status": "reserved",
"orderItemId": product._id
}
});
spyOnMethod("copyCartToOrder", cart.userId);
Meteor.call("cart/copyCartToOrder", cart._id);
let updatedInventoryItem = Inventory.findOne({
productId: product.productId,
variantId: product.variants._id,
shopId: shop._id,
orderItemId: product._id
});
expect(updatedInventoryItem.workflow.status).to.equal("sold");
});

it("should move allocated inventory to 'shipped' when an order is shipped", function () {
this.timeout(50000);
Inventory.direct.remove({});
const cart = Factory.create("cartToOrder");
sandbox.stub(Reaction, "getShopId", function () {
return cart.shopId;
});
sandbox.stub(Reaction, "hasPermission", () => true);
sandbox.stub(Meteor.server.method_handlers, "orders/sendNotification", function () {
check(arguments, [Match.Any]);
});
let shop = getShop();
let product = cart.items[0];
const inventoryItem = Inventory.insert({
productId: product.productId,
variantId: product.variants._id,
shopId: shop._id,
workflow: {
status: "reserved"
},
orderItemId: product._id
});
expect(inventoryItem).to.not.be.undefined;
Inventory.update(inventoryItem._id,
{
$set: {
"workflow.status": "reserved",
"orderItemId": product._id
}
});
spyOnMethod("copyCartToOrder", cart.userId);
const orderId = Meteor.call("cart/copyCartToOrder", cart._id);
const order = Orders.findOne(orderId);
const shipping = { items: [] };
Meteor.call("orders/shipmentShipped", order, shipping);
const shippedInventoryItem = Inventory.findOne(inventoryItem._id);
expect(shippedInventoryItem.workflow.status).to.equal("shipped");
});
});
4 changes: 4 additions & 0 deletions lib/collections/schemas/cart.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export const CartItem = new SimpleSchema({
label: "Product Type",
type: String,
optional: true
},
cartItemId: { // Seems strange here but has to be here since we share schemas between cart and order
type: String,
optional: true
}
});

Expand Down
3 changes: 3 additions & 0 deletions server/imports/fixtures/fixtures.app-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ describe("Fixtures:", function () {
sandbox.stub(Meteor.server.method_handlers, "inventory/register", function () {
check(arguments, [Match.Any]);
});
sandbox.stub(Meteor.server.method_handlers, "inventory/sold", function () {
check(arguments, [Match.Any]);
});
const order = Factory.create("order");
expect(order).to.not.be.undefined;
const orderCount = Collections.Orders.find().count();
Expand Down
1 change: 1 addition & 0 deletions server/methods/core/cart.js
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ Meteor.methods({
itemClone.quantity = 1;

itemClone._id = Random.id();
itemClone.cartItemId = item._id; // used for transitioning inventry
itemClone.workflow = {
status: "new"
};
Expand Down
1 change: 1 addition & 0 deletions server/methods/core/orders.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import accounting from "accounting-js";
import Future from "fibers/future";
import { Meteor } from "meteor/meteor";
import { check } from "meteor/check";
import { Email } from "meteor/email";
import { Cart, Orders, Products, Shops } from "/lib/collections";
import * as Schemas from "/lib/collections/schemas";
import { Logger, Reaction } from "/server/api";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ describe("Order Publication", function () {
sandbox.stub(Meteor.server.method_handlers, "inventory/register", function () {
check(arguments, [Match.Any]);
});
sandbox.stub(Meteor.server.method_handlers, "inventory/sold", function () {
check(arguments, [Match.Any]);
});
sandbox.stub(Reaction, "getShopId", () => shop._id);
sandbox.stub(Roles, "userIsInRole", () => true);
order = Factory.create("order", { status: "created" });
Expand All @@ -73,6 +76,9 @@ describe("Order Publication", function () {
sandbox.stub(Meteor.server.method_handlers, "inventory/register", function () {
check(arguments, [Match.Any]);
});
sandbox.stub(Meteor.server.method_handlers, "inventory/sold", function () {
check(arguments, [Match.Any]);
});
sandbox.stub(Reaction, "getShopId", () => shop._id);
sandbox.stub(Roles, "userIsInRole", () => false);
order = Factory.create("order", { status: "created" });
Expand Down