adds endpoints for draft orders

This commit is contained in:
olivermrbl
2021-02-16 16:53:55 +01:00
parent 75bc1f67f4
commit 362c5c6055
31 changed files with 1871 additions and 251 deletions
@@ -0,0 +1,344 @@
const { dropDatabase } = require("pg-god");
const path = require("path");
const setupServer = require("../../../helpers/setup-server");
const { useApi } = require("../../../helpers/use-api");
const { initDb } = require("../../../helpers/use-db");
const draftOrderSeeder = require("../../helpers/draft-order-seeder");
const adminSeeder = require("../../helpers/admin-seeder");
jest.setTimeout(30000);
describe("/admin/draft-orders", () => {
let medusaProcess;
let dbConnection;
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."));
dbConnection = await initDb({ cwd });
medusaProcess = await setupServer({ cwd });
});
afterAll(async () => {
await dbConnection.close();
await dropDatabase({ databaseName: "medusa-integration" });
medusaProcess.kill();
});
describe("POST /admin/draft-orders", () => {
beforeEach(async () => {
try {
await adminSeeder(dbConnection);
await draftOrderSeeder(dbConnection);
} catch (err) {
console.log(err);
throw err;
}
});
afterEach(async () => {
const manager = dbConnection.manager;
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_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 "draft_order" SET order_id=NULL`);
await manager.query(`DELETE FROM "draft_order"`);
await manager.query(`DELETE FROM "cart"`);
await manager.query(`DELETE FROM "payment"`);
await manager.query(`DELETE FROM "customer"`);
await manager.query(`DELETE FROM "address"`);
await manager.query(
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'`
);
await manager.query(
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'de'`
);
await manager.query(`DELETE FROM "region"`);
await manager.query(`DELETE FROM "user"`);
});
it("creates a draft order cart", async () => {
const api = useApi();
const payload = {
email: "oli@test.dk",
shipping_address_id: "oli-shipping",
items: [
{
variant_id: "test-variant",
quantity: 2,
metadata: {},
},
],
region_id: "test-region",
customer_id: "oli-test",
shipping_methods: [
{
option_id: "test-option",
},
],
};
const response = await api
.post("/admin/draft-orders", payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err);
});
expect(response.status).toEqual(200);
});
it("creates a draft order with custom item", async () => {
const api = useApi();
const payload = {
email: "oli@test.dk",
shipping_address_id: "oli-shipping",
items: [
{
variant_id: "test-variant",
quantity: 2,
metadata: {},
},
{
quantity: 1,
metadata: {},
unit_price: 10000,
},
],
region_id: "test-region",
customer_id: "oli-test",
shipping_methods: [
{
option_id: "test-option",
},
],
};
const response = await api
.post("/admin/draft-orders", payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err);
});
expect(response.status).toEqual(200);
});
it("creates a draft order with created shipping address", async () => {
const api = useApi();
const payload = {
email: "oli@test.dk",
shipping_address: {
first_name: "new",
last_name: "one",
address_1: "New place 1",
city: "Copenhagen",
country_code: "us",
postal_code: "2100",
},
items: [
{
variant_id: "test-variant",
quantity: 2,
metadata: {},
},
{
quantity: 1,
metadata: {},
unit_price: 10000,
},
],
region_id: "test-region",
customer_id: "oli-test",
shipping_methods: [
{
option_id: "test-option",
},
],
};
const response = await api
.post("/admin/draft-orders", payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err);
});
expect(response.status).toEqual(200);
});
it("creates a draft order and registers manual payment", async () => {
const api = useApi();
// register system payment for draft order
const orderResponse = await api.post(
`/admin/draft-orders/test-draft-order/register-payment`,
{},
{
headers: {
Authorization: "Bearer test_token",
},
}
);
const createdOrder = await api.get(
`/admin/orders/${orderResponse.data.order.id}`,
{
headers: {
Authorization: "Bearer test_token",
},
}
);
const updatedDraftOrder = await api.get(
`/admin/draft-orders/test-draft-order`,
{
headers: {
Authorization: "Bearer test_token",
},
}
);
expect(orderResponse.status).toEqual(200);
// expect newly created order to have id of draft order and system payment
expect(createdOrder.data.order.draft_order_id).toEqual(
"test-draft-order"
);
expect(createdOrder.data.order.payments).toEqual(
expect.arrayContaining([
expect.objectContaining({ provider_id: "system" }),
])
);
// expect draft order to be complete
expect(updatedDraftOrder.data.draft_order.status).toEqual("completed");
});
});
describe("GET /admin/draft-orders", () => {
beforeEach(async () => {
try {
await adminSeeder(dbConnection);
await draftOrderSeeder(dbConnection);
} catch (err) {
console.log(err);
throw err;
}
});
afterEach(async () => {
const manager = dbConnection.manager;
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_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 "draft_order" SET order_id=NULL`);
await manager.query(`DELETE FROM "draft_order"`);
await manager.query(`DELETE FROM "cart"`);
await manager.query(`DELETE FROM "payment"`);
await manager.query(`DELETE FROM "customer"`);
await manager.query(`DELETE FROM "address"`);
await manager.query(
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'`
);
await manager.query(
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'de'`
);
await manager.query(`DELETE FROM "region"`);
await manager.query(`DELETE FROM "user"`);
});
it("lists draft orders", async () => {
const api = useApi();
const response = await api
.get("/admin/draft-orders", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err);
});
expect(response.status).toEqual(200);
expect(response.data.draft_orders).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: "test-draft-order" }),
])
);
});
it("lists draft orders with query", async () => {
const api = useApi();
const response = await api
.get("/admin/draft-orders?q=oli@test", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err);
});
expect(response.status).toEqual(200);
expect(response.data.draft_orders).toEqual(
expect.arrayContaining([
expect.objectContaining({
cart: expect.objectContaining({ email: "oli@test.dk" }),
}),
])
);
});
it("lists no draft orders on query for non-existing email", async () => {
const api = useApi();
const response = await api
.get("/admin/draft-orders?q=heyo@heyo.dk", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err);
});
expect(response.status).toEqual(200);
console.log(response.data.draft_orders);
expect(response.data.draft_orders).toEqual([]);
expect(response.data.count).toEqual(0);
});
});
});
@@ -0,0 +1,88 @@
const { dropDatabase } = require("pg-god");
const path = require("path");
const setupServer = require("../../../helpers/setup-server");
const { useApi } = require("../../../helpers/use-api");
const { initDb } = require("../../../helpers/use-db");
const draftOrderSeeder = require("../../helpers/draft-order-seeder");
const { create } = require("domain");
jest.setTimeout(30000);
describe("/store/carts (draft-orders)", () => {
let medusaProcess;
let dbConnection;
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."));
dbConnection = await initDb({ cwd });
medusaProcess = await setupServer({ cwd });
});
afterAll(async () => {
await dbConnection.close();
await dropDatabase({ databaseName: "medusa-integration" });
medusaProcess.kill();
});
describe("POST /admin/draft-order", () => {
beforeEach(async () => {
try {
await draftOrderSeeder(dbConnection);
} catch (err) {
console.log(err);
throw err;
}
});
afterEach(async () => {
const manager = dbConnection.manager;
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 "draft_order"`);
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(`DELETE FROM "cart"`);
await manager.query(`DELETE FROM "payment"`);
await manager.query(`DELETE FROM "customer"`);
await manager.query(`DELETE FROM "address"`);
await manager.query(
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'`
);
await manager.query(
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'de'`
);
await manager.query(`DELETE FROM "region"`);
});
it("completes a cart for a draft order thereby creating an order for the draft order", async () => {
const api = useApi();
const response = await api
.post("/store/carts/test-cart/complete-cart", {})
.catch((err) => {
console.log(err);
});
expect(response.status).toEqual(200);
const createdOrder = await api
.get(`/store/orders/${response.data.data.id}`, {})
.catch((err) => {
console.log(err);
});
expect(createdOrder.data.order.cart_id).toEqual("test-cart");
});
});
});
@@ -0,0 +1,200 @@
const {
ShippingProfile,
Customer,
MoneyAmount,
ShippingOption,
Product,
ProductVariant,
Region,
Address,
Cart,
PaymentSession,
DraftOrder,
} = require("@medusajs/medusa");
module.exports = async (connection, data = {}) => {
const manager = connection.manager;
const defaultProfile = await manager.findOne(ShippingProfile, {
type: "default",
});
await manager.insert(Product, {
id: "test-product",
title: "test product",
profile_id: defaultProfile.id,
options: [{ id: "test-option", title: "Size" }],
});
await manager.insert(Address, {
id: "oli-shipping",
first_name: "oli",
last_name: "test",
country_code: "us",
});
await manager.insert(Product, {
id: "test-product-2",
title: "test product 2",
profile_id: defaultProfile.id,
options: [{ id: "test-option-color", title: "Color" }],
});
await manager.insert(ProductVariant, {
id: "test-variant",
title: "test variant",
product_id: "test-product",
inventory_quantity: 1,
options: [
{
option_id: "test-option",
value: "Size",
},
],
});
await manager.insert(ProductVariant, {
id: "test-variant-2",
title: "test variant-2",
product_id: "test-product-2",
inventory_quantity: 4,
options: [
{
option_id: "test-option-color",
value: "Color",
},
],
});
const ma = manager.create(MoneyAmount, {
variant_id: "test-variant",
currency_code: "usd",
amount: 8000,
});
await manager.save(ma);
const ma2 = manager.create(MoneyAmount, {
variant_id: "test-variant-2",
currency_code: "usd",
amount: 10000,
});
await manager.save(ma2);
await manager.insert(Region, {
id: "test-region",
name: "Test Region",
currency_code: "usd",
tax_rate: 0,
payment_providers: [
{
id: "test-pay",
is_installed: true,
},
],
});
await manager.insert(Region, {
id: "test-region-2",
name: "Test Region 2",
currency_code: "eur",
tax_rate: 0,
payment_providers: [
{
id: "test-pay",
is_installed: true,
},
],
});
await manager.query(
`UPDATE "country" SET region_id='test-region' WHERE iso_2 = 'us'`
);
await manager.query(
`UPDATE "country" SET region_id='test-region-2' WHERE iso_2 = 'de'`
);
await manager.insert(Customer, {
id: "oli-test",
email: "oli@test.dk",
});
await manager.insert(Customer, {
id: "lebron-james",
email: "lebron@james.com",
});
await manager.insert(ShippingOption, {
id: "test-option",
name: "test-option",
provider_id: "test-ful",
region_id: "test-region",
profile_id: defaultProfile.id,
price_type: "flat_rate",
amount: 1000,
data: {},
});
const c = manager.create(Cart, {
id: "test-cart",
customer_id: "oli-test",
email: "oli@test.dk",
shipping_address_id: "oli-shipping",
region_id: "test-region",
currency_code: "usd",
payment_sessions: [],
items: [
{
id: "test-item",
fulfilled_quantity: 1,
title: "Line Item",
description: "Line Item Desc",
thumbnail: "https://test.js/1234",
unit_price: 8000,
quantity: 1,
variant_id: "test-variant",
},
],
type: "draft_order",
metadata: { draft_order_id: "test-draft-order" },
});
await manager.save(c);
await manager.insert(PaymentSession, {
id: "test-session",
cart_id: "test-cart",
provider_id: "test-pay",
is_selected: true,
data: {},
status: "pending",
});
// await manager.save(cart);
const draftOrder = manager.create(DraftOrder, {
id: "test-draft-order",
status: "awaiting",
display_id: 4,
cart_id: "test-cart",
customer_id: "oli-test",
items: [
{
id: "test-item",
fulfilled_quantity: 1,
title: "Line Item",
description: "Line Item Desc",
thumbnail: "https://test.js/1234",
unit_price: 8000,
quantity: 1,
variant_id: "test-variant",
},
],
email: "oli@test.dk",
region_id: "test-region",
discounts: [],
});
await manager.save(draftOrder);
};
@@ -28,7 +28,7 @@ class TestPayService extends PaymentService {
} }
async authorizePayment(sessionData, context = {}) { async authorizePayment(sessionData, context = {}) {
return {}; return { data: {}, status: "authorized" };
} }
async updatePaymentData(sessionData, update) { async updatePaymentData(sessionData, update) {
@@ -3,7 +3,7 @@ export default async (req, res) => {
try { try {
const customerService = req.scope.resolve("customerService") const customerService = req.scope.resolve("customerService")
const customer = await customerService.retrieve(id, { const customer = await customerService.retrieve(id, {
relations: ["orders"], relations: ["orders", "shipping_addresses"],
}) })
res.json({ customer }) res.json({ customer })
@@ -5,13 +5,19 @@ export default async (req, res) => {
const limit = parseInt(req.query.limit) || 10 const limit = parseInt(req.query.limit) || 10
const offset = parseInt(req.query.offset) || 0 const offset = parseInt(req.query.offset) || 0
const selector = {}
if ("q" in req.query) {
selector.q = req.query.q
}
const listConfig = { const listConfig = {
relations: [], relations: [],
skip: offset, skip: offset,
take: limit, take: limit,
} }
const customers = await customerService.list({}, listConfig) const customers = await customerService.list(selector, listConfig)
res.json({ customers, count: customers.length, offset, limit }) res.json({ customers, count: customers.length, offset, limit })
} catch (error) { } catch (error) {
@@ -0,0 +1,73 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultFields, defaultRelations } from "."
export default async (req, res) => {
const schema = Validator.object().keys({
status: Validator.string()
.valid("open", "awaiting", "completed")
.optional(),
email: Validator.string()
.email()
.required(),
billing_address: Validator.address().optional(),
shipping_address: Validator.address().optional(),
billing_address_id: Validator.string().optional(),
shipping_address_id: Validator.string().optional(),
items: Validator.array()
.items({
variant_id: Validator.string().optional(),
unit_price: Validator.number().optional(),
title: Validator.string().optional(),
quantity: Validator.number().required(),
metadata: Validator.object().default({}),
})
.required(),
region_id: Validator.string().required(),
discounts: Validator.array().optional(),
customer_id: Validator.string().optional(),
customer: Validator.string().optional(),
shipping_methods: Validator.array()
.items({
option_id: Validator.string().required(),
data: Validator.object().optional(),
price: Validator.number()
.integer()
.integer()
.allow(0)
.optional(),
})
.required(),
metadata: Validator.object().optional(),
requires_shipping: Validator.boolean().default(true),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const draftOrderService = req.scope.resolve("draftOrderService")
const entityManager = req.scope.resolve("manager")
await entityManager.transaction(async manager => {
const requiresShipping = value.requires_shipping
delete value.requires_shipping
let draftOrder = await draftOrderService
.withTransaction(manager)
.create(value, requiresShipping)
draftOrder = await draftOrderService
.withTransaction(manager)
.retrieve(draftOrder.id, {
relations: defaultRelations,
select: defaultFields,
})
res.status(200).json({ draft_order: draftOrder })
})
} catch (err) {
throw err
}
}
@@ -0,0 +1,76 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultCartFields, defaultCartRelations, defaultFields } from "."
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
title: Validator.string().optional(),
unit_price: Validator.number().optional(),
variant_id: Validator.string().optional(),
quantity: Validator.number().required(),
metadata: Validator.object().optional(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const draftOrderService = req.scope.resolve("draftOrderService")
const cartService = req.scope.resolve("cartService")
const lineItemService = req.scope.resolve("lineItemService")
const entityManager = req.scope.resolve("manager")
await entityManager.transaction(async manager => {
const draftOrder = await draftOrderService
.withTransaction(manager)
.retrieve(id, { select: defaultFields, relations: ["cart"] })
if (
draftOrder.status === "completed" ||
draftOrder.status === "awaiting"
) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"You are only allowed to update open draft orders"
)
}
if (value.variant_id) {
const line = await lineItemService.generate(
value.variant_id,
draftOrder.cart.region_id,
value.quantity,
value.metadata
)
await cartService
.withTransaction(manager)
.addLineItem(draftOrder.cart_id, line)
} else {
// custom line items can be added to a draft order
await lineItemService.withTransaction(manager).create({
cart_id: draftOrder.cart_id,
has_shipping: true,
title: value.title || "Custom item",
allow_discounts: false,
unit_price: value.unit_price || 0,
quantity: value.quantity,
})
}
draftOrder.cart = await cartService
.withTransaction(manager)
.retrieve(draftOrder.cart_id, {
relations: defaultCartRelations,
select: defaultCartFields,
})
res.status(200).json({ draft_order: draftOrder })
})
} catch (err) {
throw err
}
}
@@ -0,0 +1,43 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultCartFields, defaultCartRelations, defaultFields } from "."
export default async (req, res) => {
const { id, line_id } = req.params
try {
const draftOrderService = req.scope.resolve("draftOrderService")
const cartService = req.scope.resolve("cartService")
const entityManager = req.scope.resolve("manager")
await entityManager.transaction(async manager => {
const draftOrder = await draftOrderService
.withTransaction(manager)
.retrieve(id, { select: defaultFields })
if (
draftOrder.status === "completed" ||
draftOrder.status === "awaiting"
) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"You are only allowed to update open draft orders"
)
}
await cartService
.withTransaction(manager)
.removeLineItem(draftOrder.cart_id, line_id)
draftOrder.cart = await cartService
.withTransaction(manager)
.retrieve(draftOrder.cart_id, {
relations: defaultCartRelations,
select: defaultCartFields,
})
res.status(200).json({ draft_order: draftOrder })
})
} catch (err) {
throw err
}
}
@@ -0,0 +1,44 @@
import { defaultRelations, defaultFields } from "."
export default async (req, res) => {
const { id } = req.params
try {
const draftOrderService = req.scope.resolve("draftOrderService")
const cartService = req.scope.resolve("cartService")
const draftOrder = await draftOrderService.retrieve(id, {
select: defaultFields,
relations: defaultRelations,
})
draftOrder.cart = await cartService.retrieve(draftOrder.cart_id, {
relations: [
"gift_cards",
"region",
"items",
"payment",
"shipping_address",
"billing_address",
"region.countries",
"region.payment_providers",
"shipping_methods",
"payment_sessions",
"shipping_methods.shipping_option",
"discounts",
],
select: [
"subtotal",
"tax_total",
"shipping_total",
"discount_total",
"gift_card_total",
"total",
],
})
res.json({ draft_order: draftOrder })
} catch (error) {
throw error
}
}
@@ -0,0 +1,21 @@
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const { id } = req.params
try {
const draftOrderService = req.scope.resolve("draftOrderService")
const shippingProfileService = req.scope.resolve("shippingProfileService")
const draftOrder = await cartService.retrieve(value.cart_id, {
select: ["subtotal"],
relations: ["region", "items", "items.variant", "items.variant.product"],
})
const options = await shippingProfileService.fetchCartOptions(cart)
res.status(200).json({ shipping_options: options })
} catch (err) {
throw err
}
}
@@ -0,0 +1,85 @@
import { Router } from "express"
import middlewares from "../../../middlewares"
const route = Router()
export default app => {
app.use("/draft-orders", route)
route.get("/", middlewares.wrap(require("./list-draft-orders").default))
route.get("/:id", middlewares.wrap(require("./get-draft-order").default))
route.post("/", middlewares.wrap(require("./create-draft-order").default))
route.delete(
"/:id/line-items/:line_id",
middlewares.wrap(require("./delete-line-item").default)
)
route.post(
"/:id/line-items",
middlewares.wrap(require("./create-line-item").default)
)
route.post("/", middlewares.wrap(require("./create-draft-order").default))
route.post(
"/:id/register-payment",
middlewares.wrap(require("./register-payment").default)
)
return app
}
export const defaultRelations = []
export const defaultCartRelations = [
"region",
"items",
"payment",
"shipping_address",
"billing_address",
"region.payment_providers",
"shipping_methods",
"payment_sessions",
"shipping_methods.shipping_option",
"discounts",
]
export const defaultCartFields = [
"region",
"items",
"payment",
"shipping_address",
"billing_address",
"region.payment_providers",
"shipping_methods",
"payment_sessions",
"shipping_methods.shipping_option",
"discounts",
]
export const defaultFields = [
"id",
"status",
"display_id",
"cart_id",
"canceled_at",
"created_at",
"updated_at",
"metadata",
]
export const allowedFields = [
"id",
"status",
"display_id",
"cart_id",
"canceled_at",
"created_at",
"updated_at",
"metadata",
]
export const allowedRelations = ["cart"]
@@ -0,0 +1,34 @@
import _ from "lodash"
import { defaultRelations, defaultFields } from "./"
export default async (req, res) => {
try {
const draftOrderService = req.scope.resolve("draftOrderService")
const limit = parseInt(req.query.limit) || 50
const offset = parseInt(req.query.offset) || 0
let selector = {}
if ("q" in req.query) {
selector.q = req.query.q
}
const listConfig = {
select: defaultFields,
relations: defaultRelations,
skip: offset,
take: limit,
order: { created_at: "DESC" },
}
const [draftOrders, count] = await draftOrderService.listAndCount(
selector,
listConfig
)
res.json({ draft_orders: draftOrders, count, offset, limit })
} catch (error) {
throw error
}
}
@@ -0,0 +1,24 @@
import {
defaultFields as defaultOrderFields,
defaultRelations as defaultOrderRelations,
} from "../orders/index"
export default async (req, res) => {
const { id } = req.params
try {
const draftOrderService = req.scope.resolve("draftOrderService")
const orderService = req.scope.resolve("orderService")
const createdOrder = await draftOrderService.registerSystemPayment(id)
const order = await orderService.retrieve(createdOrder.id, {
relations: defaultOrderRelations,
select: defaultOrderFields,
})
res.status(200).json({ order })
} catch (err) {
throw err
}
}
@@ -0,0 +1,65 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultCartFields, defaultCartRelations, defaultFields } from "."
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
region_id: Validator.string().optional(),
country_code: Validator.string().optional(),
email: Validator.string()
.email()
.optional(),
billing_address: Validator.object().optional(),
shipping_address: Validator.object().optional(),
discounts: Validator.array()
.items({
code: Validator.string(),
})
.optional(),
customer_id: Validator.string().optional(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const draftOrderService = req.scope.resolve("draftOrderService")
const cartService = req.scope.resolve("cartService")
const entityManager = req.scope.resolve("manager")
await entityManager.transaction(async manager => {
const draftOrder = await draftOrderService
.withTransaction(manager)
.retrieve(id, { select: defaultFields })
if (
draftOrder.status === "completed" ||
draftOrder.status === "awaiting"
) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"You are only allowed to update open draft orders"
)
}
await cartService
.withTransaction(manager)
.update(draftOrder.cart_id, value)
draftOrder.cart = await cartService
.withTransaction(manager)
.retrieve(draftOrder.cart_id, {
relations: defaultCartRelations,
select: defaultCartFields,
})
res.status(200).json({ draft_order: draftOrder })
})
} catch (err) {
throw err
}
}
@@ -18,6 +18,7 @@ import appRoutes from "./apps"
import swapRoutes from "./swaps" import swapRoutes from "./swaps"
import returnRoutes from "./returns" import returnRoutes from "./returns"
import variantRoutes from "./variants" import variantRoutes from "./variants"
import draftOrderRoutes from "./draft-orders"
const route = Router() const route = Router()
@@ -60,6 +61,7 @@ export default (app, container, config) => {
swapRoutes(route) swapRoutes(route)
returnRoutes(route) returnRoutes(route)
variantRoutes(route) variantRoutes(route)
draftOrderRoutes(route)
return app return app
} }
@@ -21,7 +21,6 @@ const defaultRelations = [
"claims.fulfillments", "claims.fulfillments",
"claims.claim_items", "claims.claim_items",
"claims.claim_items.images", "claims.claim_items.images",
"claims.claim_items.tags",
"swaps", "swaps",
"swaps.return_order", "swaps.return_order",
"swaps.payment", "swaps.payment",
@@ -38,6 +37,7 @@ const defaultFields = [
"payment_status", "payment_status",
"display_id", "display_id",
"cart_id", "cart_id",
"draft_order_id",
"customer_id", "customer_id",
"email", "email",
"region_id", "region_id",
@@ -208,6 +208,7 @@ export const defaultFields = [
"payment_status", "payment_status",
"display_id", "display_id",
"cart_id", "cart_id",
"draft_order_id",
"customer_id", "customer_id",
"email", "email",
"region_id", "region_id",
@@ -235,6 +236,7 @@ export const allowedFields = [
"payment_status", "payment_status",
"display_id", "display_id",
"cart_id", "cart_id",
"draft_order_id",
"customer_id", "customer_id",
"email", "email",
"region_id", "region_id",
+1
View File
@@ -37,3 +37,4 @@ export { StagedJob } from "./models/staged-job"
export { Store } from "./models/store" export { Store } from "./models/store"
export { Swap } from "./models/swap" export { Swap } from "./models/swap"
export { User } from "./models/user" export { User } from "./models/user"
export { DraftOrder } from "./models/draft-order"
@@ -0,0 +1,46 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class draftOrders1613384784316 implements MigrationInterface {
name = 'draftOrders1613384784316'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "draft_order_status_enum" AS ENUM('open', 'awaiting', 'completed')`);
await queryRunner.query(`CREATE TABLE "draft_order" ("id" character varying NOT NULL, "status" "draft_order_status_enum" NOT NULL DEFAULT 'open', "display_id" SERIAL NOT NULL, "cart_id" character varying, "order_id" character varying, "canceled_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, "idempotency_key" character varying, CONSTRAINT "REL_5bd11d0e2a9628128e2c26fd0a" UNIQUE ("cart_id"), CONSTRAINT "REL_8f6dd6c49202f1466ebf21e77d" UNIQUE ("order_id"), CONSTRAINT "PK_f478946c183d98f8d88a94cfcd7" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_e87cc617a22ef4edce5601edab" ON "draft_order" ("display_id") `);
await queryRunner.query(`CREATE INDEX "IDX_5bd11d0e2a9628128e2c26fd0a" ON "draft_order" ("cart_id") `);
await queryRunner.query(`CREATE INDEX "IDX_8f6dd6c49202f1466ebf21e77d" ON "draft_order" ("order_id") `);
await queryRunner.query(`ALTER TABLE "order" ADD "draft_order_id" character varying`);
await queryRunner.query(`ALTER TABLE "order" ADD CONSTRAINT "UQ_727b872f86c7378474a8fa46147" UNIQUE ("draft_order_id")`);
await queryRunner.query(`ALTER TYPE "public"."cart_type_enum" RENAME TO "cart_type_enum_old"`);
await queryRunner.query(`CREATE TYPE "cart_type_enum" AS ENUM('default', 'swap', 'draft_order', 'payment_link')`);
await queryRunner.query(`ALTER TABLE "cart" ALTER COLUMN "type" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "cart" ALTER COLUMN "type" TYPE "cart_type_enum" USING "type"::"text"::"cart_type_enum"`);
await queryRunner.query(`ALTER TABLE "cart" ALTER COLUMN "type" SET DEFAULT 'default'`);
await queryRunner.query(`DROP TYPE "cart_type_enum_old"`);
await queryRunner.query(`COMMENT ON COLUMN "cart"."type" IS NULL`);
await queryRunner.query(`ALTER TABLE "draft_order" ADD CONSTRAINT "FK_5bd11d0e2a9628128e2c26fd0a6" FOREIGN KEY ("cart_id") REFERENCES "cart"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "draft_order" ADD CONSTRAINT "FK_8f6dd6c49202f1466ebf21e77da" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "order" ADD CONSTRAINT "FK_727b872f86c7378474a8fa46147" FOREIGN KEY ("draft_order_id") REFERENCES "draft_order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "order" DROP CONSTRAINT "FK_727b872f86c7378474a8fa46147"`);
await queryRunner.query(`ALTER TABLE "draft_order" DROP CONSTRAINT "FK_8f6dd6c49202f1466ebf21e77da"`);
await queryRunner.query(`ALTER TABLE "draft_order" DROP CONSTRAINT "FK_5bd11d0e2a9628128e2c26fd0a6"`);
await queryRunner.query(`COMMENT ON COLUMN "cart"."type" IS NULL`);
await queryRunner.query(`CREATE TYPE "cart_type_enum_old" AS ENUM('default', 'swap', 'payment_link')`);
await queryRunner.query(`ALTER TABLE "cart" ALTER COLUMN "type" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "cart" ALTER COLUMN "type" TYPE "cart_type_enum_old" USING "type"::"text"::"cart_type_enum_old"`);
await queryRunner.query(`ALTER TABLE "cart" ALTER COLUMN "type" SET DEFAULT 'default'`);
await queryRunner.query(`DROP TYPE "cart_type_enum"`);
await queryRunner.query(`ALTER TYPE "cart_type_enum_old" RENAME TO "cart_type_enum"`);
await queryRunner.query(`ALTER TABLE "order" DROP CONSTRAINT "UQ_727b872f86c7378474a8fa46147"`);
await queryRunner.query(`ALTER TABLE "order" DROP COLUMN "draft_order_id"`);
await queryRunner.query(`DROP INDEX "IDX_8f6dd6c49202f1466ebf21e77d"`);
await queryRunner.query(`DROP INDEX "IDX_5bd11d0e2a9628128e2c26fd0a"`);
await queryRunner.query(`DROP INDEX "IDX_e87cc617a22ef4edce5601edab"`);
await queryRunner.query(`DROP TABLE "draft_order"`);
await queryRunner.query(`DROP TYPE "draft_order_status_enum"`);
}
}
+1
View File
@@ -32,6 +32,7 @@ import { ShippingMethod } from "./shipping-method"
export enum CartType { export enum CartType {
DEFAULT = "default", DEFAULT = "default",
SWAP = "swap", SWAP = "swap",
DRAFT_ORDER = "draft_order",
PAYMENT_LINK = "payment_link", PAYMENT_LINK = "payment_link",
} }
-70
View File
@@ -61,76 +61,6 @@ export class DraftOrder {
@JoinColumn({ name: "order_id" }) @JoinColumn({ name: "order_id" })
order: Order order: Order
@Index()
@Column()
customer_id: string
@ManyToOne(() => Customer, { cascade: ["insert"] })
@JoinColumn({ name: "customer_id" })
customer: Customer
@OneToMany(
() => LineItem,
lineItem => lineItem.draft_order,
{ cascade: ["insert", "remove"] }
)
items: LineItem[]
@Column()
email: string
@Index()
@Column({ nullable: true })
billing_address_id: string
@ManyToOne(() => Address, { cascade: ["insert"] })
@JoinColumn({ name: "billing_address_id" })
billing_address: Address
@Index()
@Column({ nullable: true })
shipping_address_id: string
@ManyToOne(() => Address, { cascade: ["insert"] })
@JoinColumn({ name: "shipping_address_id" })
shipping_address: Address
@Index()
@Column()
region_id: string
@ManyToOne(() => Region)
@JoinColumn({ name: "region_id" })
region: Region
@ManyToMany(() => Discount, { cascade: ["insert"] })
@JoinTable({
name: "draft_order_discounts",
joinColumn: {
name: "draft_order_id",
referencedColumnName: "id",
},
inverseJoinColumn: {
name: "discount_id",
referencedColumnName: "id",
},
})
discounts: Discount[]
@OneToMany(
() => ShippingMethod,
m => m.draft_order,
{ cascade: ["soft-remove", "remove"] }
)
shipping_methods: ShippingMethod[]
@OneToMany(
() => Payment,
p => p.draft_order,
{ cascade: ["insert"] }
)
payments: Payment[]
@Column({ nullable: true, type: "timestamptz" }) @Column({ nullable: true, type: "timestamptz" })
canceled_at: Date canceled_at: Date
-11
View File
@@ -50,17 +50,6 @@ export class LineItem {
@JoinColumn({ name: "order_id" }) @JoinColumn({ name: "order_id" })
order: Order order: Order
@Index()
@Column({ nullable: true })
draft_order_id: string
@ManyToOne(
() => DraftOrder,
dorder => dorder.items
)
@JoinColumn({ name: "draft_order_id" })
draft_order: DraftOrder
@Index() @Index()
@Column({ nullable: true }) @Column({ nullable: true })
swap_id: string swap_id: string
-8
View File
@@ -50,14 +50,6 @@ export class Payment {
@JoinColumn({ name: "order_id" }) @JoinColumn({ name: "order_id" })
order: Order order: Order
@Index()
@Column({ nullable: true })
draft_order_id: string
@OneToOne(() => DraftOrder)
@JoinColumn({ name: "draft_order_id" })
draft_order: DraftOrder
@Column({ type: "int" }) @Column({ type: "int" })
amount: number amount: number
@@ -68,14 +68,6 @@ export class ShippingMethod {
@Column({ nullable: true }) @Column({ nullable: true })
return_id: string return_id: string
@Index()
@Column({ nullable: true })
draft_order_id: string
@ManyToOne(() => DraftOrder)
@JoinColumn({ name: "draft_order_id" })
draft_order: DraftOrder
@OneToOne( @OneToOne(
() => Return, () => Return,
ret => ret.shipping_method ret => ret.shipping_method
@@ -0,0 +1,335 @@
import _ from "lodash"
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
import DraftOrderService from "../draft-order"
const eventBusService = {
emit: jest.fn(),
withTransaction: function() {
return this
},
}
describe("DraftOrderService", () => {
const totalsService = {
getTotal: o => {
return o.total || 0
},
getSubtotal: o => {
return o.subtotal || 0
},
getTaxTotal: o => {
return o.tax_total || 0
},
getDiscountTotal: o => {
return o.discount_total || 0
},
getShippingTotal: o => {
return o.shipping_total || 0
},
getGiftCardTotal: o => {
return o.gift_card_total || 0
},
}
describe("create", () => {
let result
const regionService = {
retrieve: () =>
Promise.resolve({ id: "test-region", countries: [{ iso_2: "dk" }] }),
withTransaction: function() {
return this
},
}
const shippingOptionService = {
createShippingMethod: jest.fn().mockImplementation(() =>
Promise.resolve({
shipping_option: {
profile_id: "test-profile",
},
})
),
withTransaction: function() {
return this
},
}
const lineItemService = {
generate: jest.fn().mockImplementation(() =>
Promise.resolve({
title: "test-item",
variant_id: "test-variant",
})
),
create: jest.fn(),
withTransaction: function() {
return this
},
}
const productVariantService = {
retrieve: () =>
Promise.resolve({
id: "test-variant",
product: {
profile_id: "test-profile",
},
}),
withTransaction: function() {
return this
},
}
const cartService = {
create: jest.fn().mockImplementation(data =>
Promise.resolve({
id: "test-cart",
...data,
})
),
withTransaction: function() {
return this
},
}
const testOrder = {
region_id: "test-region",
shipping_address_id: "test-shipping",
billing_address_id: "test-billing",
customer_id: "test-customer",
items: [{ variant_id: "test-variant", quantity: 2, metadata: {} }],
shipping_methods: [
{
option_id: "test-option",
data: {},
},
],
}
const addressRepository = MockRepository({
create: addr => ({
...addr,
}),
})
const draftOrderRepository = MockRepository({
create: d => ({
...d,
}),
save: d => ({
id: "test-draft-order",
...d,
}),
})
const draftOrderService = new DraftOrderService({
manager: MockManager,
regionService,
cartService,
shippingOptionService,
lineItemService,
productVariantService,
draftOrderRepository,
addressRepository,
eventBusService,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("creates a draft order", async () => {
await draftOrderService.create(testOrder)
expect(draftOrderRepository.create).toHaveBeenCalledTimes(1)
expect(draftOrderRepository.create).toHaveBeenCalledWith({
cart_id: "test-cart",
})
expect(cartService.create).toHaveBeenCalledTimes(1)
expect(cartService.create).toHaveBeenCalledWith({
region_id: "test-region",
shipping_address_id: "test-shipping",
billing_address_id: "test-billing",
customer_id: "test-customer",
})
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledTimes(
1
)
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledWith(
"test-option",
{},
{
cart: {
id: "test-cart",
region_id: "test-region",
shipping_address_id: "test-shipping",
billing_address_id: "test-billing",
customer_id: "test-customer",
},
}
)
expect(lineItemService.generate).toHaveBeenCalledTimes(1)
expect(lineItemService.generate).toHaveBeenCalledWith(
"test-variant",
"test-region",
2,
{}
)
expect(lineItemService.create).toHaveBeenCalledTimes(1)
expect(lineItemService.create).toHaveBeenCalledWith({
cart_id: "test-cart",
has_shipping: true,
title: "test-item",
variant_id: "test-variant",
})
})
it("fails on missing region", async () => {
try {
await draftOrderService.create({
items: [],
})
} catch (error) {
expect(error.message).toEqual(
`region_id is required to create a draft order`
)
}
})
it("fails on missing items", async () => {
try {
await draftOrderService.create({
region_id: "test-region",
items: [],
})
} catch (error) {
expect(error.message).toEqual(
`Items are required to create a draft order`
)
}
})
})
describe("registerSystemPayment", () => {
let result
const shippingOptionService = {
update: jest.fn(),
withTransaction: function() {
return this
},
}
const lineItemService = {
update: jest.fn(),
withTransaction: function() {
return this
},
}
const cartService = {
retrieve: jest.fn().mockImplementation(data =>
Promise.resolve({
id: "test-cart",
total: 1000,
region_id: "test-region",
region: {
id: "test-region",
currency_code: "usd",
tax_rate: 0,
},
items: [{ id: "test-item" }],
discounts: [],
email: "oli@test.dk",
customer_id: "test-customer",
draft_order_id: "test-draft-order",
metadata: {},
})
),
withTransaction: function() {
return this
},
}
const draftOrderRepository = MockRepository({
findOne: () =>
Promise.resolve({ id: "test-draft-order", cart_id: "test-cart" }),
})
const orderRepository = MockRepository({
create: d => ({ id: "test-order", ...d }),
})
const paymentRepository = MockRepository({
create: d => ({
...d,
}),
})
const draftOrderService = new DraftOrderService({
manager: MockManager,
cartService,
shippingOptionService,
lineItemService,
paymentRepository,
draftOrderRepository,
orderRepository,
eventBusService,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("registers system payment", async () => {
await draftOrderService.registerSystemPayment("test-draft-order")
expect(cartService.retrieve).toHaveBeenCalledTimes(1)
expect(cartService.retrieve).toHaveBeenCalledWith("test-cart", {
relations: ["discounts", "shipping_methods", "region", "items"],
select: ["total"],
})
expect(paymentRepository.create).toHaveBeenCalledTimes(1)
expect(paymentRepository.create).toHaveBeenCalledWith({
provider_id: "system",
amount: 1000,
currency_code: "usd",
data: {},
})
expect(orderRepository.create).toHaveBeenCalledTimes(1)
expect(orderRepository.create).toHaveBeenCalledWith({
cart_id: "test-cart",
discounts: [],
region_id: "test-region",
discounts: [],
email: "oli@test.dk",
customer_id: "test-customer",
draft_order_id: "test-draft-order",
tax_rate: 0,
payment_status: "awaiting",
currency_code: "usd",
metadata: {},
payments: [
{
provider_id: "system",
amount: 1000,
currency_code: "usd",
data: {},
},
],
})
expect(draftOrderRepository.save).toHaveBeenCalledTimes(1)
expect(draftOrderRepository.save).toHaveBeenCalledWith({
id: "test-draft-order",
cart_id: "test-cart",
status: "completed",
})
})
})
})
+2
View File
@@ -958,6 +958,8 @@ class CartService extends BaseService {
relations: ["payment_sessions"], relations: ["payment_sessions"],
}) })
console.log("YELLO: ", session.status)
if (session.status === "authorized") { if (session.status === "authorized") {
const payment = await this.paymentProviderService_ const payment = await this.paymentProviderService_
.withTransaction(manager) .withTransaction(manager)
+28
View File
@@ -3,6 +3,7 @@ import Scrypt from "scrypt-kdf"
import _ from "lodash" import _ from "lodash"
import { Validator, MedusaError } from "medusa-core-utils" import { Validator, MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces" import { BaseService } from "medusa-interfaces"
import { Brackets } from "typeorm"
/** /**
* Provides layer to manipulate customers. * Provides layer to manipulate customers.
@@ -127,7 +128,34 @@ class CustomerService extends BaseService {
this.customerRepository_ this.customerRepository_
) )
let q
if ("q" in selector) {
q = selector.q
delete selector.q
}
const query = this.buildQuery_(selector, config) const query = this.buildQuery_(selector, config)
if (q) {
const where = query.where
delete where.email
delete where.first_name
delete where.last_name
query.where = qb => {
qb.where(where)
qb.andWhere(
new Brackets(qb => {
qb.where(`email ILIKE :q`, { q: `%${q}%` })
.orWhere(`first_name ILIKE :q`, { q: `%${q}%` })
.orWhere(`last_name ILIKE :q`, { q: `%${q}%` })
})
)
}
}
return customerRepo.find(query) return customerRepo.find(query)
} }
+300 -149
View File
@@ -1,6 +1,7 @@
import _ from "lodash" import _ from "lodash"
import { BaseService } from "medusa-interfaces" import { BaseService } from "medusa-interfaces"
import { MedusaError } from "medusa-core-utils" import { MedusaError, Validator } from "medusa-core-utils"
import { Brackets } from "typeorm"
/** /**
* Handles swaps * Handles swaps
@@ -14,11 +15,15 @@ class DraftOrderService extends BaseService {
constructor({ constructor({
manager, manager,
draftOrderRepository, draftOrderRepository,
paymentRepository,
orderRepository,
eventBusService, eventBusService,
addressRepository, addressRepository,
cartService, cartService,
customerService,
totalsService, totalsService,
lineItemService, lineItemService,
paymentProviderService,
productVariantService, productVariantService,
shippingOptionService, shippingOptionService,
regionService, regionService,
@@ -28,9 +33,15 @@ class DraftOrderService extends BaseService {
/** @private @const {EntityManager} */ /** @private @const {EntityManager} */
this.manager_ = manager this.manager_ = manager
/** @private @const {SwapModel} */ /** @private @const {DraftOrderRepository} */
this.draftOrderRepository_ = draftOrderRepository this.draftOrderRepository_ = draftOrderRepository
/** @private @const {PaymentRepository} */
this.paymentRepository_ = paymentRepository
/** @private @const {OrderRepository} */
this.orderRepository_ = orderRepository
/** @private @const {TotalsService} */ /** @private @const {TotalsService} */
this.totalsService_ = totalsService this.totalsService_ = totalsService
@@ -40,15 +51,18 @@ class DraftOrderService extends BaseService {
/** @private @const {LineItemService} */ /** @private @const {LineItemService} */
this.lineItemService_ = lineItemService this.lineItemService_ = lineItemService
/** @private @const {ReturnService} */
this.returnService_ = returnService
/** @private @const {CartService} */ /** @private @const {CartService} */
this.cartService_ = cartService this.cartService_ = cartService
/** @private @const {CustomerService} */
this.customerService_ = customerService
/** @private @const {RegionService} */ /** @private @const {RegionService} */
this.regionService_ = regionService this.regionService_ = regionService
/** @private @const {PaymentProviderService} */
this.paymentProviderService_ = paymentProviderService
/** @private @const {ProductVariantService} */ /** @private @const {ProductVariantService} */
this.productVariantService_ = productVariantService this.productVariantService_ = productVariantService
@@ -67,10 +81,16 @@ class DraftOrderService extends BaseService {
const cloned = new DraftOrderService({ const cloned = new DraftOrderService({
manager: transactionManager, manager: transactionManager,
draftOrderRepository: this.draftOrderRepository_, draftOrderRepository: this.draftOrderRepository_,
orderRepository: this.orderRepository_,
paymentRepository: this.paymentRepository_,
paymentProviderService: this.paymentProviderService_,
regionService: this.regionService_,
eventBusService: this.eventBus_, eventBusService: this.eventBus_,
cartService: this.cartService_, cartService: this.cartService_,
totalsService: this.totalsService_, totalsService: this.totalsService_,
productVariantService: this.productVariantService_, productVariantService: this.productVariantService_,
addressRepository: this.addressRepository_,
shippingOptionService: this.shippingOptionService_,
lineItemService: this.lineItemService_, lineItemService: this.lineItemService_,
}) })
@@ -79,6 +99,68 @@ class DraftOrderService extends BaseService {
return cloned return cloned
} }
transformQueryForTotals_(config) {
let { select, relations } = config
if (!select) {
return {
select,
relations,
totalsToSelect: [],
}
}
const totalFields = [
"subtotal",
"tax_total",
"shipping_total",
"discount_total",
"total",
]
const totalsToSelect = select.filter(v => totalFields.includes(v))
if (totalsToSelect.length > 0) {
const relationSet = new Set(relations)
relationSet.add("items")
relationSet.add("discounts")
relationSet.add("shipping_methods")
relationSet.add("region")
relations = [...relationSet]
select = select.filter(v => !totalFields.includes(v))
}
return {
relations,
select,
totalsToSelect,
}
}
decorateTotals_(draftOrder, totalsFields = []) {
if (totalsFields.includes("shipping_total")) {
draftOrder.shipping_total = this.totalsService_.getShippingTotal(
draftOrder
)
}
if (totalsFields.includes("discount_total")) {
draftOrder.discount_total = this.totalsService_.getDiscountTotal(
draftOrder
)
}
if (totalsFields.includes("tax_total")) {
draftOrder.tax_total = this.totalsService_.getTaxTotal(draftOrder)
}
if (totalsFields.includes("subtotal")) {
draftOrder.subtotal = this.totalsService_.getSubtotal(draftOrder)
}
if (totalsFields.includes("total")) {
draftOrder.total = this.totalsService_.getTotal(draftOrder)
}
return draftOrder
}
/** /**
* Retrieves a draft order with the given id. * Retrieves a draft order with the given id.
* @param {string} id - id of the draft order to retrieve * @param {string} id - id of the draft order to retrieve
@@ -91,17 +173,32 @@ class DraftOrderService extends BaseService {
const validatedId = this.validateId_(id) const validatedId = this.validateId_(id)
const query = this.buildQuery_({ id: validatedId }, config) const { select, relations, totalsToSelect } = this.transformQueryForTotals_(
config
)
const draftOrder = await draftOrderRepo.findOne(query) const query = {
where: { id: validatedId },
}
if (!draftOrder) { if (relations && relations.length > 0) {
query.relations = relations
}
if (select && select.length > 0) {
query.select = select
}
const raw = await draftOrderRepo.findOne(query)
if (!raw) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.NOT_FOUND, MedusaError.Types.NOT_FOUND,
`Draft order with id: ${id} was not found` `Draft order with ${id} was not found`
) )
} }
const draftOrder = this.decorateTotals_(raw, totalsToSelect)
return draftOrder return draftOrder
} }
@@ -132,6 +229,65 @@ class DraftOrderService extends BaseService {
return draftOrder return draftOrder
} }
async listAndCount(
selector,
config = { skip: 0, take: 50, order: { created_at: "DESC" } }
) {
const draftOrderRepository = this.manager_.getCustomRepository(
this.draftOrderRepository_
)
let q
if ("q" in selector) {
q = selector.q
delete selector.q
}
const query = this.buildQuery_(selector, config)
if (q) {
const where = query.where
delete where.display_id
query.join = {
alias: "draftOrder",
innerJoin: {
cart: "draftOrder.cart",
},
}
query.where = qb => {
qb.where(where)
qb.andWhere(
new Brackets(qb => {
qb.where(`cart.email ILIKE :q`, {
q: `%${q}%`,
}).orWhere(`display_id::varchar(255) ILIKE :dId`, { dId: `${q}` })
})
)
}
}
const { select, relations, totalsToSelect } = this.transformQueryForTotals_(
config
)
if (select && select.length) {
query.select = select
}
if (relations && relations.length) {
query.relations = relations
}
const [raw, count] = await draftOrderRepository.findAndCount(query)
const draftOrders = raw.map(r => this.decorateTotals_(r, totalsToSelect))
return [draftOrders, count]
}
/** /**
* Lists draft orders * Lists draft orders
* @param {Object} selector - query object for find * @param {Object} selector - query object for find
@@ -151,31 +307,6 @@ class DraftOrderService extends BaseService {
return draftOrderRepo.find(query) return draftOrderRepo.find(query)
} }
async setAddress_(region, address) {
const addressRepo = this.manager_.getCustomRepository(
this.addressRepository_
)
const regCountries = region.countries.map(({ iso_2 }) => iso_2)
if (!address.country_code) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Address is missing country code`
)
}
if (!regCountries.includes(address.country_code)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Country ${address.country_code} is not in region ${region.name}`
)
}
const created = addressRepo.create(adress)
return created
}
/** /**
* Confirms if the contents of a line item is covered by the inventory. * 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 * To be covered a variant must either not have its inventory managed or it
@@ -197,86 +328,13 @@ class DraftOrderService extends BaseService {
return this.productVariantService_.canCoverQuantity(variantId, quantity) return this.productVariantService_.canCoverQuantity(variantId, quantity)
} }
async addLineItem(doId, lineItem) {
return this.atomicPhase_(async manager => {
const draftOrder = await this.retrieve(doId, {
relations: [
"shipping_methods",
"items",
"payment_sessions",
"items.variant",
"items.variant.product",
],
})
if (draftOrder.status !== "open") {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"You are not allowed to add items to a draft order with status awaiting or completed"
)
}
let currentItem
if (lineItem.should_merge) {
currentItem = draftOrder.items.find(line => {
if (line.should_merge && line.variant_id === lineItem.variant_id) {
return _.isEqual(line.metadata, lineItem.metadata)
}
})
}
// If content matches one of the line items currently in the cart we can
// 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"
)
}
await this.lineItemService_
.withTransaction(manager)
.update(currentItem.id, {
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"
)
}
await this.lineItemService_.withTransaction(manager).create({
...lineItem,
has_shipping: false,
cart_id: cartId,
})
}
})
}
/** /**
* Creates a draft order. * Creates a draft order.
* @param {Object} data - data to create draft order from * @param {object} data - data to create draft order from
* @param {boolean} shippingRequired - needs shipping flag
* @return {Promise<DraftOrder>} the created draft order * @return {Promise<DraftOrder>} the created draft order
*/ */
async create(data) { async create(data, shippingRequired = true) {
return this.atomicPhase_(async manager => { return this.atomicPhase_(async manager => {
const draftOrderRepo = manager.getCustomRepository( const draftOrderRepo = manager.getCustomRepository(
this.draftOrderRepository_ this.draftOrderRepository_
@@ -289,36 +347,20 @@ class DraftOrderService extends BaseService {
) )
} }
const region = await this.regionService_ if (!data.items || !data.items.length) {
.withTransaction(manager)
.retrieve(data.region_id, {
relations: ["countries"],
})
if (!data.shipping_address && !data.shipping_address_id) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.INVALID_DATA, MedusaError.Types.INVALID_DATA,
`Shipping addresss is required to create a draft order` `Items are required to create a draft order`
)
}
if (data.shipping_address && !data.shipping_address_id) {
data.shipping_address = await this.setAddress_(
region,
data.shipping_address
)
}
if (data.billing_address && !data.billing_address_id) {
data.billing_address = await this.setAddress_(
region,
data.billing_address
) )
} }
const { items, shipping_methods, ...rest } = data const { items, shipping_methods, ...rest } = data
const draftOrder = draftOrderRepo.create(rest) const createdCart = await this.cartService_
.withTransaction(manager)
.create({ type: "draft_order", ...rest })
const draftOrder = draftOrderRepo.create({ cart_id: createdCart.id })
const result = await draftOrderRepo.save(draftOrder) const result = await draftOrderRepo.save(draftOrder)
await this.eventBus_ await this.eventBus_
@@ -328,31 +370,140 @@ class DraftOrderService extends BaseService {
}) })
let shippingMethods = [] let shippingMethods = []
for (const method of shipping_methods) { let profiles = []
const m = await this.shippingOptionService_ if (shippingRequired) {
.withTransaction(manager) for (const method of shipping_methods) {
.createShippingMethod(method.option_id, method.data, { const m = await this.shippingOptionService_
draft_order_id: draftOrder.id, .withTransaction(manager)
}) .createShippingMethod(method.option_id, method.data, {
cart: createdCart,
})
shippingMethods.push(m) shippingMethods.push(m)
}
profiles = shippingMethods.map(
({ shipping_option }) => shipping_option.profile_id
)
} }
for (const item of items) { for (const item of items) {
const line = await this.lineItemService_ if (item.variant_id) {
.withTransaction(manager) const line = await this.lineItemService_
.generate( .withTransaction(manager)
item.variant_id, .generate(
cart.region_id, item.variant_id,
item.quantity, data.region_id,
item.metadata item.quantity,
) item.metadata
)
const variant = await this.productVariantService_
.withTransaction(manager)
.retrieve(item.variant_id)
const itemProfile = variant.product.profile_id
let hasShipping = true
// if shipping is required, ensure items can be shipped
if (shippingRequired) {
hasShipping = profiles.includes(itemProfile)
}
await this.lineItemService_.withTransaction(manager).create({
cart_id: createdCart.id,
has_shipping: hasShipping,
...line,
})
} else {
// custom line items can be added to a draft order
await this.lineItemService_.withTransaction(manager).create({
cart_id: createdCart.id,
has_shipping: true,
title: item.title || "Custom item",
allow_discounts: false,
unit_price: item.unit_price || 0,
quantity: item.quantity,
})
}
}
return result
})
}
async registerSystemPayment(doId) {
return this.atomicPhase_(async manager => {
const draftOrder = await this.retrieve(doId)
const draftOrderCart = await this.cartService_
.withTransaction(manager)
.retrieve(draftOrder.cart_id, {
select: ["total"],
relations: ["discounts", "shipping_methods", "region", "items"],
})
const orderRepo = manager.getCustomRepository(this.orderRepository_)
const draftOrderRepo = manager.getCustomRepository(
this.draftOrderRepository_
)
const paymentRepo = manager.getCustomRepository(this.paymentRepository_)
const created = paymentRepo.create({
provider_id: "system",
amount: draftOrderCart.total,
currency_code: draftOrderCart.region.currency_code,
data: {},
})
const toCreate = {
payment_status: "awaiting",
discounts: draftOrderCart.discounts,
region_id: draftOrderCart.region_id,
email: draftOrderCart.email,
customer_id: draftOrderCart.customer_id,
draft_order_id: draftOrder.id,
cart_id: draftOrderCart.id,
tax_rate: draftOrderCart.region.tax_rate,
currency_code: draftOrderCart.region.currency_code,
metadata: draftOrderCart.metadata || {},
payments: [created],
}
if (draftOrderCart.shipping_address_id) {
toCreate.shipping_address_id = draftOrderCart.shipping_address_id
}
if (draftOrderCart.billing_address_id) {
toCreate.billing_address_id = draftOrderCart.billing_address_id
}
if (draftOrderCart.shipping_methods) {
toCreate.shipping_methods = draftOrderCart.shipping_methods
}
const o = orderRepo.create(toCreate)
const result = await orderRepo.save(o)
if (draftOrderCart.shipping_methods) {
for (const method of draftOrderCart.shipping_methods) {
await this.shippingOptionService_
.withTransaction(manager)
.updateShippingMethod(method.id, { order_id: result.id })
}
}
for (const item of draftOrderCart.items) {
await this.lineItemService_ await this.lineItemService_
.withTransaction(manager) .withTransaction(manager)
.create({ draft_order_id: draftOrder.id, ...line }) .update(item.id, { order_id: result.id })
} }
draftOrder.status = "completed"
await draftOrderRepo.save(draftOrder)
return result return result
}) })
} }
-1
View File
@@ -484,7 +484,6 @@ class OrderService extends BaseService {
payment_status: "awaiting", payment_status: "awaiting",
discounts: cart.discounts, discounts: cart.discounts,
gift_cards: cart.gift_cards, gift_cards: cart.gift_cards,
payment_status: "awaiting",
shipping_methods: cart.shipping_methods, shipping_methods: cart.shipping_methods,
shipping_address_id: cart.shipping_address_id, shipping_address_id: cart.shipping_address_id,
billing_address_id: cart.billing_address_id, billing_address_id: cart.billing_address_id,
@@ -0,0 +1,47 @@
import { BaseService } from "medusa-interfaces"
class SystemProviderService extends BaseService {
static identifier = "system"
constructor({}) {
super()
}
async createPayment(_) {
return {}
}
async getPaymentData(_) {
return {}
}
async authorizePayment(_) {
return { data: {}, status: "authorized" }
}
async updatePaymentData(_) {
return {}
}
async updatePayment(_) {
return {}
}
async deletePayment(_) {
return {}
}
async capturePayment(_) {
return {}
}
async refundPayment(_) {
return {}
}
async cancelPayment(_) {
return {}
}
}
export default SystemProviderService