-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
Copy pathinventory.js
169 lines (155 loc) · 6.2 KB
/
inventory.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import { Meteor } from "meteor/meteor";
import { check, Match } from "meteor/check";
import { Catalog } from "/lib/api";
import { Inventory } from "/lib/collections";
import { Logger, Reaction } from "/server/api";
/**
* inventory/register
* @summary check a product and update Inventory collection with inventory documents.
* @param {Object} product - valid Schemas.Product object
* @return {Number} - returns the total amount of new inventory created
*/
export function registerInventory(product) {
// Retrieve schemas
// TODO: Permit product type registration and iterate through product types and schemas
const simpleProductSchema = Reaction.collectionSchema("Products", { type: "simple" });
const variantProductSchema = Reaction.collectionSchema("Products", { type: "variant" });
check(product, Match.OneOf(simpleProductSchema, variantProductSchema));
let type;
switch (product.type) {
case "variant":
check(product, variantProductSchema);
type = "variant";
break;
default:
check(product, simpleProductSchema);
type = "simple";
}
let totalNewInventory = 0;
const productId = type === "variant" ? product.ancestors[0] : product._id;
const variants = Catalog.getVariants(productId);
// we'll check each variant to see if it has been fully registered
for (const variant of variants) {
const inventory = Inventory.find({
productId,
variantId: variant._id,
shopId: product.shopId
});
// we'll return this as well
const inventoryVariantCount = inventory.count();
// if the variant exists already we're remove from the inventoryVariants
// so that we don't process it as an insert
if (inventoryVariantCount < variant.inventoryQuantity) {
const newQty = variant.inventoryQuantity || 0;
let i = inventoryVariantCount + 1;
Logger.debug(`inserting ${newQty - inventoryVariantCount} new inventory items for ${variant._id}`);
const batch = Inventory._collection.rawCollection().initializeUnorderedBulkOp();
while (i <= newQty) {
const id = Inventory._makeNewID();
batch.insert({
_id: id,
productId,
variantId: variant._id,
shopId: product.shopId,
createdAt: new Date(),
updatedAt: new Date(),
workflow: { // we add this line because `batchInsert` doesn't know
status: "new" // about SimpleSchema, so `defaultValue` will not
}
});
i++;
}
// took from: http://guide.meteor.com/collections.html#bulk-data-changes
const execute = Meteor.wrapAsync(batch.execute, batch);
const inventoryItem = execute();
const inserted = inventoryItem.nInserted;
if (!inserted) { // or maybe `inventory.length === 0`?
// throw new Meteor.Error("Inventory Anomaly Detected. Abort! Abort!");
return totalNewInventory;
}
Logger.debug(`registered ${inserted}`);
totalNewInventory += inserted;
}
}
// returns the total amount of new inventory created
return totalNewInventory;
}
function adjustInventory(product, userId, context) {
// TODO: This can fail even if updateVariant succeeds.
// Should probably look at making these two more atomic
const simpleProductSchema = Reaction.collectionSchema("Products", { type: "simple" });
const variantProductSchema = Reaction.collectionSchema("Products", { type: "variant" });
let type;
let results;
// adds or updates inventory collection with this product
switch (product.type) {
case "variant":
check(product, variantProductSchema);
type = "variant";
break;
default:
check(product, simpleProductSchema);
type = "simple";
}
// calledByServer is only true if this method was triggered by the server, such as from a webhook.
// there will be a null connection and no userId.
const calledByServer = (context && context.connection === null && !Meteor.userId());
// if this method is calledByServer, skip permission check.
// user needs createProduct permission to adjust inventory
// REVIEW: Should this be checking shop permission instead?
if (!calledByServer && !Reaction.hasPermission("createProduct", userId, product.shopId)) {
throw new Meteor.Error("access-denied", "Access Denied");
}
// Quantity and variants of this product's variant inventory
if (type === "variant") {
const variant = {
_id: product._id,
qty: product.inventoryQuantity || 0
};
const inventory = Inventory.find({
productId: product.ancestors[0],
variantId: product._id
});
const itemCount = inventory.count();
if (itemCount !== variant.qty) {
if (itemCount < variant.qty) {
// we need to register some new variants to inventory
results = itemCount + Meteor.call("inventory/register", product);
} else if (itemCount > variant.qty) {
// determine how many records to delete
const removeQty = itemCount - variant.qty;
// we're only going to delete records that are new
const removeInventory = Inventory.find({
"variantId": variant._id,
"workflow.status": "new"
}, {
sort: {
updatedAt: -1
},
limit: removeQty
}).fetch();
results = itemCount;
// delete latest inventory "status:new" records
for (const inventoryItem of removeInventory) {
results -= Meteor.call("inventory/remove", inventoryItem);
// we could add handling for the case when aren't enough "new" items
}
}
Logger.debug(`adjust variant ${variant._id} from ${itemCount} to ${results}`);
}
}
}
Meteor.methods({
"inventory/register"(product) {
if (!Reaction.hasPermission("createProduct", this.userId, product.shopId)) {
throw new Meteor.Error("access-denied", "Access Denied");
}
registerInventory(product);
},
"inventory/adjust"(product) { // TODO: this should be variant
const simpleProductSchema = Reaction.collectionSchema("Products", { type: "simple" });
const variantProductSchema = Reaction.collectionSchema("Products", { type: "variant" });
check(product, Match.OneOf(simpleProductSchema, variantProductSchema));
adjustInventory(product, this.userId, this);
}
});