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
parent 44fce520aa
commit f07cc0fa40
32 changed files with 1870 additions and 268 deletions

View File

@@ -1,6 +1,11 @@
const { dropDatabase } = require("pg-god");
const path = require("path");
const { ReturnReason } = require("@medusajs/medusa");
const {
ReturnReason,
Order,
LineItem,
ProductVariant,
} = require("@medusajs/medusa");
const setupServer = require("../../../helpers/setup-server");
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", () => {
beforeEach(async () => {
try {
@@ -154,6 +328,18 @@ describe("/admin/orders", () => {
);
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(
"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", () => {
@@ -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", () => {
@@ -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.data.order.items[1].returned_quantity).toBe(3);
expect(toTest.returned_quantity).toBe(3);
});
it("creates a swap and receives the items", async () => {

View File

@@ -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",
}
`;

View File

@@ -1,6 +1,6 @@
const { dropDatabase } = require("pg-god");
const path = require("path");
const { Region } = require("@medusajs/medusa");
const { Region, LineItem, Payment } = require("@medusajs/medusa");
const setupServer = require("../../../helpers/setup-server");
const { useApi } = require("../../../helpers/use-api");
@@ -15,15 +15,21 @@ describe("/store/carts", () => {
let dbConnection;
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_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"`);
await manager.query(`DELETE FROM "shipping_method"`);
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 "customer"`);
await manager.query(
@@ -208,6 +214,65 @@ describe("/store/carts", () => {
expect(cart.data.cart.shipping_total).toBe(1000);
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", () => {

View File

@@ -9,12 +9,19 @@ const {
ProductVariant,
MoneyAmount,
LineItem,
Payment,
Cart,
ShippingMethod,
Swap,
} = require("@medusajs/medusa");
const setupServer = require("../../../helpers/setup-server");
const { useApi } = require("../../../helpers/use-api");
const { initDb } = require("../../../helpers/use-db");
const swapSeeder = require("../../helpers/swap-seeder");
const cartSeeder = require("../../helpers/cart-seeder");
jest.setTimeout(30000);
describe("/store/carts", () => {
@@ -34,6 +41,92 @@ describe("/store/carts", () => {
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", () => {
beforeEach(async () => {
const manager = dbConnection.manager;

View File

@@ -8,9 +8,12 @@ const {
ShippingOption,
ShippingMethod,
Address,
ProductVariant,
Product,
ProductVariant,
MoneyAmount,
LineItem,
Payment,
PaymentSession,
} = require("@medusajs/medusa");
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, {
variant_id: "test-variant",
currency_code: "usd",
amount: 1000,
});
await manager.save(ma);
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",
currency_code: "usd",
amount: 1000,
});
await manager.save(ma2);
await manager.save(ma3);
const cart = manager.create(Cart, {
id: "test-cart",
@@ -219,6 +245,45 @@ module.exports = async (connection, data = {}) => {
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, {
id: "test-method",
shipping_option_id: "test-option",
@@ -226,4 +291,16 @@ module.exports = async (connection, data = {}) => {
price: 1000,
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);
};

View File

@@ -4,11 +4,11 @@
"main": "index.js",
"license": "MIT",
"scripts": {
"test": "jest --runInBand",
"test": "jest --runInBand --silent=false",
"build": "babel src -d dist --extensions \".ts,.js\""
},
"dependencies": {
"@medusajs/medusa": "1.1.33-dev-1627995051381",
"@medusajs/medusa": "1.1.33-dev-1628079986095",
"medusa-interfaces": "^1.1.18",
"typeorm": "^0.2.31"
},
@@ -19,4 +19,4 @@
"babel-preset-medusa-package": "^1.1.11",
"jest": "^26.6.3"
}
}
}

View File

@@ -1265,10 +1265,10 @@
winston "^3.3.3"
yargs "^15.3.1"
"@medusajs/medusa@1.1.33-dev-1627995051381":
version "1.1.33-dev-1627995051381"
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.1.33-dev-1627995051381.tgz#def214374c31daca1f37c2dd5ee55f119236555f"
integrity sha512-yeH/YscYfWn4jYF+YSPyexMgAaDOfjvKm2xH95C+T9w4Ct08z06uBw0G0klIKPatNTirfD7HVgvg2vWb+ChDWA==
"@medusajs/medusa@1.1.33-dev-1628079986095":
version "1.1.33-dev-1628079986095"
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.1.33-dev-1628079986095.tgz#0e8aa3bd83174366d1266823f093f21968b0f6e4"
integrity sha512-zqvz8+NL5+3Ba5uRS6Z2SaXpRTF2r6B+o0HGY8TjkAmvf2pMKyfIOGpBLtnhezDpDb9mkLdiHqrV23lNE+5pAw==
dependencies:
"@hapi/joi" "^16.1.8"
"@medusajs/medusa-cli" "^1.1.14"