feat: In band inventory updates (#311)

Co-authored-by: olivermrbl <oliver@mrbltech.com>
This commit is contained in:
Kasper Fabricius Kristensen
2021-08-05 12:21:15 +02:00
committed by GitHub
co-authored by olivermrbl
parent 44fce520aa
commit f07cc0fa40
32 changed files with 1870 additions and 268 deletions
+295 -2
View File
@@ -1,6 +1,11 @@
const { dropDatabase } = require("pg-god"); const { dropDatabase } = require("pg-god");
const path = require("path"); const path = require("path");
const { ReturnReason } = require("@medusajs/medusa"); const {
ReturnReason,
Order,
LineItem,
ProductVariant,
} = require("@medusajs/medusa");
const setupServer = require("../../../helpers/setup-server"); const setupServer = require("../../../helpers/setup-server");
const { useApi } = require("../../../helpers/use-api"); const { useApi } = require("../../../helpers/use-api");
@@ -83,6 +88,175 @@ describe("/admin/orders", () => {
}); });
}); });
describe("GET /admin/orders", () => {
beforeEach(async () => {
try {
await adminSeeder(dbConnection);
await orderSeeder(dbConnection);
} catch (err) {
console.log(err);
throw err;
}
const manager = dbConnection.manager;
const order2 = manager.create(Order, {
id: "test-order-not-payed",
customer_id: "test-customer",
email: "test@email.com",
fulfillment_status: "not_fulfilled",
payment_status: "awaiting",
billing_address: {
id: "test-billing-address",
first_name: "lebron",
},
shipping_address: {
id: "test-shipping-address",
first_name: "lebron",
country_code: "us",
},
region_id: "test-region",
currency_code: "usd",
tax_rate: 0,
discounts: [
{
id: "test-discount",
code: "TEST134",
is_dynamic: false,
rule: {
id: "test-rule",
description: "Test Discount",
type: "percentage",
value: 10,
allocation: "total",
},
is_disabled: false,
regions: [
{
id: "test-region",
},
],
},
],
payments: [
{
id: "test-payment",
amount: 10000,
currency_code: "usd",
amount_refunded: 0,
provider_id: "test-pay",
data: {},
},
],
items: [],
});
await manager.save(order2);
const li2 = manager.create(LineItem, {
id: "test-item",
fulfilled_quantity: 0,
returned_quantity: 0,
title: "Line Item",
description: "Line Item Desc",
thumbnail: "https://test.js/1234",
unit_price: 8000,
quantity: 1,
variant_id: "test-variant",
order_id: "test-order-not-payed",
});
await manager.save(li2);
});
afterEach(async () => {
const manager = dbConnection.manager;
await manager.query(`DELETE FROM "cart"`);
await manager.query(`DELETE FROM "fulfillment"`);
await manager.query(`DELETE FROM "swap"`);
await manager.query(`DELETE FROM "return"`);
await manager.query(`DELETE FROM "claim_image"`);
await manager.query(`DELETE FROM "claim_tag"`);
await manager.query(`DELETE FROM "claim_item"`);
await manager.query(`DELETE FROM "claim_order"`);
await manager.query(`DELETE FROM "line_item"`);
await manager.query(`DELETE FROM "money_amount"`);
await manager.query(`DELETE FROM "product_variant"`);
await manager.query(`DELETE FROM "product"`);
await manager.query(`DELETE FROM "shipping_method"`);
await manager.query(`DELETE FROM "shipping_option"`);
await manager.query(`DELETE FROM "discount"`);
await manager.query(`DELETE FROM "payment"`);
await manager.query(`DELETE FROM "order"`);
await manager.query(`DELETE FROM "customer"`);
await manager.query(
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'`
);
await manager.query(`DELETE FROM "region"`);
await manager.query(`DELETE FROM "user"`);
});
it("cancels an order and increments inventory_quantity", async () => {
const api = useApi();
const manager = dbConnection.manager;
const initialInventoryRes = await api.get("/store/variants/test-variant");
expect(initialInventoryRes.data.variant.inventory_quantity).toEqual(1);
const response = await api
.post(
`/admin/orders/test-order-not-payed/cancel`,
{},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err);
});
expect(response.status).toEqual(200);
const secondInventoryRes = await api.get("/store/variants/test-variant");
expect(secondInventoryRes.data.variant.inventory_quantity).toEqual(2);
});
it("cancels an order but does not increment inventory_quantity of unmanaged variant", async () => {
const api = useApi();
const manager = dbConnection.manager;
await manager.query(
`UPDATE "product_variant" SET manage_inventory=false WHERE id = 'test-variant'`
);
const initialInventoryRes = await api.get("/store/variants/test-variant");
expect(initialInventoryRes.data.variant.inventory_quantity).toEqual(1);
const response = await api
.post(
`/admin/orders/test-order-not-payed/cancel`,
{},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err);
});
expect(response.status).toEqual(200);
const secondInventoryRes = await api.get("/store/variants/test-variant");
expect(secondInventoryRes.data.variant.inventory_quantity).toEqual(1);
});
});
describe("POST /admin/orders/:id/claims", () => { describe("POST /admin/orders/:id/claims", () => {
beforeEach(async () => { beforeEach(async () => {
try { try {
@@ -154,6 +328,18 @@ describe("/admin/orders", () => {
); );
expect(response.status).toEqual(200); expect(response.status).toEqual(200);
const variant = await api.get("/admin/products", {
headers: {
authorization: "Bearer test_token",
},
});
// find test variant and verify that its inventory quantity has changed
const toTest = variant.data.products[0].variants.find(
(v) => v.id === "test-variant"
);
expect(toTest.inventory_quantity).toEqual(0);
expect(response.data.order.claims[0].shipping_address_id).toEqual( expect(response.data.order.claims[0].shipping_address_id).toEqual(
"test-shipping-address" "test-shipping-address"
); );
@@ -620,6 +806,43 @@ describe("/admin/orders", () => {
}), }),
]); ]);
}); });
it("fails to creates a claim due to no stock on additional items", async () => {
const api = useApi();
try {
await api.post(
"/admin/orders/test-order/claims",
{
type: "replace",
claim_items: [
{
item_id: "test-item",
quantity: 1,
reason: "production_failure",
tags: ["fluff"],
images: ["https://test.image.com"],
},
],
additional_items: [
{
variant_id: "test-variant",
quantity: 2,
},
],
},
{
headers: {
authorization: "Bearer test_token",
},
}
);
} catch (e) {
expect(e.response.status).toEqual(400);
expect(e.response.data.message).toEqual(
"Variant with id: test-variant does not have the required inventory"
);
}
});
}); });
describe("POST /admin/orders/:id/return", () => { describe("POST /admin/orders/:id/return", () => {
@@ -705,6 +928,71 @@ describe("/admin/orders", () => {
}), }),
]); ]);
}); });
it("increases inventory_quantity when return is received", async () => {
const api = useApi();
const returned = await api.post(
"/admin/orders/test-order/return",
{
items: [
{
item_id: "test-item",
quantity: 1,
},
],
receive_now: true,
},
{
headers: {
authorization: "Bearer test_token",
},
}
);
//Find variant that should have its inventory_quantity updated
const toTest = returned.data.order.items.find(
(i) => i.id === "test-item"
);
expect(returned.status).toEqual(200);
expect(toTest.variant.inventory_quantity).toEqual(2);
});
it("does not increases inventory_quantity when return is received when inventory is not managed", async () => {
const api = useApi();
const manager = dbConnection.manager;
await manager.query(
`UPDATE "product_variant" SET manage_inventory=false WHERE id = 'test-variant'`
);
const returned = await api.post(
"/admin/orders/test-order/return",
{
items: [
{
item_id: "test-item",
quantity: 1,
},
],
receive_now: true,
},
{
headers: {
authorization: "Bearer test_token",
},
}
);
//Find variant that should have its inventory_quantity updated
const toTest = returned.data.order.items.find(
(i) => i.id === "test-item"
);
expect(returned.status).toEqual(200);
expect(toTest.variant.inventory_quantity).toEqual(1);
});
}); });
describe("GET /admin/orders", () => { describe("GET /admin/orders", () => {
@@ -967,8 +1255,13 @@ describe("/admin/orders", () => {
} }
); );
// find item to test returned quantiy for
const toTest = returnedOrderSecond.data.order.items.find(
(i) => i.id === "test-item-many"
);
expect(returnedOrderSecond.status).toEqual(200); expect(returnedOrderSecond.status).toEqual(200);
expect(returnedOrderSecond.data.order.items[1].returned_quantity).toBe(3); expect(toTest.returned_quantity).toBe(3);
}); });
it("creates a swap and receives the items", async () => { it("creates a swap and receives the items", async () => {
@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`/store/carts POST /store/carts/:id fails to complete cart with items inventory not/partially covered 1`] = `
Object {
"code": "insufficient_inventory",
"message": "Variant with id: test-variant-2 does not have the required inventory",
"type": "not_allowed",
}
`;
exports[`/store/carts POST /store/carts/:id returns early, if cart is already completed 1`] = `
Object {
"code": "cart_incompatible_state",
"message": "Cart has already been completed",
"type": "not_allowed",
}
`;
+70 -5
View File
@@ -1,6 +1,6 @@
const { dropDatabase } = require("pg-god"); const { dropDatabase } = require("pg-god");
const path = require("path"); const path = require("path");
const { Region } = require("@medusajs/medusa"); const { Region, LineItem, Payment } = require("@medusajs/medusa");
const setupServer = require("../../../helpers/setup-server"); const setupServer = require("../../../helpers/setup-server");
const { useApi } = require("../../../helpers/use-api"); const { useApi } = require("../../../helpers/use-api");
@@ -15,15 +15,21 @@ describe("/store/carts", () => {
let dbConnection; let dbConnection;
const doAfterEach = async (manager) => { const doAfterEach = async (manager) => {
await manager.query(`DELETE FROM "line_item"`);
await manager.query(`DELETE FROM "money_amount"`);
await manager.query(`DELETE FROM "discount"`); await manager.query(`DELETE FROM "discount"`);
await manager.query(`DELETE FROM "discount_rule"`); await manager.query(`DELETE FROM "discount_rule"`);
await manager.query(`DELETE FROM "shipping_method"`);
await manager.query(`DELETE FROM "line_item"`);
await manager.query(`DELETE FROM "cart"`);
await manager.query(`DELETE FROM "money_amount"`);
await manager.query(`DELETE FROM "product_variant"`); await manager.query(`DELETE FROM "product_variant"`);
await manager.query(`DELETE FROM "product"`); await manager.query(`DELETE FROM "product"`);
await manager.query(`DELETE FROM "shipping_method"`);
await manager.query(`DELETE FROM "shipping_option"`); await manager.query(`DELETE FROM "shipping_option"`);
await manager.query(`DELETE FROM "payment_provider"`);
await manager.query(`DELETE FROM "payment_session"`);
await manager.query(`UPDATE "payment" SET order_id=NULL`);
await manager.query(`DELETE FROM "order"`);
await manager.query(`UPDATE "payment" SET cart_id=NULL`);
await manager.query(`DELETE FROM "cart"`);
await manager.query(`DELETE FROM "payment"`);
await manager.query(`DELETE FROM "address"`); await manager.query(`DELETE FROM "address"`);
await manager.query(`DELETE FROM "customer"`); await manager.query(`DELETE FROM "customer"`);
await manager.query( await manager.query(
@@ -208,6 +214,65 @@ describe("/store/carts", () => {
expect(cart.data.cart.shipping_total).toBe(1000); expect(cart.data.cart.shipping_total).toBe(1000);
expect(cart.status).toEqual(200); expect(cart.status).toEqual(200);
}); });
it("complete cart with items inventory covered", async () => {
const api = useApi();
const getRes = await api.post(`/store/carts/test-cart-2/complete-cart`);
expect(getRes.status).toEqual(200);
const variantRes = await api.get("/store/variants/test-variant");
expect(variantRes.data.variant.inventory_quantity).toEqual(0);
});
it("returns early, if cart is already completed", async () => {
const manager = dbConnection.manager;
const api = useApi();
await manager.query(
`UPDATE "cart" SET completed_at=current_timestamp WHERE id = 'test-cart-2'`
);
try {
await api.post(`/store/carts/test-cart-2/complete-cart`);
} catch (error) {
expect(error.response.data).toMatchSnapshot({
code: "not_allowed",
message: "Cart has already been completed",
code: "cart_incompatible_state",
});
expect(error.response.status).toEqual(409);
}
});
it("fails to complete cart with items inventory not/partially covered", async () => {
const manager = dbConnection.manager;
const li = manager.create(LineItem, {
id: "test-item",
title: "Line Item",
description: "Line Item Desc",
thumbnail: "https://test.js/1234",
unit_price: 8000,
quantity: 99,
variant_id: "test-variant-2",
cart_id: "test-cart-2",
});
await manager.save(li);
const api = useApi();
try {
await api.post(`/store/carts/test-cart-2/complete-cart`);
} catch (e) {
expect(e.response.data).toMatchSnapshot({
code: "insufficient_inventory",
});
expect(e.response.status).toBe(409);
}
//check to see if payment has been cancelled
const res = await api.get(`/store/carts/test-cart-2`);
expect(res.data.cart.payment.canceled_at).not.toBe(null);
});
}); });
describe("POST /store/carts/:id/shipping-methods", () => { describe("POST /store/carts/:id/shipping-methods", () => {
@@ -9,12 +9,19 @@ const {
ProductVariant, ProductVariant,
MoneyAmount, MoneyAmount,
LineItem, LineItem,
Payment,
Cart,
ShippingMethod,
Swap,
} = require("@medusajs/medusa"); } = require("@medusajs/medusa");
const setupServer = require("../../../helpers/setup-server"); const setupServer = require("../../../helpers/setup-server");
const { useApi } = require("../../../helpers/use-api"); const { useApi } = require("../../../helpers/use-api");
const { initDb } = require("../../../helpers/use-db"); const { initDb } = require("../../../helpers/use-db");
const swapSeeder = require("../../helpers/swap-seeder");
const cartSeeder = require("../../helpers/cart-seeder");
jest.setTimeout(30000); jest.setTimeout(30000);
describe("/store/carts", () => { describe("/store/carts", () => {
@@ -34,6 +41,92 @@ describe("/store/carts", () => {
medusaProcess.kill(); medusaProcess.kill();
}); });
describe("/store/swaps", () => {
beforeEach(async () => {
try {
await cartSeeder(dbConnection);
await swapSeeder(dbConnection);
const manager = dbConnection.manager;
await manager.query(
`UPDATE "swap" SET cart_id='test-cart-2' WHERE id = 'test-swap'`
);
await manager.query(
`UPDATE "payment" SET swap_id=NULL WHERE id = 'test-payment-swap'`
);
} catch (err) {
console.log(err);
throw err;
}
});
afterEach(async () => {
const manager = dbConnection.manager;
await manager.query(
`UPDATE "swap" SET cart_id=NULL WHERE id = 'test-swap'`
);
await manager.query(`DELETE FROM "payment_session"`);
await manager.query(`DELETE FROM "shipping_method"`);
await manager.query(`DELETE FROM "return_item"`);
await manager.query(`DELETE FROM "line_item"`);
await manager.query(`DELETE FROM "cart"`);
await manager.query(`DELETE FROM "payment"`);
await manager.query(`DELETE FROM "return"`);
await manager.query(`DELETE FROM "swap"`);
await manager.query(`DELETE FROM "fulfillment_item"`);
await manager.query(`DELETE FROM "fulfillment"`);
await manager.query(`DELETE FROM "shipping_method"`);
await manager.query(`DELETE FROM "money_amount"`);
await manager.query(`DELETE FROM "product_variant"`);
await manager.query(`DELETE FROM "product"`);
await manager.query(`DELETE FROM "shipping_option"`);
await manager.query(`DELETE FROM "order"`);
await manager.query(`DELETE FROM "address"`);
await manager.query(`DELETE FROM "customer"`);
await manager.query(
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'`
);
await manager.query(`DELETE FROM "region"`);
});
it("creates a swap from a cart id", async () => {
const api = useApi();
const getRes = await api.post("/store/swaps", {
cart_id: "test-cart-2",
});
expect(getRes.status).toEqual(200);
});
it("fails due to partial inventory", async () => {
const api = useApi();
const manager = dbConnection.manager;
const li = manager.create(LineItem, {
id: "test-item-with-no-stock",
title: "No Stock Item",
description: "Line Item Desc",
thumbnail: "https://test.js/1234",
unit_price: 8000,
quantity: 1,
variant_id: "test-variant-2",
cart_id: "test-cart-2",
});
await manager.save(li);
try {
await api.post("/store/swaps", {
cart_id: "test-cart-2",
});
} catch (e) {
expect(e.response.data.message).toEqual(
"Variant with id: test-variant-2 does not have the required inventory"
);
}
});
});
describe("GET /store/orders", () => { describe("GET /store/orders", () => {
beforeEach(async () => { beforeEach(async () => {
const manager = dbConnection.manager; const manager = dbConnection.manager;
+79 -2
View File
@@ -8,9 +8,12 @@ const {
ShippingOption, ShippingOption,
ShippingMethod, ShippingMethod,
Address, Address,
ProductVariant,
Product, Product,
ProductVariant,
MoneyAmount, MoneyAmount,
LineItem,
Payment,
PaymentSession,
} = require("@medusajs/medusa"); } = require("@medusajs/medusa");
module.exports = async (connection, data = {}) => { module.exports = async (connection, data = {}) => {
@@ -189,19 +192,42 @@ module.exports = async (connection, data = {}) => {
], ],
}); });
await manager.insert(ProductVariant, {
id: "test-variant-2",
title: "test variant 2",
product_id: "test-product",
inventory_quantity: 0,
options: [
{
option_id: "test-option",
value: "Size",
},
],
});
const ma = manager.create(MoneyAmount, { const ma = manager.create(MoneyAmount, {
variant_id: "test-variant", variant_id: "test-variant",
currency_code: "usd", currency_code: "usd",
amount: 1000, amount: 1000,
}); });
await manager.save(ma); await manager.save(ma);
const ma2 = manager.create(MoneyAmount, { const ma2 = manager.create(MoneyAmount, {
variant_id: "test-variant-2",
currency_code: "usd",
amount: 8000,
});
await manager.save(ma2);
const ma3 = manager.create(MoneyAmount, {
variant_id: "giftcard-denom", variant_id: "giftcard-denom",
currency_code: "usd", currency_code: "usd",
amount: 1000, amount: 1000,
}); });
await manager.save(ma2);
await manager.save(ma3);
const cart = manager.create(Cart, { const cart = manager.create(Cart, {
id: "test-cart", id: "test-cart",
@@ -219,6 +245,45 @@ module.exports = async (connection, data = {}) => {
await manager.save(cart); await manager.save(cart);
const cart2 = manager.create(Cart, {
id: "test-cart-2",
customer_id: "some-customer",
email: "some-customer@email.com",
shipping_address: {
id: "test-shipping-address",
first_name: "lebron",
country_code: "us",
},
region_id: "test-region",
currency_code: "usd",
completed_at: null,
items: [],
});
const pay = manager.create(Payment, {
id: "test-payment",
amount: 10000,
currency_code: "usd",
amount_refunded: 0,
provider_id: "test-pay",
data: {},
});
await manager.save(pay);
cart2.payment = pay;
await manager.save(cart2);
await manager.insert(PaymentSession, {
id: "test-session",
cart_id: "test-cart-2",
provider_id: "test-pay",
is_selected: true,
data: {},
status: "authorized",
});
await manager.insert(ShippingMethod, { await manager.insert(ShippingMethod, {
id: "test-method", id: "test-method",
shipping_option_id: "test-option", shipping_option_id: "test-option",
@@ -226,4 +291,16 @@ module.exports = async (connection, data = {}) => {
price: 1000, price: 1000,
data: {}, data: {},
}); });
const li = manager.create(LineItem, {
id: "test-item",
title: "Line Item",
description: "Line Item Desc",
thumbnail: "https://test.js/1234",
unit_price: 8000,
quantity: 1,
variant_id: "test-variant",
cart_id: "test-cart-2",
});
await manager.save(li);
}; };
+3 -3
View File
@@ -4,11 +4,11 @@
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"test": "jest --runInBand", "test": "jest --runInBand --silent=false",
"build": "babel src -d dist --extensions \".ts,.js\"" "build": "babel src -d dist --extensions \".ts,.js\""
}, },
"dependencies": { "dependencies": {
"@medusajs/medusa": "1.1.33-dev-1627995051381", "@medusajs/medusa": "1.1.33-dev-1628079986095",
"medusa-interfaces": "^1.1.18", "medusa-interfaces": "^1.1.18",
"typeorm": "^0.2.31" "typeorm": "^0.2.31"
}, },
@@ -19,4 +19,4 @@
"babel-preset-medusa-package": "^1.1.11", "babel-preset-medusa-package": "^1.1.11",
"jest": "^26.6.3" "jest": "^26.6.3"
} }
} }
+4 -4
View File
@@ -1265,10 +1265,10 @@
winston "^3.3.3" winston "^3.3.3"
yargs "^15.3.1" yargs "^15.3.1"
"@medusajs/medusa@1.1.33-dev-1627995051381": "@medusajs/medusa@1.1.33-dev-1628079986095":
version "1.1.33-dev-1627995051381" version "1.1.33-dev-1628079986095"
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.1.33-dev-1627995051381.tgz#def214374c31daca1f37c2dd5ee55f119236555f" resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.1.33-dev-1628079986095.tgz#0e8aa3bd83174366d1266823f093f21968b0f6e4"
integrity sha512-yeH/YscYfWn4jYF+YSPyexMgAaDOfjvKm2xH95C+T9w4Ct08z06uBw0G0klIKPatNTirfD7HVgvg2vWb+ChDWA== integrity sha512-zqvz8+NL5+3Ba5uRS6Z2SaXpRTF2r6B+o0HGY8TjkAmvf2pMKyfIOGpBLtnhezDpDb9mkLdiHqrV23lNE+5pAw==
dependencies: dependencies:
"@hapi/joi" "^16.1.8" "@hapi/joi" "^16.1.8"
"@medusajs/medusa-cli" "^1.1.14" "@medusajs/medusa-cli" "^1.1.14"
+1
View File
@@ -36,6 +36,7 @@
"test:fixtures": "jest --config=docs-util/jest.config.js --runInBand" "test:fixtures": "jest --config=docs-util/jest.config.js --runInBand"
}, },
"dependencies": { "dependencies": {
"global": "^4.4.0",
"import-from": "^3.0.0", "import-from": "^3.0.0",
"oas-normalize": "^2.3.1", "oas-normalize": "^2.3.1",
"swagger-inline": "^3.2.2" "swagger-inline": "^3.2.2"
+10 -2
View File
@@ -12,6 +12,11 @@ export const MedusaErrorTypes = {
NOT_ALLOWED: "not_allowed", NOT_ALLOWED: "not_allowed",
} }
export const MedusaErrorCodes = {
INSUFFICIENT_INVENTORY: "insufficient_inventory",
CART_INCOMPATIBLE_STATE: "cart_incompatible_state",
}
/** /**
* Standardized error to be used across Medusa project. * Standardized error to be used across Medusa project.
* @extends Error * @extends Error
@@ -22,19 +27,22 @@ class MedusaError extends Error {
* @param type {MedusaErrorType} - the type of error. * @param type {MedusaErrorType} - the type of error.
* @param params {Array} - Error params. * @param params {Array} - Error params.
*/ */
constructor(name, message, ...params) { constructor(type, message, code, ...params) {
super(...params) super(...params)
if (Error.captureStackTrace) { if (Error.captureStackTrace) {
Error.captureStackTrace(this, MedusaError) Error.captureStackTrace(this, MedusaError)
} }
this.name = name this.type = type
this.name = type
this.code = code
this.message = message this.message = message
this.date = new Date() this.date = new Date()
} }
} }
MedusaError.Types = MedusaErrorTypes MedusaError.Types = MedusaErrorTypes
MedusaError.Codes = MedusaErrorCodes
export default MedusaError export default MedusaError
@@ -104,8 +104,8 @@ describe("POST /store/carts/:id", () => {
) )
}) })
it("Call CartService retrieve 0 times", () => { it("Call CartService retrieve 1 time", () => {
expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(0) expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1)
}) })
it("returns 200", () => { it("returns 200", () => {
@@ -71,7 +71,20 @@ export default async (req, res) => {
const { key, error } = await idempotencyKeyService.workStage( const { key, error } = await idempotencyKeyService.workStage(
idempotencyKey.idempotency_key, idempotencyKey.idempotency_key,
async manager => { async manager => {
const cart = await cartService let cart = await cartService.withTransaction(manager).retrieve(id)
if (cart.completed_at) {
return {
response_code: 409,
response_body: {
code: MedusaError.Codes.CART_INCOMPATIBLE_STATE,
message: "Cart has already been completed",
type: MedusaError.Types.NOT_ALLOWED,
},
}
}
cart = await cartService
.withTransaction(manager) .withTransaction(manager)
.authorizePayment(id, { .authorizePayment(id, {
...req.request_context, ...req.request_context,
@@ -85,7 +98,11 @@ export default async (req, res) => {
) { ) {
return { return {
response_code: 200, response_code: 200,
response_body: { data: cart }, response_body: {
data: cart,
payment_status: cart.payment_session.status,
type: "cart",
},
} }
} }
} }
@@ -122,17 +139,17 @@ export default async (req, res) => {
switch (cart.type) { switch (cart.type) {
case "swap": { case "swap": {
const swapId = cart.metadata?.swap_id const swapId = cart.metadata?.swap_id
order = await swapService let swap = await swapService
.withTransaction(manager) .withTransaction(manager)
.registerCartCompletion(swapId) .registerCartCompletion(swapId)
order = await swapService swap = await swapService
.withTransaction(manager) .withTransaction(manager)
.retrieve(order.id, { relations: ["shipping_address"] }) .retrieve(swap.id, { relations: ["shipping_address"] })
return { return {
response_code: 200, response_code: 200,
response_body: { data: order }, response_body: { data: swap, type: "swap" },
} }
} }
// case "payment_link": // case "payment_link":
@@ -168,7 +185,19 @@ export default async (req, res) => {
return { return {
response_code: 200, response_code: 200,
response_body: { data: order }, response_body: { data: order, type: "order" },
}
} else if (
error &&
error.code === MedusaError.Codes.INSUFFICIENT_INVENTORY
) {
return {
response_code: 409,
response_body: {
message: error.message,
type: error.type,
code: error.code,
},
} }
} else { } else {
throw error throw error
@@ -192,7 +221,7 @@ export default async (req, res) => {
return { return {
response_code: 200, response_code: 200,
response_body: { data: order }, response_body: { data: order, type: "order" },
} }
} }
) )
@@ -230,7 +259,6 @@ export default async (req, res) => {
res.status(idempotencyKey.response_code).json(idempotencyKey.response_body) res.status(idempotencyKey.response_code).json(idempotencyKey.response_body)
} catch (error) { } catch (error) {
console.log(error)
throw error throw error
} }
} }
@@ -260,6 +260,9 @@ export const CartServiceMock = {
if (cartId === IdMap.getId("cartWithPaySessions")) { if (cartId === IdMap.getId("cartWithPaySessions")) {
return Promise.resolve(carts.cartWithPaySessions) return Promise.resolve(carts.cartWithPaySessions)
} }
if (cartId === IdMap.getId("test-cart2")) {
return Promise.resolve(carts.testCart)
}
throw new MedusaError(MedusaError.Types.NOT_FOUND, "cart not found") throw new MedusaError(MedusaError.Types.NOT_FOUND, "cart not found")
}), }),
addLineItem: jest.fn().mockImplementation((cartId, lineItem) => { addLineItem: jest.fn().mockImplementation((cartId, lineItem) => {
@@ -0,0 +1,20 @@
import { MedusaError } from "medusa-core-utils"
export const InventoryServiceMock = {
withTransaction: function() {
return this
},
adjustInventory: jest.fn().mockReturnValue((_variantId, _quantity) => {
return Promise.resolve({})
}),
confirmInventory: jest.fn().mockImplementation((variantId, quantity) => {
if (quantity < 10) {
return true
} else {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Variant with id: ${variantId} does not have the required inventory`
)
}
}),
}
@@ -111,6 +111,46 @@ const giftCardVar = {
title: "100 USD", title: "100 USD",
} }
const outOfStockBackOrder = {
id: "bo",
title: "variant_popular",
inventory_quantity: 0,
allow_backorder: true,
manage_inventory: true,
}
const outOfStockNoBackOrder = {
id: "no_bo",
title: "variant_popular",
inventory_quantity: 0,
allow_backorder: false,
manage_inventory: true,
}
const outOfStockNoManage = {
id: "no_manage",
title: "variant_popular",
inventory_quantity: 0,
allow_backorder: false,
manage_inventory: false,
}
const StockOf10Manage = {
id: "10_man",
title: "variant_popular",
inventory_quantity: 10,
allow_backorder: false,
manage_inventory: true,
}
const StockOf1Manage = {
id: "1_man",
title: "variant_popular",
inventory_quantity: 1,
allow_backorder: false,
manage_inventory: true,
}
export const variants = { export const variants = {
one: variant1, one: variant1,
two: variant2, two: variant2,
@@ -171,17 +211,21 @@ export const ProductVariantServiceMock = {
if (variantId === IdMap.getId("testVariant")) { if (variantId === IdMap.getId("testVariant")) {
return Promise.resolve(testVariant) return Promise.resolve(testVariant)
} }
}), if (variantId === "bo") {
canCoverQuantity: jest.fn().mockImplementation((variantId, quantity) => { return Promise.resolve(outOfStockBackOrder)
if (variantId === IdMap.getId("can-cover")) {
return Promise.resolve(true)
} }
if (variantId === "no_bo") {
if (variantId === IdMap.getId("cannot-cover")) { return Promise.resolve(outOfStockNoBackOrder)
return Promise.resolve(false) }
if (variantId === "no_manage") {
return Promise.resolve(outOfStockNoManage)
}
if (variantId === "10_man") {
return Promise.resolve(StockOf10Manage)
}
if (variantId === "1_man") {
return Promise.resolve(StockOf1Manage)
} }
return Promise.reject(new Error("Not found"))
}), }),
getRegionPrice: jest.fn().mockImplementation((variantId, regionId) => { getRegionPrice: jest.fn().mockImplementation((variantId, regionId) => {
if (variantId === IdMap.getId("eur-10-us-12")) { if (variantId === IdMap.getId("eur-10-us-12")) {
+29 -10
View File
@@ -1,6 +1,8 @@
import _ from "lodash" import _ from "lodash"
import { IdMap, MockRepository, MockManager } from "medusa-test-utils" import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
import CartService from "../cart" import CartService from "../cart"
import { InventoryServiceMock } from "../__mocks__/inventory"
import { MedusaError } from "medusa-core-utils"
const eventBusService = { const eventBusService = {
emit: jest.fn(), emit: jest.fn(),
@@ -311,10 +313,18 @@ describe("CartService", () => {
}, },
} }
const productVariantService = { const inventoryService = {
canCoverQuantity: jest ...InventoryServiceMock,
.fn() confirmInventory: jest.fn().mockImplementation((variantId, _quantity) => {
.mockImplementation(id => id !== IdMap.getId("cannot-cover")), if (variantId !== IdMap.getId("cannot-cover")) {
return true
} else {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Variant with id: ${variantId} does not have the required inventory`
)
}
}),
} }
const cartRepository = MockRepository({ const cartRepository = MockRepository({
@@ -349,9 +359,9 @@ describe("CartService", () => {
totalsService, totalsService,
cartRepository, cartRepository,
lineItemService, lineItemService,
productVariantService,
eventBusService, eventBusService,
shippingOptionService, shippingOptionService,
inventoryService,
}) })
beforeEach(() => { beforeEach(() => {
@@ -449,7 +459,11 @@ describe("CartService", () => {
await expect( await expect(
cartService.addLineItem(IdMap.getId("cartWithLine"), lineItem) cartService.addLineItem(IdMap.getId("cartWithLine"), lineItem)
).rejects.toThrow(`Inventory doesn't cover the desired quantity`) ).rejects.toThrow(
`Variant with id: ${IdMap.getId(
"cannot-cover"
)} does not have the required inventory`
)
}) })
it("throws if inventory isn't covered", async () => { it("throws if inventory isn't covered", async () => {
@@ -463,7 +477,11 @@ describe("CartService", () => {
await expect( await expect(
cartService.addLineItem(IdMap.getId("cartWithLine"), lineItem) cartService.addLineItem(IdMap.getId("cartWithLine"), lineItem)
).rejects.toThrow(`Inventory doesn't cover the desired quantity`) ).rejects.toThrow(
`Variant with id: ${IdMap.getId(
"cannot-cover"
)} does not have the required inventory`
)
}) })
}) })
@@ -635,8 +653,9 @@ describe("CartService", () => {
return this return this
}, },
} }
const productVariantService = { const inventoryService = {
canCoverQuantity: jest ...InventoryServiceMock,
confirmInventory: jest
.fn() .fn()
.mockImplementation(id => id !== IdMap.getId("cannot-cover")), .mockImplementation(id => id !== IdMap.getId("cannot-cover")),
} }
@@ -669,9 +688,9 @@ describe("CartService", () => {
manager: MockManager, manager: MockManager,
totalsService, totalsService,
cartRepository, cartRepository,
productVariantService,
lineItemService, lineItemService,
eventBusService, eventBusService,
inventoryService,
}) })
beforeEach(() => { beforeEach(() => {
@@ -1,6 +1,7 @@
import _ from "lodash" import _ from "lodash"
import { IdMap, MockRepository, MockManager } from "medusa-test-utils" import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
import ClaimService from "../claim" import ClaimService from "../claim"
import { InventoryServiceMock } from "../__mocks__/inventory"
const withTransactionMock = jest.fn() const withTransactionMock = jest.fn()
const eventBusService = { const eventBusService = {
@@ -73,6 +74,14 @@ describe("ClaimService", () => {
}, },
} }
const inventoryService = {
...InventoryServiceMock,
withTransaction: function() {
withTransactionMock("inventory")
return this
},
}
const claimItemService = { const claimItemService = {
create: jest.fn(), create: jest.fn(),
withTransaction: function() { withTransaction: function() {
@@ -88,6 +97,7 @@ describe("ClaimService", () => {
returnService, returnService,
lineItemService, lineItemService,
claimItemService, claimItemService,
inventoryService,
eventBusService, eventBusService,
}) })
@@ -125,6 +135,18 @@ describe("ClaimService", () => {
1 1
) )
expect(inventoryService.confirmInventory).toHaveBeenCalledTimes(1)
expect(inventoryService.confirmInventory).toHaveBeenCalledWith(
"var_123",
1
)
expect(withTransactionMock).toHaveBeenCalledWith("inventory")
expect(inventoryService.adjustInventory).toHaveBeenCalledTimes(1)
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
"var_123",
-1
)
expect(withTransactionMock).toHaveBeenCalledWith("claimItem") expect(withTransactionMock).toHaveBeenCalledWith("claimItem")
expect(claimItemService.create).toHaveBeenCalledTimes(1) expect(claimItemService.create).toHaveBeenCalledTimes(1)
expect(claimItemService.create).toHaveBeenCalledWith({ expect(claimItemService.create).toHaveBeenCalledWith({
@@ -213,6 +235,24 @@ describe("ClaimService", () => {
).rejects.toThrow(`Claims must have at least one claim item.`) ).rejects.toThrow(`Claims must have at least one claim item.`)
}) })
it("fails if additional items are not in stock", async () => {
try {
const res = await claimService.create({
...testClaim,
additional_items: [
{
variant_id: "var_123",
quantity: 25,
},
],
})
console.warn(res)
} catch (e) {
expect(e.message).toEqual(
`Variant with id: var_123 does not have the required inventory`
)
}
})
it.each( it.each(
[ [
[false, false], [false, false],
@@ -0,0 +1,91 @@
import { MockManager } from "medusa-test-utils"
import InventoryService from "../inventory"
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
describe("InventoryService", () => {
describe("confirmInventory", () => {
const inventoryService = new InventoryService({
manager: MockManager,
productVariantService: ProductVariantServiceMock,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("returns false when inventory is managed, and no back orders are allowed and the quantity is larger than inventory", async () => {
await expect(
inventoryService.confirmInventory("no_bo", 10)
).rejects.toThrow(
`Variant with id: no_bo does not have the required inventory`
)
})
it("returns true when variant is out of stock but allows back orders", async () => {
const result = await inventoryService.confirmInventory("bo", 100)
expect(result).toEqual(true)
})
it("returns true when variant is out of stock but inventory quantity is not managed", async () => {
const result = await inventoryService.confirmInventory("no_manage", 10000)
expect(result).toEqual(true)
})
it("returns true when managed variant inventory_quantity > requested quantity", async () => {
const result = await inventoryService.confirmInventory("10_man", 5)
expect(result).toEqual(true)
})
it("returns false when managed variant inventory_quantity < requested quantity", async () => {
await expect(
inventoryService.confirmInventory("10_man", 50)
).rejects.toThrow(
`Variant with id: 10_man does not have the required inventory`
)
})
})
describe("adjustInventory", () => {
const inventoryService = new InventoryService({
manager: MockManager,
productVariantService: ProductVariantServiceMock,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("should not call update in productVariantService because variant is not managed", async () => {
await inventoryService.adjustInventory("no_manage", 1000)
expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(0)
})
it("should call update in productVariantService once", async () => {
await inventoryService.adjustInventory("10_man", 10)
expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(1)
expect(ProductVariantServiceMock.update).toHaveBeenCalledWith(
{
id: "10_man",
title: "variant_popular",
inventory_quantity: 10,
allow_backorder: false,
manage_inventory: true,
},
{
inventory_quantity: 20,
}
)
})
it("should update update once for 1man", async () => {
await inventoryService.adjustInventory("1_man", -1)
expect(ProductVariantServiceMock.update).toHaveBeenCalledWith(
{
id: "1_man",
title: "variant_popular",
inventory_quantity: 1,
allow_backorder: false,
manage_inventory: true,
},
{
inventory_quantity: 0,
}
)
})
})
})
+106 -4
View File
@@ -1,5 +1,6 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import OrderService from "../order" import OrderService from "../order"
import { InventoryServiceMock } from "../__mocks__/inventory"
describe("OrderService", () => { describe("OrderService", () => {
const totalsService = { const totalsService = {
@@ -37,6 +38,10 @@ describe("OrderService", () => {
}, },
} }
const inventoryService = {
...InventoryServiceMock,
}
describe("create", () => { describe("create", () => {
const orderRepo = MockRepository({ create: f => f }) const orderRepo = MockRepository({ create: f => f })
const orderService = new OrderService({ const orderService = new OrderService({
@@ -95,6 +100,9 @@ describe("OrderService", () => {
return Promise.resolve(payment.status || "authorized") return Promise.resolve(payment.status || "authorized")
}, },
updatePayment: jest.fn(), updatePayment: jest.fn(),
cancelPayment: jest.fn().mockImplementation(payment => {
return Promise.resolve({ ...payment, status: "cancelled" })
}),
withTransaction: function() { withTransaction: function() {
return this return this
}, },
@@ -153,6 +161,7 @@ describe("OrderService", () => {
regionService, regionService,
eventBusService, eventBusService,
cartService, cartService,
inventoryService,
}) })
beforeEach(async () => { beforeEach(async () => {
@@ -186,7 +195,10 @@ describe("OrderService", () => {
gift_cards: [], gift_cards: [],
discounts: [], discounts: [],
shipping_methods: [{ id: "method_1" }], shipping_methods: [{ id: "method_1" }],
items: [{ id: "item_1" }, { id: "item_2" }], items: [
{ id: "item_1", variant_id: "variant-1", quantity: 1 },
{ id: "item_2", variant_id: "variant-2", quantity: 1 },
],
total: 100, total: 100,
} }
@@ -231,6 +243,16 @@ describe("OrderService", () => {
} }
) )
expect(inventoryService.adjustInventory).toHaveBeenCalledTimes(2)
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
"variant-2",
-1
)
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
"variant-1",
-1
)
expect(lineItemService.update).toHaveBeenCalledTimes(2) expect(lineItemService.update).toHaveBeenCalledTimes(2)
expect(lineItemService.update).toHaveBeenCalledWith("item_1", { expect(lineItemService.update).toHaveBeenCalledWith("item_1", {
order_id: "id", order_id: "id",
@@ -272,7 +294,10 @@ describe("OrderService", () => {
], ],
discounts: [], discounts: [],
shipping_methods: [{ id: "method_1" }], shipping_methods: [{ id: "method_1" }],
items: [{ id: "item_1" }, { id: "item_2" }], items: [
{ id: "item_1", variant_id: "variant-1", quantity: 1 },
{ id: "item_2", variant_id: "variant-2", quantity: 1 },
],
subtotal: 100, subtotal: 100,
total: 100, total: 100,
} }
@@ -361,7 +386,10 @@ describe("OrderService", () => {
billing_address_id: "1234", billing_address_id: "1234",
discounts: [], discounts: [],
shipping_methods: [{ id: "method_1" }], shipping_methods: [{ id: "method_1" }],
items: [{ id: "item_1" }, { id: "item_2" }], items: [
{ id: "item_1", variant_id: "variant-1", quantity: 1 },
{ id: "item_2", variant_id: "variant-2", quantity: 1 },
],
total: 0, total: 0,
} }
orderService.cartService_.retrieve = () => Promise.resolve(cart) orderService.cartService_.retrieve = () => Promise.resolve(cart)
@@ -395,6 +423,45 @@ describe("OrderService", () => {
expect(orderRepo.save).toHaveBeenCalledWith(order) expect(orderRepo.save).toHaveBeenCalledWith(order)
}) })
it("fails because an item does not have the required inventory", async () => {
const cart = {
id: "cart_id",
email: "test@test.com",
customer_id: "cus_1234",
payment: {
id: "testpayment",
amount: 100,
status: "authorized",
},
region_id: "test",
region: {
id: "test",
currency_code: "eur",
name: "test",
tax_rate: 25,
},
gift_cards: [],
shipping_address_id: "1234",
billing_address_id: "1234",
discounts: [],
shipping_methods: [{ id: "method_1" }],
items: [
{ id: "item_1", variant_id: "variant-1", quantity: 12 },
{ id: "item_2", variant_id: "variant-2", quantity: 1 },
],
total: 100,
}
orderService.cartService_.retrieve = () => Promise.resolve(cart)
const res = orderService.createFromCart(cart)
await expect(res).rejects.toThrow(
"Variant with id: variant-1 does not have the required inventory"
)
//check to see if payment is cancelled
expect(
orderService.paymentProviderService_.cancelPayment
).toHaveBeenCalledTimes(1)
})
}) })
describe("retrieve", () => { describe("retrieve", () => {
@@ -545,6 +612,10 @@ describe("OrderService", () => {
status: "pending", status: "pending",
fulfillments: [{ id: "fulfillment_test" }], fulfillments: [{ id: "fulfillment_test" }],
payments: [{ id: "payment_test" }], payments: [{ id: "payment_test" }],
items: [
{ id: "item_1", variant_id: "variant-1", quantity: 12 },
{ id: "item_2", variant_id: "variant-2", quantity: 1 },
],
}) })
} }
}, },
@@ -571,6 +642,7 @@ describe("OrderService", () => {
paymentProviderService, paymentProviderService,
fulfillmentService, fulfillmentService,
eventBusService, eventBusService,
inventoryService,
}) })
beforeEach(async () => { beforeEach(async () => {
@@ -578,7 +650,15 @@ describe("OrderService", () => {
}) })
it("calls order model functions", async () => { it("calls order model functions", async () => {
await orderService.cancel(IdMap.getId("not-fulfilled-order")) try {
const order = await orderService.retrieve(
IdMap.getId("not-fulfilled-order")
)
console.warn(order)
await orderService.cancel(IdMap.getId("not-fulfilled-order"))
} catch (e) {
console.warn(e)
}
expect(paymentProviderService.cancelPayment).toHaveBeenCalledTimes(1) expect(paymentProviderService.cancelPayment).toHaveBeenCalledTimes(1)
expect(paymentProviderService.cancelPayment).toHaveBeenCalledWith({ expect(paymentProviderService.cancelPayment).toHaveBeenCalledWith({
@@ -590,6 +670,16 @@ describe("OrderService", () => {
id: "fulfillment_test", id: "fulfillment_test",
}) })
expect(inventoryService.adjustInventory).toHaveBeenCalledTimes(2)
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
"variant-1",
12
)
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
"variant-2",
1
)
expect(orderRepo.save).toHaveBeenCalledTimes(1) expect(orderRepo.save).toHaveBeenCalledTimes(1)
expect(orderRepo.save).toHaveBeenCalledWith({ expect(orderRepo.save).toHaveBeenCalledWith({
fulfillment_status: "canceled", fulfillment_status: "canceled",
@@ -597,6 +687,18 @@ describe("OrderService", () => {
status: "canceled", status: "canceled",
fulfillments: [{ id: "fulfillment_test" }], fulfillments: [{ id: "fulfillment_test" }],
payments: [{ id: "payment_test" }], payments: [{ id: "payment_test" }],
items: [
{
id: "item_1",
quantity: 12,
variant_id: "variant-1",
},
{
id: "item_2",
quantity: 1,
variant_id: "variant-2",
},
],
}) })
}) })
@@ -384,6 +384,29 @@ describe("ProductVariantService", () => {
}) })
}) })
it("successfully updates variant inventory_quantity", async () => {
await productVariantService.update(IdMap.getId("ironman"), {
title: "new title",
inventory_quantity: 98,
})
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
expect(eventBusService.emit).toHaveBeenCalledWith(
"product-variant.updated",
{
id: IdMap.getId("ironman"),
fields: ["title", "inventory_quantity"],
}
)
expect(productVariantRepository.save).toHaveBeenCalledTimes(1)
expect(productVariantRepository.save).toHaveBeenCalledWith({
id: IdMap.getId("ironman"),
inventory_quantity: 98,
title: "new title",
})
})
it("successfully updates variant prices", async () => { it("successfully updates variant prices", async () => {
await productVariantService.update(IdMap.getId("ironman"), { await productVariantService.update(IdMap.getId("ironman"), {
title: "new title", title: "new title",
@@ -809,73 +832,4 @@ describe("ProductVariantService", () => {
expect(result).toBe(undefined) expect(result).toBe(undefined)
}) })
}) })
describe("canCoverQuantity", () => {
const productVariantRepository = MockRepository({
findOne: query => {
if (query.where.id === IdMap.getId("no-manageable-ironman")) {
return Promise.resolve({ manage_inventory: false })
}
if (query.where.id === IdMap.getId("backorder-ironman")) {
return Promise.resolve({ allow_backorder: true })
}
if (query.where.id === IdMap.getId("no-ironman")) {
return Promise.resolve({
inventory_quantity: 5,
manage_inventory: true,
allow_backorder: false,
})
}
return Promise.resolve({
inventory_quantity: 20,
})
},
})
const productVariantService = new ProductVariantService({
manager: MockManager,
eventBusService,
productVariantRepository,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("returns true if there is more inventory than requested", async () => {
const res = await productVariantService.canCoverQuantity(
IdMap.getId("ironman"),
10
)
expect(res).toEqual(true)
})
it("returns true if inventory not managed", async () => {
const res = await productVariantService.canCoverQuantity(
IdMap.getId("no-manageable-ironman"),
10
)
expect(res).toEqual(true)
})
it("returns true if backorders allowed", async () => {
const res = await productVariantService.canCoverQuantity(
IdMap.getId("backorder-ironman"),
10
)
expect(res).toEqual(true)
})
it("returns false if insufficient inventory", async () => {
const res = await productVariantService.canCoverQuantity(
IdMap.getId("no-ironman"),
20
)
expect(res).toEqual(false)
})
})
}) })
@@ -1,5 +1,7 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import idMap from "medusa-test-utils/dist/id-map"
import ReturnService from "../return" import ReturnService from "../return"
import { InventoryServiceMock } from "../__mocks__/inventory"
describe("ReturnService", () => { describe("ReturnService", () => {
// describe("requestReturn", () => { // describe("requestReturn", () => {
@@ -196,11 +198,13 @@ describe("ReturnService", () => {
id: IdMap.getId("test-line"), id: IdMap.getId("test-line"),
quantity: 10, quantity: 10,
returned_quantity: 0, returned_quantity: 0,
variant_id: "test-variant",
}, },
{ {
id: IdMap.getId("test-line-2"), id: IdMap.getId("test-line-2"),
quantity: 10, quantity: 10,
returned_quantity: 0, returned_quantity: 0,
variant_id: "test-variant-2",
}, },
], ],
payments: [{ id: "payment_test" }], payments: [{ id: "payment_test" }],
@@ -221,12 +225,29 @@ describe("ReturnService", () => {
}, },
} }
const inventoryService = {
adjustInventory: jest.fn((variantId, quantity) => {
return Promise.resolve({})
}),
confirmInventory: jest.fn((variantId, quantity) => {
if (quantity < 10) {
return true
} else {
return false
}
}),
withTransaction: function() {
return this
},
}
const returnService = new ReturnService({ const returnService = new ReturnService({
manager: MockManager, manager: MockManager,
totalsService, totalsService,
lineItemService, lineItemService,
orderService, orderService,
returnRepository, returnRepository,
inventoryService,
}) })
beforeEach(async () => { beforeEach(async () => {
@@ -268,6 +289,12 @@ describe("ReturnService", () => {
returned_quantity: 10, returned_quantity: 10,
} }
) )
expect(inventoryService.adjustInventory).toHaveBeenCalledTimes(1)
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
"test-variant",
10
)
}) })
it("successfully receives a return with requires_action status", async () => { it("successfully receives a return with requires_action status", async () => {
@@ -280,6 +307,16 @@ describe("ReturnService", () => {
1000 1000
) )
expect(inventoryService.adjustInventory).toHaveBeenCalledTimes(2)
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
"test-variant",
10
)
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
"test-variant-2",
10
)
expect(returnRepository.save).toHaveBeenCalledTimes(1) expect(returnRepository.save).toHaveBeenCalledTimes(1)
expect(returnRepository.save).toHaveBeenCalledWith({ expect(returnRepository.save).toHaveBeenCalledWith({
id: IdMap.getId("test-return-2"), id: IdMap.getId("test-return-2"),
+92 -35
View File
@@ -1,5 +1,6 @@
import { IdMap, MockRepository, MockManager } from "medusa-test-utils" import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
import SwapService from "../swap" import SwapService from "../swap"
import { InventoryServiceMock } from "../__mocks__/inventory"
const eventBusService = { const eventBusService = {
emit: jest.fn(), emit: jest.fn(),
@@ -685,44 +686,51 @@ describe("SwapService", () => {
Date.now = jest.fn(() => 1572393600000) Date.now = jest.fn(() => 1572393600000)
}) })
const eventBusService = {
emit: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function() {
return this
},
}
const totalsService = {
getTotal: () => {
return Promise.resolve(100)
},
}
const shippingOptionService = {
updateShippingMethod: () => {
return Promise.resolve()
},
withTransaction: function() {
return this
},
}
const paymentProviderService = {
getStatus: jest.fn(() => {
return Promise.resolve("authorized")
}),
updatePayment: jest.fn(() => {
return Promise.resolve()
}),
withTransaction: function() {
return this
},
}
const inventoryService = {
...InventoryServiceMock,
withTransaction: function() {
return this
},
}
describe("success", () => { describe("success", () => {
const eventBusService = {
emit: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function() {
return this
},
}
const totalsService = {
getTotal: () => {
return Promise.resolve(100)
},
}
const shippingOptionService = {
updateShippingMethod: () => {
return Promise.resolve()
},
withTransaction: function() {
return this
},
}
const paymentProviderService = {
getStatus: jest.fn(() => {
return Promise.resolve("authorized")
}),
updatePayment: jest.fn(() => {
return Promise.resolve()
}),
withTransaction: function() {
return this
},
}
const existing = { const existing = {
cart: { cart: {
items: [{ id: "1" }], items: [{ id: "1", variant_id: "variant", quantity: 2 }],
shipping_methods: [{ id: "method_1" }], shipping_methods: [{ id: "method_1" }],
payment: { payment: {
good: "yes", good: "yes",
@@ -744,6 +752,7 @@ describe("SwapService", () => {
paymentProviderService, paymentProviderService,
eventBusService, eventBusService,
shippingOptionService, shippingOptionService,
inventoryService,
}) })
it("creates a shipment", async () => { it("creates a shipment", async () => {
@@ -753,6 +762,15 @@ describe("SwapService", () => {
good: "yes", good: "yes",
}) })
expect(inventoryService.confirmInventory).toHaveBeenCalledWith(
"variant",
2
)
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
"variant",
-2
)
expect(swapRepo.save).toHaveBeenCalledWith({ expect(swapRepo.save).toHaveBeenCalledWith({
...existing, ...existing,
difference_due: 100, difference_due: 100,
@@ -761,6 +779,45 @@ describe("SwapService", () => {
}) })
}) })
}) })
describe("failure", () => {
const existing = {
cart: {
items: [{ id: "1", variant_id: "variant", quantity: 25 }],
shipping_methods: [{ id: "method_1" }],
payment: {
good: "yes",
},
shipping_address_id: 1234,
},
other: "data",
}
const swapRepo = MockRepository({
findOneWithRelations: () => Promise.resolve(existing),
})
const swapService = new SwapService({
manager: MockManager,
eventBusService,
swapRepository: swapRepo,
totalsService,
paymentProviderService,
eventBusService,
shippingOptionService,
inventoryService,
})
it("throws an error because inventory is to low", async () => {
try {
await swapService.registerCartCompletion(IdMap.getId("swap"))
} catch (e) {
expect(e.message).toEqual(
`Variant with id: variant does not have the required inventory`
)
}
})
})
}) })
describe("processDifference", () => { describe("processDifference", () => {
+16 -50
View File
@@ -31,6 +31,7 @@ class CartService extends BaseService {
totalsService, totalsService,
addressRepository, addressRepository,
paymentSessionRepository, paymentSessionRepository,
inventoryService,
}) { }) {
super() super()
@@ -84,6 +85,9 @@ class CartService extends BaseService {
/** @private @const {PaymentSessionRepository} */ /** @private @const {PaymentSessionRepository} */
this.paymentSessionRepository_ = paymentSessionRepository this.paymentSessionRepository_ = paymentSessionRepository
/** @private @const {InventoryService} */
this.inventoryService_ = inventoryService
} }
withTransaction(transactionManager) { withTransaction(transactionManager) {
@@ -109,6 +113,7 @@ class CartService extends BaseService {
totalsService: this.totalsService_, totalsService: this.totalsService_,
addressRepository: this.addressRepository_, addressRepository: this.addressRepository_,
giftCardService: this.giftCardService_, giftCardService: this.giftCardService_,
inventoryService: this.inventoryService_,
}) })
cloned.transactionManager_ = transactionManager cloned.transactionManager_ = transactionManager
@@ -135,27 +140,6 @@ class CartService extends BaseService {
* @typedef {LineItemContent[]} LineItemContentArray * @typedef {LineItemContent[]} LineItemContentArray
*/ */
/**
* Confirms if the contents of a line item is covered by the inventory.
* To be covered a variant must either not have its inventory managed or it
* must allow backorders or it must have enough inventory to cover the request.
* If the content is made up of multiple variants it will return true if all
* variants can be covered. If the content consists of a single variant it will
* return true if the variant is covered.
* @param {(LineItemContent | LineItemContentArray)} - the content of the line
* item
* @param {number} - the quantity of the line item
* @return {boolean} true if the inventory covers the line item.
*/
async confirmInventory_(variantId, quantity) {
// If the line item is not stock tracked we don't have double check it
if (!variantId) {
return true
}
return this.productVariantService_.canCoverQuantity(variantId, quantity)
}
transformQueryForTotals_(config) { transformQueryForTotals_(config) {
let { select, relations } = config let { select, relations } = config
@@ -454,19 +438,10 @@ class CartService extends BaseService {
// simply update the quantity of the existing line item // simply update the quantity of the existing line item
if (currentItem) { if (currentItem) {
const newQuantity = currentItem.quantity + lineItem.quantity const newQuantity = currentItem.quantity + lineItem.quantity
// Confirm inventory or throw error
// Confirm inventory await this.inventoryService_
const hasInventory = await this.confirmInventory_( .withTransaction(manager)
lineItem.variant_id, .confirmInventory(lineItem.variant_id, newQuantity)
newQuantity
)
if (!hasInventory) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Inventory doesn't cover the desired quantity"
)
}
await this.lineItemService_ await this.lineItemService_
.withTransaction(manager) .withTransaction(manager)
@@ -474,18 +449,10 @@ class CartService extends BaseService {
quantity: newQuantity, quantity: newQuantity,
}) })
} else { } else {
// Confirm inventory // Confirm inventory or throw error
const hasInventory = await this.confirmInventory_( await this.inventoryService_
lineItem.variant_id, .withTransaction(manager)
lineItem.quantity .confirmInventory(lineItem.variant_id, lineItem.quantity)
)
if (!hasInventory) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Inventory doesn't cover the desired quantity"
)
}
await this.lineItemService_.withTransaction(manager).create({ await this.lineItemService_.withTransaction(manager).create({
...lineItem, ...lineItem,
@@ -541,10 +508,9 @@ class CartService extends BaseService {
} }
if (lineItemUpdate.quantity) { if (lineItemUpdate.quantity) {
const hasInventory = await this.confirmInventory_( const hasInventory = await this.inventoryService_
lineItemExists.variant_id, .withTransaction(manager)
lineItemUpdate.quantity .confirmInventory(lineItemExists.variant_id, lineItemUpdate.quantity)
)
if (!hasInventory) { if (!hasInventory) {
throw new MedusaError( throw new MedusaError(
+16
View File
@@ -26,6 +26,7 @@ class ClaimService extends BaseService {
shippingOptionService, shippingOptionService,
claimItemService, claimItemService,
regionService, regionService,
inventoryService,
eventBusService, eventBusService,
}) { }) {
super() super()
@@ -63,6 +64,9 @@ class ClaimService extends BaseService {
/** @private @constant {TotalsService} */ /** @private @constant {TotalsService} */
this.totalsService_ = totalsService this.totalsService_ = totalsService
/** @private @constant {InventoryService} */
this.inventoryService_ = inventoryService
/** @private @constant {EventBus} */ /** @private @constant {EventBus} */
this.eventBus_ = eventBusService this.eventBus_ = eventBusService
@@ -88,6 +92,7 @@ class ClaimService extends BaseService {
claimItemService: this.claimItemService_, claimItemService: this.claimItemService_,
eventBusService: this.eventBus_, eventBusService: this.eventBus_,
totalsService: this.totalsService_, totalsService: this.totalsService_,
inventoryService: this.inventoryService_,
shippingOptionService: this.shippingOptionService_, shippingOptionService: this.shippingOptionService_,
}) })
@@ -232,6 +237,12 @@ class ClaimService extends BaseService {
toRefund = await this.totalsService_.getRefundTotal(order, lines) toRefund = await this.totalsService_.getRefundTotal(order, lines)
} }
for (const item of additional_items) {
await this.inventoryService_
.withTransaction(manager)
.confirmInventory(item.variant_id, item.quantity)
}
const newItems = await Promise.all( const newItems = await Promise.all(
additional_items.map(i => additional_items.map(i =>
this.lineItemService_ this.lineItemService_
@@ -240,6 +251,11 @@ class ClaimService extends BaseService {
) )
) )
for (const newItem of newItems) {
await this.inventoryService_
.withTransaction(manager)
.adjustInventory(newItem.variant_id, -newItem.quantity)
}
const evaluatedNoNotification = const evaluatedNoNotification =
no_notification !== undefined ? no_notification : order.no_notification no_notification !== undefined ? no_notification : order.no_notification
@@ -148,7 +148,7 @@ class IdempotencyKeyService extends BaseService {
*/ */
async workStage(idempotencyKey, func) { async workStage(idempotencyKey, func) {
try { try {
return this.atomicPhase_(async manager => { return await this.atomicPhase_(async manager => {
let key let key
const { recovery_point, response_code, response_body } = await func( const { recovery_point, response_code, response_body } = await func(
+87
View File
@@ -0,0 +1,87 @@
import { BaseService } from "medusa-interfaces"
import { MedusaError } from "medusa-core-utils"
const fs = require("fs")
class InventoryService extends BaseService {
constructor({ manager, productVariantService }) {
super()
/** @private @const {EntityManager} */
this.manager_ = manager
/** @private @const {ProductVariantRepository_} */
this.productVariantService_ = productVariantService
}
withTransaction(transactionManager) {
if (!transactionManager) {
return this
}
const cloned = new InventoryService({
manager: transactionManager,
productVariantService: this.productVariantService_,
})
cloned.transactionManager_ = transactionManager
return cloned
}
/**
* Updates the inventory of a variant based on a given adjustment.
* @params {string} variantId - the id of the variant to update
* @params {number} adjustment - the number to adjust the inventory quantity by
* @return {Promise} resolves to the update result.
*/
async adjustInventory(variantId, adjustment) {
//if variantId is undefined ergo. a custom item then do nothing
if (typeof variantId === "undefined") {
return
}
return this.atomicPhase_(async manager => {
const variant = await this.productVariantService_.retrieve(variantId)
//if inventory is managed then update
if (variant.manage_inventory) {
return await this.productVariantService_
.withTransaction(manager)
.update(variant, {
inventory_quantity: variant.inventory_quantity + adjustment,
})
}
})
}
/**
* Checks if the inventory of a variant can cover a given quantity. Will
* return true if the variant doesn't have managed inventory or if the variant
* allows backorders or if the inventory quantity is greater than `quantity`.
* @params {string} variantId - the id of the variant to check
* @params {number} quantity - the number of units to check availability for
* @return {boolean} true if the inventory covers the quantity
*/
async confirmInventory(variantId, quantity) {
//if variantId is undefined then confirm inventory as it
//is a custom item that is not managed
if (typeof variantId === "undefined") {
return true
}
const variant = await this.productVariantService_.retrieve(variantId)
const { inventory_quantity, allow_backorder, manage_inventory } = variant
const isCovered =
!manage_inventory || allow_backorder || inventory_quantity >= quantity
if (!isCovered) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Variant with id: ${variant.id} does not have the required inventory`,
MedusaError.Codes.INSUFFICIENT_INVENTORY
)
}
return isCovered
}
}
export default InventoryService
+33 -2
View File
@@ -39,6 +39,7 @@ class OrderService extends BaseService {
addressRepository, addressRepository,
giftCardService, giftCardService,
draftOrderService, draftOrderService,
inventoryService,
eventBusService, eventBusService,
}) { }) {
super() super()
@@ -93,6 +94,9 @@ class OrderService extends BaseService {
/** @private @constant {DraftOrderService} */ /** @private @constant {DraftOrderService} */
this.draftOrderService_ = draftOrderService this.draftOrderService_ = draftOrderService
/** @private @constant {InventoryService} */
this.inventoryService_ = inventoryService
} }
withTransaction(manager) { withTransaction(manager) {
@@ -118,6 +122,7 @@ class OrderService extends BaseService {
giftCardService: this.giftCardService_, giftCardService: this.giftCardService_,
addressRepository: this.addressRepository_, addressRepository: this.addressRepository_,
draftOrderService: this.draftOrderService_, draftOrderService: this.draftOrderService_,
inventoryService: this.inventoryService_,
}) })
cloned.transactionManager_ = manager cloned.transactionManager_ = manager
@@ -451,6 +456,21 @@ class OrderService extends BaseService {
) )
} }
const { payment, region, total } = cart
for (const item of cart.items) {
try {
await this.inventoryService_
.withTransaction(manager)
.confirmInventory(item.variant_id, item.quantity)
} catch (err) {
await this.paymentProviderService_
.withTransaction(manager)
.cancelPayment(payment)
throw err
}
}
const exists = await this.existsByCartId(cart.id) const exists = await this.existsByCartId(cart.id)
if (exists) { if (exists) {
throw new MedusaError( throw new MedusaError(
@@ -459,7 +479,6 @@ class OrderService extends BaseService {
) )
} }
const { payment, region, total } = cart
// Would be the case if a discount code is applied that covers the item // Would be the case if a discount code is applied that covers the item
// total // total
if (total !== 0) { if (total !== 0) {
@@ -551,6 +570,12 @@ class OrderService extends BaseService {
.update(item.id, { order_id: result.id }) .update(item.id, { order_id: result.id })
} }
for (const item of cart.items) {
await this.inventoryService_
.withTransaction(manager)
.adjustInventory(item.variant_id, -item.quantity)
}
await this.eventBus_ await this.eventBus_
.withTransaction(manager) .withTransaction(manager)
.emit(OrderService.Events.PLACED, { .emit(OrderService.Events.PLACED, {
@@ -864,7 +889,7 @@ class OrderService extends BaseService {
async cancel(orderId) { async cancel(orderId) {
return this.atomicPhase_(async manager => { return this.atomicPhase_(async manager => {
const order = await this.retrieve(orderId, { const order = await this.retrieve(orderId, {
relations: ["fulfillments", "payments"], relations: ["fulfillments", "payments", "items"],
}) })
if (order.payment_status !== "awaiting") { if (order.payment_status !== "awaiting") {
@@ -882,6 +907,12 @@ class OrderService extends BaseService {
) )
) )
for (const item of order.items) {
await this.inventoryService_
.withTransaction(manager)
.adjustInventory(item.variant_id, item.quantity)
}
for (const p of order.payments) { for (const p of order.payments) {
await this.paymentProviderService_ await this.paymentProviderService_
.withTransaction(manager) .withTransaction(manager)
@@ -253,6 +253,7 @@ class PaymentProviderService extends BaseService {
amount: total, amount: total,
currency_code: region.currency_code, currency_code: region.currency_code,
data: paymentData, data: paymentData,
cart_id: cart.id,
}) })
return paymentRepo.save(created) return paymentRepo.save(created)
@@ -260,7 +260,7 @@ class ProductVariantService extends BaseService {
) )
} }
const { prices, options, metadata, ...rest } = update const { prices, options, metadata, inventory_quantity, ...rest } = update
if (prices) { if (prices) {
for (const price of prices) { for (const price of prices) {
@@ -291,11 +291,16 @@ class ProductVariantService extends BaseService {
variant.metadata = this.setMetadata_(variant, metadata) variant.metadata = this.setMetadata_(variant, metadata)
} }
if (typeof inventory_quantity === "number") {
variant.inventory_quantity = inventory_quantity
}
for (const [key, value] of Object.entries(rest)) { for (const [key, value] of Object.entries(rest)) {
variant[key] = value variant[key] = value
} }
const result = await variantRepo.save(variant) const result = await variantRepo.save(variant)
await this.eventBus_ await this.eventBus_
.withTransaction(manager) .withTransaction(manager)
.emit(ProductVariantService.Events.UPDATED, { .emit(ProductVariantService.Events.UPDATED, {
@@ -518,23 +523,6 @@ class ProductVariantService extends BaseService {
}) })
} }
/**
* Checks if the inventory of a variant can cover a given quantity. Will
* return true if the variant doesn't have managed inventory or if the variant
* allows backorders or if the inventory quantity is greater than `quantity`.
* @params {string} variantId - the id of the variant to check
* @params {number} quantity - the number of units to check availability for
* @return {boolean} true if the inventory covers the quantity
*/
async canCoverQuantity(variantId, quantity) {
const variant = await this.retrieve(variantId)
const { inventory_quantity, allow_backorder, manage_inventory } = variant
return (
!manage_inventory || allow_backorder || inventory_quantity >= quantity
)
}
/** /**
* @param {Object} selector - the query object for find * @param {Object} selector - the query object for find
* @return {Promise} the result of the find operation * @return {Promise} the result of the find operation
+13
View File
@@ -16,6 +16,7 @@ class ReturnService extends BaseService {
shippingOptionService, shippingOptionService,
returnReasonService, returnReasonService,
fulfillmentProviderService, fulfillmentProviderService,
inventoryService,
orderService, orderService,
}) { }) {
super() super()
@@ -43,6 +44,8 @@ class ReturnService extends BaseService {
this.returnReasonService_ = returnReasonService this.returnReasonService_ = returnReasonService
this.inventoryService_ = inventoryService
/** @private @const {OrderService} */ /** @private @const {OrderService} */
this.orderService_ = orderService this.orderService_ = orderService
} }
@@ -61,6 +64,7 @@ class ReturnService extends BaseService {
shippingOptionService: this.shippingOptionService_, shippingOptionService: this.shippingOptionService_,
fulfillmentProviderService: this.fulfillmentProviderService_, fulfillmentProviderService: this.fulfillmentProviderService_,
returnReasonService: this.returnReasonService_, returnReasonService: this.returnReasonService_,
inventoryService: this.inventoryService_,
orderService: this.orderService_, orderService: this.orderService_,
}) })
@@ -513,6 +517,15 @@ class ReturnService extends BaseService {
}) })
} }
for (const line of newLines) {
const orderItem = order.items.find(i => i.id === line.item_id)
if (orderItem) {
await this.inventoryService_
.withTransaction(manager)
.adjustInventory(orderItem.variant_id, line.received_quantity)
}
}
return result return result
}) })
} }
+19
View File
@@ -31,6 +31,7 @@ class SwapService extends BaseService {
shippingOptionService, shippingOptionService,
fulfillmentService, fulfillmentService,
orderService, orderService,
inventoryService,
}) { }) {
super() super()
@@ -64,6 +65,9 @@ class SwapService extends BaseService {
/** @private @const {ShippingOptionService} */ /** @private @const {ShippingOptionService} */
this.shippingOptionService_ = shippingOptionService this.shippingOptionService_ = shippingOptionService
/** @private @const {InventoryService} */
this.inventoryService_ = inventoryService
/** @private @const {EventBusService} */ /** @private @const {EventBusService} */
this.eventBus_ = eventBusService this.eventBus_ = eventBusService
} }
@@ -84,6 +88,7 @@ class SwapService extends BaseService {
paymentProviderService: this.paymentProviderService_, paymentProviderService: this.paymentProviderService_,
shippingOptionService: this.shippingOptionService_, shippingOptionService: this.shippingOptionService_,
orderService: this.orderService_, orderService: this.orderService_,
inventoryService: this.inventoryService_,
fulfillmentService: this.fulfillmentService_, fulfillmentService: this.fulfillmentService_,
}) })
@@ -621,6 +626,14 @@ class SwapService extends BaseService {
const cart = swap.cart const cart = swap.cart
const items = swap.cart.items
for (const item of items) {
await this.inventoryService_
.withTransaction(manager)
.confirmInventory(item.variant_id, item.quantity)
}
const total = await this.totalsService_.getTotal(cart) const total = await this.totalsService_.getTotal(cart)
if (total > 0) { if (total > 0) {
@@ -651,6 +664,12 @@ class SwapService extends BaseService {
swap_id: swapId, swap_id: swapId,
order_id: swap.order_id, order_id: swap.order_id,
}) })
for (const item of items) {
await this.inventoryService_
.withTransaction(manager)
.adjustInventory(item.variant_id, -item.quantity)
}
} }
const now = new Date() const now = new Date()
+548 -41
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -4296,6 +4296,11 @@ dir-glob@^3.0.1:
dependencies: dependencies:
path-type "^4.0.0" path-type "^4.0.0"
dom-walk@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==
domexception@^2.0.1: domexception@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304"
@@ -5109,6 +5114,14 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
once "^1.3.0" once "^1.3.0"
path-is-absolute "^1.0.0" path-is-absolute "^1.0.0"
global@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
dependencies:
min-document "^2.19.0"
process "^0.11.10"
globals@^11.1.0: globals@^11.1.0:
version "11.12.0" version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@@ -6833,6 +6846,13 @@ mimic-fn@^2.0.0, mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
min-document@^2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=
dependencies:
dom-walk "^0.1.0"
minimatch@^3.0.4: minimatch@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@@ -7894,6 +7914,11 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
process@^0.11.10:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
promise-inflight@^1.0.1: promise-inflight@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"