feat: In band inventory updates (#311)
Co-authored-by: olivermrbl <oliver@mrbltech.com>
This commit is contained in:
committed by
GitHub
parent
44fce520aa
commit
f07cc0fa40
@@ -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 () => {
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
`;
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"test:fixtures": "jest --config=docs-util/jest.config.js --runInBand"
|
||||
},
|
||||
"dependencies": {
|
||||
"global": "^4.4.0",
|
||||
"import-from": "^3.0.0",
|
||||
"oas-normalize": "^2.3.1",
|
||||
"swagger-inline": "^3.2.2"
|
||||
|
||||
@@ -12,6 +12,11 @@ export const MedusaErrorTypes = {
|
||||
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.
|
||||
* @extends Error
|
||||
@@ -22,19 +27,22 @@ class MedusaError extends Error {
|
||||
* @param type {MedusaErrorType} - the type of error.
|
||||
* @param params {Array} - Error params.
|
||||
*/
|
||||
constructor(name, message, ...params) {
|
||||
constructor(type, message, code, ...params) {
|
||||
super(...params)
|
||||
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, MedusaError)
|
||||
}
|
||||
|
||||
this.name = name
|
||||
this.type = type
|
||||
this.name = type
|
||||
this.code = code
|
||||
this.message = message
|
||||
this.date = new Date()
|
||||
}
|
||||
}
|
||||
|
||||
MedusaError.Types = MedusaErrorTypes
|
||||
MedusaError.Codes = MedusaErrorCodes
|
||||
|
||||
export default MedusaError
|
||||
|
||||
@@ -104,8 +104,8 @@ describe("POST /store/carts/:id", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("Call CartService retrieve 0 times", () => {
|
||||
expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(0)
|
||||
it("Call CartService retrieve 1 time", () => {
|
||||
expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
|
||||
@@ -71,7 +71,20 @@ export default async (req, res) => {
|
||||
const { key, error } = await idempotencyKeyService.workStage(
|
||||
idempotencyKey.idempotency_key,
|
||||
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)
|
||||
.authorizePayment(id, {
|
||||
...req.request_context,
|
||||
@@ -85,7 +98,11 @@ export default async (req, res) => {
|
||||
) {
|
||||
return {
|
||||
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) {
|
||||
case "swap": {
|
||||
const swapId = cart.metadata?.swap_id
|
||||
order = await swapService
|
||||
let swap = await swapService
|
||||
.withTransaction(manager)
|
||||
.registerCartCompletion(swapId)
|
||||
|
||||
order = await swapService
|
||||
swap = await swapService
|
||||
.withTransaction(manager)
|
||||
.retrieve(order.id, { relations: ["shipping_address"] })
|
||||
.retrieve(swap.id, { relations: ["shipping_address"] })
|
||||
|
||||
return {
|
||||
response_code: 200,
|
||||
response_body: { data: order },
|
||||
response_body: { data: swap, type: "swap" },
|
||||
}
|
||||
}
|
||||
// case "payment_link":
|
||||
@@ -168,7 +185,19 @@ export default async (req, res) => {
|
||||
|
||||
return {
|
||||
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 {
|
||||
throw error
|
||||
@@ -192,7 +221,7 @@ export default async (req, res) => {
|
||||
|
||||
return {
|
||||
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)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +260,9 @@ export const CartServiceMock = {
|
||||
if (cartId === IdMap.getId("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")
|
||||
}),
|
||||
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",
|
||||
}
|
||||
|
||||
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 = {
|
||||
one: variant1,
|
||||
two: variant2,
|
||||
@@ -171,17 +211,21 @@ export const ProductVariantServiceMock = {
|
||||
if (variantId === IdMap.getId("testVariant")) {
|
||||
return Promise.resolve(testVariant)
|
||||
}
|
||||
}),
|
||||
canCoverQuantity: jest.fn().mockImplementation((variantId, quantity) => {
|
||||
if (variantId === IdMap.getId("can-cover")) {
|
||||
return Promise.resolve(true)
|
||||
if (variantId === "bo") {
|
||||
return Promise.resolve(outOfStockBackOrder)
|
||||
}
|
||||
|
||||
if (variantId === IdMap.getId("cannot-cover")) {
|
||||
return Promise.resolve(false)
|
||||
if (variantId === "no_bo") {
|
||||
return Promise.resolve(outOfStockNoBackOrder)
|
||||
}
|
||||
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) => {
|
||||
if (variantId === IdMap.getId("eur-10-us-12")) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import _ from "lodash"
|
||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||
import CartService from "../cart"
|
||||
import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
@@ -311,10 +313,18 @@ describe("CartService", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const productVariantService = {
|
||||
canCoverQuantity: jest
|
||||
.fn()
|
||||
.mockImplementation(id => id !== IdMap.getId("cannot-cover")),
|
||||
const inventoryService = {
|
||||
...InventoryServiceMock,
|
||||
confirmInventory: jest.fn().mockImplementation((variantId, _quantity) => {
|
||||
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({
|
||||
@@ -349,9 +359,9 @@ describe("CartService", () => {
|
||||
totalsService,
|
||||
cartRepository,
|
||||
lineItemService,
|
||||
productVariantService,
|
||||
eventBusService,
|
||||
shippingOptionService,
|
||||
inventoryService,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -449,7 +459,11 @@ describe("CartService", () => {
|
||||
|
||||
await expect(
|
||||
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 () => {
|
||||
@@ -463,7 +477,11 @@ describe("CartService", () => {
|
||||
|
||||
await expect(
|
||||
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
|
||||
},
|
||||
}
|
||||
const productVariantService = {
|
||||
canCoverQuantity: jest
|
||||
const inventoryService = {
|
||||
...InventoryServiceMock,
|
||||
confirmInventory: jest
|
||||
.fn()
|
||||
.mockImplementation(id => id !== IdMap.getId("cannot-cover")),
|
||||
}
|
||||
@@ -669,9 +688,9 @@ describe("CartService", () => {
|
||||
manager: MockManager,
|
||||
totalsService,
|
||||
cartRepository,
|
||||
productVariantService,
|
||||
lineItemService,
|
||||
eventBusService,
|
||||
inventoryService,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from "lodash"
|
||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||
import ClaimService from "../claim"
|
||||
import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||
|
||||
const withTransactionMock = jest.fn()
|
||||
const eventBusService = {
|
||||
@@ -73,6 +74,14 @@ describe("ClaimService", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const inventoryService = {
|
||||
...InventoryServiceMock,
|
||||
withTransaction: function() {
|
||||
withTransactionMock("inventory")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const claimItemService = {
|
||||
create: jest.fn(),
|
||||
withTransaction: function() {
|
||||
@@ -88,6 +97,7 @@ describe("ClaimService", () => {
|
||||
returnService,
|
||||
lineItemService,
|
||||
claimItemService,
|
||||
inventoryService,
|
||||
eventBusService,
|
||||
})
|
||||
|
||||
@@ -125,6 +135,18 @@ describe("ClaimService", () => {
|
||||
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(claimItemService.create).toHaveBeenCalledTimes(1)
|
||||
expect(claimItemService.create).toHaveBeenCalledWith({
|
||||
@@ -213,6 +235,24 @@ describe("ClaimService", () => {
|
||||
).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(
|
||||
[
|
||||
[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,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import OrderService from "../order"
|
||||
import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||
|
||||
describe("OrderService", () => {
|
||||
const totalsService = {
|
||||
@@ -37,6 +38,10 @@ describe("OrderService", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const inventoryService = {
|
||||
...InventoryServiceMock,
|
||||
}
|
||||
|
||||
describe("create", () => {
|
||||
const orderRepo = MockRepository({ create: f => f })
|
||||
const orderService = new OrderService({
|
||||
@@ -95,6 +100,9 @@ describe("OrderService", () => {
|
||||
return Promise.resolve(payment.status || "authorized")
|
||||
},
|
||||
updatePayment: jest.fn(),
|
||||
cancelPayment: jest.fn().mockImplementation(payment => {
|
||||
return Promise.resolve({ ...payment, status: "cancelled" })
|
||||
}),
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
@@ -153,6 +161,7 @@ describe("OrderService", () => {
|
||||
regionService,
|
||||
eventBusService,
|
||||
cartService,
|
||||
inventoryService,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -186,7 +195,10 @@ describe("OrderService", () => {
|
||||
gift_cards: [],
|
||||
discounts: [],
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -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).toHaveBeenCalledWith("item_1", {
|
||||
order_id: "id",
|
||||
@@ -272,7 +294,10 @@ describe("OrderService", () => {
|
||||
],
|
||||
discounts: [],
|
||||
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,
|
||||
total: 100,
|
||||
}
|
||||
@@ -361,7 +386,10 @@ describe("OrderService", () => {
|
||||
billing_address_id: "1234",
|
||||
discounts: [],
|
||||
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,
|
||||
}
|
||||
orderService.cartService_.retrieve = () => Promise.resolve(cart)
|
||||
@@ -395,6 +423,45 @@ describe("OrderService", () => {
|
||||
|
||||
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", () => {
|
||||
@@ -545,6 +612,10 @@ describe("OrderService", () => {
|
||||
status: "pending",
|
||||
fulfillments: [{ id: "fulfillment_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,
|
||||
fulfillmentService,
|
||||
eventBusService,
|
||||
inventoryService,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -578,7 +650,15 @@ describe("OrderService", () => {
|
||||
})
|
||||
|
||||
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).toHaveBeenCalledWith({
|
||||
@@ -590,6 +670,16 @@ describe("OrderService", () => {
|
||||
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).toHaveBeenCalledWith({
|
||||
fulfillment_status: "canceled",
|
||||
@@ -597,6 +687,18 @@ describe("OrderService", () => {
|
||||
status: "canceled",
|
||||
fulfillments: [{ id: "fulfillment_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 () => {
|
||||
await productVariantService.update(IdMap.getId("ironman"), {
|
||||
title: "new title",
|
||||
@@ -809,73 +832,4 @@ describe("ProductVariantService", () => {
|
||||
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 from "medusa-test-utils/dist/id-map"
|
||||
import ReturnService from "../return"
|
||||
import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||
|
||||
describe("ReturnService", () => {
|
||||
// describe("requestReturn", () => {
|
||||
@@ -196,11 +198,13 @@ describe("ReturnService", () => {
|
||||
id: IdMap.getId("test-line"),
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
variant_id: "test-variant",
|
||||
},
|
||||
{
|
||||
id: IdMap.getId("test-line-2"),
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
variant_id: "test-variant-2",
|
||||
},
|
||||
],
|
||||
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({
|
||||
manager: MockManager,
|
||||
totalsService,
|
||||
lineItemService,
|
||||
orderService,
|
||||
returnRepository,
|
||||
inventoryService,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -268,6 +289,12 @@ describe("ReturnService", () => {
|
||||
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 () => {
|
||||
@@ -280,6 +307,16 @@ describe("ReturnService", () => {
|
||||
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).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("test-return-2"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||
import SwapService from "../swap"
|
||||
import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
@@ -685,44 +686,51 @@ describe("SwapService", () => {
|
||||
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", () => {
|
||||
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 = {
|
||||
cart: {
|
||||
items: [{ id: "1" }],
|
||||
items: [{ id: "1", variant_id: "variant", quantity: 2 }],
|
||||
shipping_methods: [{ id: "method_1" }],
|
||||
payment: {
|
||||
good: "yes",
|
||||
@@ -744,6 +752,7 @@ describe("SwapService", () => {
|
||||
paymentProviderService,
|
||||
eventBusService,
|
||||
shippingOptionService,
|
||||
inventoryService,
|
||||
})
|
||||
|
||||
it("creates a shipment", async () => {
|
||||
@@ -753,6 +762,15 @@ describe("SwapService", () => {
|
||||
good: "yes",
|
||||
})
|
||||
|
||||
expect(inventoryService.confirmInventory).toHaveBeenCalledWith(
|
||||
"variant",
|
||||
2
|
||||
)
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
|
||||
"variant",
|
||||
-2
|
||||
)
|
||||
|
||||
expect(swapRepo.save).toHaveBeenCalledWith({
|
||||
...existing,
|
||||
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", () => {
|
||||
|
||||
@@ -31,6 +31,7 @@ class CartService extends BaseService {
|
||||
totalsService,
|
||||
addressRepository,
|
||||
paymentSessionRepository,
|
||||
inventoryService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
@@ -84,6 +85,9 @@ class CartService extends BaseService {
|
||||
|
||||
/** @private @const {PaymentSessionRepository} */
|
||||
this.paymentSessionRepository_ = paymentSessionRepository
|
||||
|
||||
/** @private @const {InventoryService} */
|
||||
this.inventoryService_ = inventoryService
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
@@ -109,6 +113,7 @@ class CartService extends BaseService {
|
||||
totalsService: this.totalsService_,
|
||||
addressRepository: this.addressRepository_,
|
||||
giftCardService: this.giftCardService_,
|
||||
inventoryService: this.inventoryService_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
@@ -135,27 +140,6 @@ class CartService extends BaseService {
|
||||
* @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) {
|
||||
let { select, relations } = config
|
||||
|
||||
@@ -454,19 +438,10 @@ class CartService extends BaseService {
|
||||
// simply update the quantity of the existing line item
|
||||
if (currentItem) {
|
||||
const newQuantity = currentItem.quantity + lineItem.quantity
|
||||
|
||||
// Confirm inventory
|
||||
const hasInventory = await this.confirmInventory_(
|
||||
lineItem.variant_id,
|
||||
newQuantity
|
||||
)
|
||||
|
||||
if (!hasInventory) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Inventory doesn't cover the desired quantity"
|
||||
)
|
||||
}
|
||||
// Confirm inventory or throw error
|
||||
await this.inventoryService_
|
||||
.withTransaction(manager)
|
||||
.confirmInventory(lineItem.variant_id, newQuantity)
|
||||
|
||||
await this.lineItemService_
|
||||
.withTransaction(manager)
|
||||
@@ -474,18 +449,10 @@ class CartService extends BaseService {
|
||||
quantity: newQuantity,
|
||||
})
|
||||
} else {
|
||||
// Confirm inventory
|
||||
const hasInventory = await this.confirmInventory_(
|
||||
lineItem.variant_id,
|
||||
lineItem.quantity
|
||||
)
|
||||
|
||||
if (!hasInventory) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Inventory doesn't cover the desired quantity"
|
||||
)
|
||||
}
|
||||
// Confirm inventory or throw error
|
||||
await this.inventoryService_
|
||||
.withTransaction(manager)
|
||||
.confirmInventory(lineItem.variant_id, lineItem.quantity)
|
||||
|
||||
await this.lineItemService_.withTransaction(manager).create({
|
||||
...lineItem,
|
||||
@@ -541,10 +508,9 @@ class CartService extends BaseService {
|
||||
}
|
||||
|
||||
if (lineItemUpdate.quantity) {
|
||||
const hasInventory = await this.confirmInventory_(
|
||||
lineItemExists.variant_id,
|
||||
lineItemUpdate.quantity
|
||||
)
|
||||
const hasInventory = await this.inventoryService_
|
||||
.withTransaction(manager)
|
||||
.confirmInventory(lineItemExists.variant_id, lineItemUpdate.quantity)
|
||||
|
||||
if (!hasInventory) {
|
||||
throw new MedusaError(
|
||||
|
||||
@@ -26,6 +26,7 @@ class ClaimService extends BaseService {
|
||||
shippingOptionService,
|
||||
claimItemService,
|
||||
regionService,
|
||||
inventoryService,
|
||||
eventBusService,
|
||||
}) {
|
||||
super()
|
||||
@@ -63,6 +64,9 @@ class ClaimService extends BaseService {
|
||||
/** @private @constant {TotalsService} */
|
||||
this.totalsService_ = totalsService
|
||||
|
||||
/** @private @constant {InventoryService} */
|
||||
this.inventoryService_ = inventoryService
|
||||
|
||||
/** @private @constant {EventBus} */
|
||||
this.eventBus_ = eventBusService
|
||||
|
||||
@@ -88,6 +92,7 @@ class ClaimService extends BaseService {
|
||||
claimItemService: this.claimItemService_,
|
||||
eventBusService: this.eventBus_,
|
||||
totalsService: this.totalsService_,
|
||||
inventoryService: this.inventoryService_,
|
||||
shippingOptionService: this.shippingOptionService_,
|
||||
})
|
||||
|
||||
@@ -232,6 +237,12 @@ class ClaimService extends BaseService {
|
||||
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(
|
||||
additional_items.map(i =>
|
||||
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 =
|
||||
no_notification !== undefined ? no_notification : order.no_notification
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ class IdempotencyKeyService extends BaseService {
|
||||
*/
|
||||
async workStage(idempotencyKey, func) {
|
||||
try {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return await this.atomicPhase_(async manager => {
|
||||
let key
|
||||
|
||||
const { recovery_point, response_code, response_body } = await func(
|
||||
|
||||
@@ -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
|
||||
@@ -39,6 +39,7 @@ class OrderService extends BaseService {
|
||||
addressRepository,
|
||||
giftCardService,
|
||||
draftOrderService,
|
||||
inventoryService,
|
||||
eventBusService,
|
||||
}) {
|
||||
super()
|
||||
@@ -93,6 +94,9 @@ class OrderService extends BaseService {
|
||||
|
||||
/** @private @constant {DraftOrderService} */
|
||||
this.draftOrderService_ = draftOrderService
|
||||
|
||||
/** @private @constant {InventoryService} */
|
||||
this.inventoryService_ = inventoryService
|
||||
}
|
||||
|
||||
withTransaction(manager) {
|
||||
@@ -118,6 +122,7 @@ class OrderService extends BaseService {
|
||||
giftCardService: this.giftCardService_,
|
||||
addressRepository: this.addressRepository_,
|
||||
draftOrderService: this.draftOrderService_,
|
||||
inventoryService: this.inventoryService_,
|
||||
})
|
||||
|
||||
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)
|
||||
if (exists) {
|
||||
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
|
||||
// total
|
||||
if (total !== 0) {
|
||||
@@ -551,6 +570,12 @@ class OrderService extends BaseService {
|
||||
.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_
|
||||
.withTransaction(manager)
|
||||
.emit(OrderService.Events.PLACED, {
|
||||
@@ -864,7 +889,7 @@ class OrderService extends BaseService {
|
||||
async cancel(orderId) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const order = await this.retrieve(orderId, {
|
||||
relations: ["fulfillments", "payments"],
|
||||
relations: ["fulfillments", "payments", "items"],
|
||||
})
|
||||
|
||||
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) {
|
||||
await this.paymentProviderService_
|
||||
.withTransaction(manager)
|
||||
|
||||
@@ -253,6 +253,7 @@ class PaymentProviderService extends BaseService {
|
||||
amount: total,
|
||||
currency_code: region.currency_code,
|
||||
data: paymentData,
|
||||
cart_id: cart.id,
|
||||
})
|
||||
|
||||
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) {
|
||||
for (const price of prices) {
|
||||
@@ -291,11 +291,16 @@ class ProductVariantService extends BaseService {
|
||||
variant.metadata = this.setMetadata_(variant, metadata)
|
||||
}
|
||||
|
||||
if (typeof inventory_quantity === "number") {
|
||||
variant.inventory_quantity = inventory_quantity
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(rest)) {
|
||||
variant[key] = value
|
||||
}
|
||||
|
||||
const result = await variantRepo.save(variant)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.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
|
||||
* @return {Promise} the result of the find operation
|
||||
|
||||
@@ -16,6 +16,7 @@ class ReturnService extends BaseService {
|
||||
shippingOptionService,
|
||||
returnReasonService,
|
||||
fulfillmentProviderService,
|
||||
inventoryService,
|
||||
orderService,
|
||||
}) {
|
||||
super()
|
||||
@@ -43,6 +44,8 @@ class ReturnService extends BaseService {
|
||||
|
||||
this.returnReasonService_ = returnReasonService
|
||||
|
||||
this.inventoryService_ = inventoryService
|
||||
|
||||
/** @private @const {OrderService} */
|
||||
this.orderService_ = orderService
|
||||
}
|
||||
@@ -61,6 +64,7 @@ class ReturnService extends BaseService {
|
||||
shippingOptionService: this.shippingOptionService_,
|
||||
fulfillmentProviderService: this.fulfillmentProviderService_,
|
||||
returnReasonService: this.returnReasonService_,
|
||||
inventoryService: this.inventoryService_,
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class SwapService extends BaseService {
|
||||
shippingOptionService,
|
||||
fulfillmentService,
|
||||
orderService,
|
||||
inventoryService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
@@ -64,6 +65,9 @@ class SwapService extends BaseService {
|
||||
/** @private @const {ShippingOptionService} */
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
|
||||
/** @private @const {InventoryService} */
|
||||
this.inventoryService_ = inventoryService
|
||||
|
||||
/** @private @const {EventBusService} */
|
||||
this.eventBus_ = eventBusService
|
||||
}
|
||||
@@ -84,6 +88,7 @@ class SwapService extends BaseService {
|
||||
paymentProviderService: this.paymentProviderService_,
|
||||
shippingOptionService: this.shippingOptionService_,
|
||||
orderService: this.orderService_,
|
||||
inventoryService: this.inventoryService_,
|
||||
fulfillmentService: this.fulfillmentService_,
|
||||
})
|
||||
|
||||
@@ -621,6 +626,14 @@ class SwapService extends BaseService {
|
||||
|
||||
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)
|
||||
|
||||
if (total > 0) {
|
||||
@@ -651,6 +664,12 @@ class SwapService extends BaseService {
|
||||
swap_id: swapId,
|
||||
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()
|
||||
|
||||
+548
-41
File diff suppressed because it is too large
Load Diff
@@ -4296,6 +4296,11 @@ dir-glob@^3.0.1:
|
||||
dependencies:
|
||||
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:
|
||||
version "2.0.1"
|
||||
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"
|
||||
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:
|
||||
version "11.12.0"
|
||||
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"
|
||||
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:
|
||||
version "3.0.4"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||
|
||||
Reference in New Issue
Block a user