Adds OrderService and /orders endpoints
Adds OrderService Adds endpoints for orders - both store and admin
This commit is contained in:
committed by
GitHub
parent
7ca03579db
commit
8255a839f6
@@ -8,6 +8,7 @@ import regionRoutes from "./regions"
|
||||
import shippingOptionRoutes from "./shipping-options"
|
||||
import shippingProfileRoutes from "./shipping-profiles"
|
||||
import discountRoutes from "./discounts"
|
||||
import orderRoutes from "./orders"
|
||||
|
||||
const route = Router()
|
||||
|
||||
@@ -34,6 +35,7 @@ export default (app, container) => {
|
||||
shippingOptionRoutes(route)
|
||||
shippingProfileRoutes(route)
|
||||
discountRoutes(route)
|
||||
orderRoutes(route)
|
||||
// productVariantRoutes(route)
|
||||
|
||||
return app
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { OrderServiceMock } from "../../../../../services/__mocks__/order"
|
||||
|
||||
describe("POST /admin/orders/:id/archive", () => {
|
||||
describe("successfully archives an order", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/orders/${IdMap.getId("processed-order")}/archive`,
|
||||
{
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls OrderService archive", () => {
|
||||
expect(OrderServiceMock.archive).toHaveBeenCalledTimes(1)
|
||||
expect(OrderServiceMock.archive).toHaveBeenCalledWith(
|
||||
IdMap.getId("processed-order")
|
||||
)
|
||||
})
|
||||
|
||||
it("returns order with status = archived", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
expect(subject.body._id).toEqual(IdMap.getId("processed-order"))
|
||||
expect(subject.body.status).toEqual("archived")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { OrderServiceMock } from "../../../../../services/__mocks__/order"
|
||||
|
||||
describe("POST /admin/orders/:id/cancel", () => {
|
||||
describe("successfully cancels an order", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/orders/${IdMap.getId("test-order")}/cancel`,
|
||||
{
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls OrderService cancel", () => {
|
||||
expect(OrderServiceMock.cancel).toHaveBeenCalledTimes(1)
|
||||
expect(OrderServiceMock.cancel).toHaveBeenCalledWith(
|
||||
IdMap.getId("test-order")
|
||||
)
|
||||
})
|
||||
|
||||
it("returns order with status = cancelled", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
expect(subject.body._id).toEqual(IdMap.getId("test-order"))
|
||||
expect(subject.body.status).toEqual("cancelled")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { OrderServiceMock } from "../../../../../services/__mocks__/order"
|
||||
|
||||
describe("POST /admin/orders/:id/capture", () => {
|
||||
describe("successfully captures payment for an order", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/orders/${IdMap.getId("test-order")}/capture`,
|
||||
{
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls OrderService capturePayment", () => {
|
||||
expect(OrderServiceMock.capturePayment).toHaveBeenCalledTimes(1)
|
||||
expect(OrderServiceMock.capturePayment).toHaveBeenCalledWith(
|
||||
IdMap.getId("test-order")
|
||||
)
|
||||
})
|
||||
|
||||
it("returns order with payment_status = captured", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
expect(subject.body._id).toEqual(IdMap.getId("test-order"))
|
||||
expect(subject.body.payment_status).toEqual("captured")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { OrderServiceMock } from "../../../../../services/__mocks__/order"
|
||||
|
||||
describe("POST /admin/orders/:id/fulfillment", () => {
|
||||
describe("successfully fulfills an order", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/orders/${IdMap.getId("test-order")}/fulfillment`,
|
||||
{
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls OrderService createFulfillment", () => {
|
||||
expect(OrderServiceMock.createFulfillment).toHaveBeenCalledTimes(1)
|
||||
expect(OrderServiceMock.createFulfillment).toHaveBeenCalledWith(
|
||||
IdMap.getId("test-order")
|
||||
)
|
||||
})
|
||||
|
||||
it("returns order with fulfillment_status = fulfilled", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
expect(subject.body._id).toEqual(IdMap.getId("test-order"))
|
||||
expect(subject.body.fulfillment_status).toEqual("fulfilled")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,140 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import {
|
||||
orders,
|
||||
OrderServiceMock,
|
||||
} from "../../../../../services/__mocks__/order"
|
||||
|
||||
describe("POST /admin/orders", () => {
|
||||
describe("successful creation", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", "/admin/orders", {
|
||||
payload: {
|
||||
email: "virgil@vandijk.dk",
|
||||
billing_address: {
|
||||
first_name: "Virgil",
|
||||
last_name: "Van Dijk",
|
||||
address_1: "24 Dunks Drive",
|
||||
city: "Los Angeles",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "93011",
|
||||
},
|
||||
shipping_address: {
|
||||
first_name: "Virgil",
|
||||
last_name: "Van Dijk",
|
||||
address_1: "24 Dunks Drive",
|
||||
city: "Los Angeles",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "93011",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("validId"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
region: IdMap.getId("testRegion"),
|
||||
customer_id: IdMap.getId("testCustomer"),
|
||||
payment_method: {
|
||||
provider_id: "default_provider",
|
||||
data: {},
|
||||
},
|
||||
shipping_method: [
|
||||
{
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("validId"),
|
||||
price: 123,
|
||||
data: {},
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls OrderService create", () => {
|
||||
expect(OrderServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(OrderServiceMock.create).toHaveBeenCalledWith({
|
||||
email: "virgil@vandijk.dk",
|
||||
billing_address: {
|
||||
first_name: "Virgil",
|
||||
last_name: "Van Dijk",
|
||||
address_1: "24 Dunks Drive",
|
||||
city: "Los Angeles",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "93011",
|
||||
},
|
||||
shipping_address: {
|
||||
first_name: "Virgil",
|
||||
last_name: "Van Dijk",
|
||||
address_1: "24 Dunks Drive",
|
||||
city: "Los Angeles",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "93011",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("validId"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
region: IdMap.getId("testRegion"),
|
||||
customer_id: IdMap.getId("testCustomer"),
|
||||
payment_method: {
|
||||
provider_id: "default_provider",
|
||||
data: {},
|
||||
},
|
||||
shipping_method: [
|
||||
{
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("validId"),
|
||||
price: 123,
|
||||
data: {},
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { OrderServiceMock } from "../../../../../services/__mocks__/order"
|
||||
|
||||
describe("GET /admin/orders", () => {
|
||||
describe("successfully gets an order", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"GET",
|
||||
`/admin/orders/${IdMap.getId("test-order")}`,
|
||||
{
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls orderService retrieve", () => {
|
||||
expect(OrderServiceMock.retrieve).toHaveBeenCalledTimes(1)
|
||||
expect(OrderServiceMock.retrieve).toHaveBeenCalledWith(
|
||||
IdMap.getId("test-order")
|
||||
)
|
||||
})
|
||||
|
||||
it("returns order", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
expect(subject.body._id).toEqual(IdMap.getId("test-order"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { OrderServiceMock } from "../../../../../services/__mocks__/order"
|
||||
|
||||
describe("POST /admin/orders/:id/return", () => {
|
||||
describe("successfully returns full order", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/orders/${IdMap.getId("test-order")}/return`,
|
||||
{
|
||||
payload: {
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("validId"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls OrderService return", () => {
|
||||
expect(OrderServiceMock.return).toHaveBeenCalledTimes(1)
|
||||
expect(OrderServiceMock.return).toHaveBeenCalledWith(
|
||||
IdMap.getId("test-order"),
|
||||
[
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("validId"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
]
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { OrderServiceMock } from "../../../../../services/__mocks__/order"
|
||||
|
||||
describe("POST /admin/orders/:id", () => {
|
||||
describe("successfully updates an order", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/orders/${IdMap.getId("test-order")}`,
|
||||
{
|
||||
payload: {
|
||||
email: "oliver@test.dk",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls OrderService update", () => {
|
||||
expect(OrderServiceMock.update).toHaveBeenCalledTimes(1)
|
||||
expect(OrderServiceMock.update).toHaveBeenCalledWith(
|
||||
IdMap.getId("test-order"),
|
||||
{
|
||||
email: "oliver@test.dk",
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("handles failed update operation", () => {
|
||||
it("throws if metadata is to be updated", async () => {
|
||||
try {
|
||||
await request("POST", `/admin/orders/${IdMap.getId("test-order")}`, {
|
||||
payload: {
|
||||
_id: IdMap.getId("test-order"),
|
||||
metadata: "Test Description",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.status).toEqual(400)
|
||||
expect(error.message).toEqual(
|
||||
"Use setMetadata to update metadata fields"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
11
packages/medusa/src/api/routes/admin/orders/archive-order.js
Normal file
11
packages/medusa/src/api/routes/admin/orders/archive-order.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const order = await orderService.archive(id)
|
||||
res.json(order)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
11
packages/medusa/src/api/routes/admin/orders/cancel-order.js
Normal file
11
packages/medusa/src/api/routes/admin/orders/cancel-order.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const order = await orderService.cancel(id)
|
||||
res.json(order)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const order = await orderService.capturePayment(id)
|
||||
res.json(order)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const order = await orderService.createFulfillment(id)
|
||||
res.json(order)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
46
packages/medusa/src/api/routes/admin/orders/create-order.js
Normal file
46
packages/medusa/src/api/routes/admin/orders/create-order.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const schema = Validator.object().keys({
|
||||
status: Validator.string().optional(),
|
||||
email: Validator.string()
|
||||
.email()
|
||||
.required(),
|
||||
billing_address: Validator.address().required(),
|
||||
shipping_address: Validator.address().required(),
|
||||
items: Validator.array().required(),
|
||||
region: Validator.string().required(),
|
||||
discounts: Validator.array().optional(),
|
||||
customer_id: Validator.string().required(),
|
||||
payment_method: Validator.object()
|
||||
.keys({
|
||||
provider_id: Validator.string().required(),
|
||||
data: Validator.object().optional(),
|
||||
})
|
||||
.required(),
|
||||
shipping_method: Validator.array()
|
||||
.items({
|
||||
provider_id: Validator.string().required(),
|
||||
profile_id: Validator.string().required(),
|
||||
price: Validator.number().required(),
|
||||
data: Validator.object().optional(),
|
||||
items: Validator.array().optional(),
|
||||
})
|
||||
.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 orderService = req.scope.resolve("orderService")
|
||||
const order = await orderService.create(value)
|
||||
|
||||
res.status(200).json(order)
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
12
packages/medusa/src/api/routes/admin/orders/get-order.js
Normal file
12
packages/medusa/src/api/routes/admin/orders/get-order.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const order = await orderService.retrieve(id)
|
||||
|
||||
res.json(order)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
30
packages/medusa/src/api/routes/admin/orders/index.js
Normal file
30
packages/medusa/src/api/routes/admin/orders/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Router } from "express"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default app => {
|
||||
app.use("/orders", route)
|
||||
|
||||
route.get("/:id", middlewares.wrap(require("./get-order").default))
|
||||
|
||||
route.post("/", middlewares.wrap(require("./create-order").default))
|
||||
route.post("/:id", middlewares.wrap(require("./update-order").default))
|
||||
|
||||
route.post(
|
||||
"/:id/capture",
|
||||
middlewares.wrap(require("./capture-payment").default)
|
||||
)
|
||||
route.post(
|
||||
"/:id/fulfillment",
|
||||
middlewares.wrap(require("./create-fulfillment").default)
|
||||
)
|
||||
route.post("/:id/return", middlewares.wrap(require("./return-order").default))
|
||||
route.post("/:id/cancel", middlewares.wrap(require("./cancel-order").default))
|
||||
route.post(
|
||||
"/:id/archive",
|
||||
middlewares.wrap(require("./archive-order").default)
|
||||
)
|
||||
|
||||
return app
|
||||
}
|
||||
23
packages/medusa/src/api/routes/admin/orders/return-order.js
Normal file
23
packages/medusa/src/api/routes/admin/orders/return-order.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
items: Validator.array().required(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const order = await orderService.return(id, value.items)
|
||||
|
||||
res.status(200).json(order)
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
40
packages/medusa/src/api/routes/admin/orders/update-order.js
Normal file
40
packages/medusa/src/api/routes/admin/orders/update-order.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
email: Validator.string().email(),
|
||||
billing_address: Validator.address(),
|
||||
shipping_address: Validator.address(),
|
||||
items: Validator.array(),
|
||||
region: Validator.string(),
|
||||
discounts: Validator.array(),
|
||||
customer_id: Validator.string(),
|
||||
payment_method: Validator.object().keys({
|
||||
provider_id: Validator.string(),
|
||||
data: Validator.object(),
|
||||
}),
|
||||
shipping_method: Validator.array().items({
|
||||
provider_id: Validator.string(),
|
||||
profile_id: Validator.string(),
|
||||
price: Validator.number(),
|
||||
data: Validator.object(),
|
||||
items: Validator.array(),
|
||||
}),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const order = await orderService.update(id, value)
|
||||
|
||||
res.status(200).json(order)
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Router } from "express"
|
||||
|
||||
import productRoutes from "./products"
|
||||
import cartRoutes from "./carts"
|
||||
import orderRoutes from "./orders"
|
||||
import customerRoutes from "./customers"
|
||||
import shippingOptionRoutes from "./shipping-options"
|
||||
|
||||
@@ -12,6 +13,7 @@ export default app => {
|
||||
|
||||
customerRoutes(route)
|
||||
productRoutes(route)
|
||||
orderRoutes(route)
|
||||
cartRoutes(route)
|
||||
shippingOptionRoutes(route)
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { OrderServiceMock } from "../../../../../services/__mocks__/order"
|
||||
import { carts } from "../../../../../services/__mocks__/cart"
|
||||
|
||||
describe("POST /store/orders", () => {
|
||||
describe("successful creation", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", "/store/orders", {
|
||||
payload: {
|
||||
cartId: IdMap.getId("fr-cart"),
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls service create", () => {
|
||||
expect(OrderServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(OrderServiceMock.create).toHaveBeenCalledWith(carts.frCart)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { OrderServiceMock } from "../../../../../services/__mocks__/order"
|
||||
|
||||
describe("GET /store/orders", () => {
|
||||
describe("successfully gets an order", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"GET",
|
||||
`/store/orders/${IdMap.getId("test-order")}`
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls orderService retrieve", () => {
|
||||
expect(OrderServiceMock.retrieve).toHaveBeenCalledTimes(1)
|
||||
expect(OrderServiceMock.retrieve).toHaveBeenCalledWith(
|
||||
IdMap.getId("test-order")
|
||||
)
|
||||
})
|
||||
|
||||
it("returns order", () => {
|
||||
expect(subject.body._id).toEqual(IdMap.getId("test-order"))
|
||||
})
|
||||
})
|
||||
})
|
||||
39
packages/medusa/src/api/routes/store/orders/create-order.js
Normal file
39
packages/medusa/src/api/routes/store/orders/create-order.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const schema = Validator.object().keys({
|
||||
cartId: Validator.string().required(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const cartService = req.scope.resolve("cartService")
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
|
||||
const cart = await cartService.retrieve(value.cartId)
|
||||
let order = await orderService.create(cart)
|
||||
order = await orderService.decorate(order, [
|
||||
"status",
|
||||
"fulfillment_status",
|
||||
"payment_status",
|
||||
"email",
|
||||
"billing_address",
|
||||
"shipping_address",
|
||||
"items",
|
||||
"region",
|
||||
"discounts",
|
||||
"customer_id",
|
||||
"payment_method",
|
||||
"shipping_methods",
|
||||
"metadata",
|
||||
])
|
||||
|
||||
res.status(200).json(order)
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
27
packages/medusa/src/api/routes/store/orders/get-order.js
Normal file
27
packages/medusa/src/api/routes/store/orders/get-order.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
let order = await orderService.retrieve(id)
|
||||
order = await orderService.decorate(order, [
|
||||
"status",
|
||||
"fulfillment_status",
|
||||
"payment_status",
|
||||
"email",
|
||||
"billing_address",
|
||||
"shipping_address",
|
||||
"items",
|
||||
"region",
|
||||
"discounts",
|
||||
"customer_id",
|
||||
"payment_method",
|
||||
"shipping_methods",
|
||||
"metadata",
|
||||
])
|
||||
|
||||
res.json(order)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
14
packages/medusa/src/api/routes/store/orders/index.js
Normal file
14
packages/medusa/src/api/routes/store/orders/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Router } from "express"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default app => {
|
||||
app.use("/orders", route)
|
||||
|
||||
route.get("/:id", middlewares.wrap(require("./get-order").default))
|
||||
|
||||
route.post("/", middlewares.wrap(require("./create-order").default))
|
||||
|
||||
return app
|
||||
}
|
||||
242
packages/medusa/src/models/__mocks__/order.js
Normal file
242
packages/medusa/src/models/__mocks__/order.js
Normal file
@@ -0,0 +1,242 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
|
||||
export const orders = {
|
||||
testOrder: {
|
||||
_id: IdMap.getId("test-order"),
|
||||
email: "oliver@test.dk",
|
||||
billing_address: {
|
||||
first_name: "Oli",
|
||||
last_name: "Medusa",
|
||||
address_1: "testaddress",
|
||||
city: "LA",
|
||||
country_code: "US",
|
||||
postal_code: "90002",
|
||||
},
|
||||
shipping_address: {
|
||||
first_name: "Oli",
|
||||
last_name: "Medusa",
|
||||
address_1: "testaddress",
|
||||
city: "LA",
|
||||
country_code: "US",
|
||||
postal_code: "90002",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("validId"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
region: IdMap.getId("region-france"),
|
||||
customer_id: IdMap.getId("test-customer"),
|
||||
payment_method: {
|
||||
provider_id: "default_provider",
|
||||
},
|
||||
shipping_methods: [
|
||||
{
|
||||
_id: IdMap.getId("expensiveShipping"),
|
||||
name: "Expensive Shipping",
|
||||
price: 100,
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("default"),
|
||||
},
|
||||
{
|
||||
_id: IdMap.getId("freeShipping"),
|
||||
name: "Free Shipping",
|
||||
price: 10,
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("profile1"),
|
||||
},
|
||||
],
|
||||
fulfillment_status: "not_fulfilled",
|
||||
payment_status: "awaiting",
|
||||
status: "pending",
|
||||
},
|
||||
processedOrder: {
|
||||
_id: IdMap.getId("processed-order"),
|
||||
email: "oliver@test.dk",
|
||||
billing_address: {
|
||||
first_name: "Oli",
|
||||
last_name: "Medusa",
|
||||
address_1: "testaddress",
|
||||
city: "LA",
|
||||
country_code: "US",
|
||||
postal_code: "90002",
|
||||
},
|
||||
shipping_address: {
|
||||
first_name: "Oli",
|
||||
last_name: "Medusa",
|
||||
address_1: "testaddress",
|
||||
city: "LA",
|
||||
country_code: "US",
|
||||
postal_code: "90002",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("validId"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
region: IdMap.getId("region-france"),
|
||||
customer_id: IdMap.getId("test-customer"),
|
||||
payment_method: {
|
||||
provider_id: "default_provider",
|
||||
},
|
||||
shipping_methods: [
|
||||
{
|
||||
_id: IdMap.getId("expensiveShipping"),
|
||||
name: "Expensive Shipping",
|
||||
price: 100,
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("default"),
|
||||
},
|
||||
{
|
||||
_id: IdMap.getId("freeShipping"),
|
||||
name: "Free Shipping",
|
||||
price: 10,
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("profile1"),
|
||||
},
|
||||
],
|
||||
fulfillment_status: "fulfilled",
|
||||
payment_status: "captured",
|
||||
status: "completed",
|
||||
},
|
||||
orderToRefund: {
|
||||
_id: IdMap.getId("refund-order"),
|
||||
email: "oliver@test.dk",
|
||||
billing_address: {
|
||||
first_name: "Oli",
|
||||
last_name: "Medusa",
|
||||
address_1: "testaddress",
|
||||
city: "LA",
|
||||
country_code: "US",
|
||||
postal_code: "90002",
|
||||
},
|
||||
shipping_address: {
|
||||
first_name: "Oli",
|
||||
last_name: "Medusa",
|
||||
address_1: "testaddress",
|
||||
city: "LA",
|
||||
country_code: "US",
|
||||
postal_code: "90002",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 100,
|
||||
variant: {
|
||||
_id: IdMap.getId("eur-8-us-10"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
{
|
||||
_id: IdMap.getId("existingLine2"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 100,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
region: IdMap.getId("region-france"),
|
||||
customer_id: IdMap.getId("test-customer"),
|
||||
payment_method: {
|
||||
provider_id: "default_provider",
|
||||
},
|
||||
shipping_methods: [
|
||||
{
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("default"),
|
||||
data: {},
|
||||
items: {},
|
||||
},
|
||||
{
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("default"),
|
||||
data: {},
|
||||
items: {},
|
||||
},
|
||||
],
|
||||
discounts: [],
|
||||
},
|
||||
}
|
||||
|
||||
export const OrderModelMock = {
|
||||
create: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
updateOne: jest.fn().mockImplementation((query, update) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
findOne: jest.fn().mockImplementation(query => {
|
||||
if (query._id === IdMap.getId("test-order")) {
|
||||
orders.testOrder.payment_status = "awaiting"
|
||||
return Promise.resolve(orders.testOrder)
|
||||
}
|
||||
if (query._id === IdMap.getId("not-fulfilled-order")) {
|
||||
orders.testOrder.fulfillment_status = "not_fulfilled"
|
||||
orders.testOrder.payment_status = "awaiting"
|
||||
return Promise.resolve(orders.testOrder)
|
||||
}
|
||||
if (query._id === IdMap.getId("fulfilled-order")) {
|
||||
orders.testOrder.fulfillment_status = "fulfilled"
|
||||
return Promise.resolve(orders.testOrder)
|
||||
}
|
||||
if (query._id === IdMap.getId("payed-order")) {
|
||||
orders.testOrder.fulfillment_status = "not_fulfilled"
|
||||
orders.testOrder.payment_status = "captured"
|
||||
return Promise.resolve(orders.testOrder)
|
||||
}
|
||||
if (query._id === IdMap.getId("processed-order")) {
|
||||
return Promise.resolve(orders.processedOrder)
|
||||
}
|
||||
if (query._id === IdMap.getId("order-refund")) {
|
||||
orders.orderToRefund.payment_status = "captured"
|
||||
return Promise.resolve(orders.orderToRefund)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
/*******************************************************************************
|
||||
*
|
||||
******************************************************************************/
|
||||
import mongoose from "mongoose"
|
||||
import { BaseModel } from "medusa-interfaces"
|
||||
|
||||
@@ -8,19 +5,24 @@ import LineItemSchema from "./schemas/line-item"
|
||||
import PaymentMethodSchema from "./schemas/payment-method"
|
||||
import ShippingMethodSchema from "./schemas/shipping-method"
|
||||
import AddressSchema from "./schemas/address"
|
||||
import DiscountModel from "./discount"
|
||||
|
||||
class OrderModel extends BaseModel {
|
||||
static modelName = "Order"
|
||||
|
||||
static schema = {
|
||||
canceled: { type: Boolean, default: false },
|
||||
archived: { type: Boolean, default: false },
|
||||
// pending, completed, archived, cancelled
|
||||
status: { type: String, default: "pending" },
|
||||
// not_fulfilled, partially_fulfilled (some line items have been returned), fulfilled, returned,
|
||||
fulfillment_status: { type: String, default: "not_fulfilled" },
|
||||
// awaiting, captured, refunded
|
||||
payment_status: { type: String, default: "awaiting" },
|
||||
email: { type: String, required: true },
|
||||
billing_address: { type: AddressSchema, required: true },
|
||||
shipping_address: { type: AddressSchema, required: true },
|
||||
items: { type: [LineItemSchema], required: true },
|
||||
region: { type: String, required: true },
|
||||
discounts: { type: [String], default: [] },
|
||||
discounts: { type: [DiscountModel.schema], default: [] },
|
||||
customer_id: { type: String, required: true },
|
||||
payment_method: { type: PaymentMethodSchema, required: true },
|
||||
shipping_methods: { type: [ShippingMethodSchema], required: true },
|
||||
|
||||
@@ -33,5 +33,6 @@ export default new mongoose.Schema({
|
||||
// cards etc. the unit_price field is provided to give more granular control.
|
||||
content: { type: mongoose.Schema.Types.Mixed, required: true },
|
||||
quantity: { type: Number, required: true },
|
||||
returned: { type: Boolean, default: false },
|
||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
})
|
||||
|
||||
@@ -2,6 +2,8 @@ import mongoose from "mongoose"
|
||||
|
||||
export default new mongoose.Schema({
|
||||
provider_id: { type: String, required: true },
|
||||
profile_id: { type: String, required: true },
|
||||
price: { type: Number, required: true },
|
||||
data: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
items: { type: [mongoose.Schema.Types.Mixed], default: [] },
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ class ShippingOptionModel extends BaseModel {
|
||||
name: { type: String, required: true },
|
||||
region_id: { type: String, required: true },
|
||||
provider_id: { type: String, required: true },
|
||||
profile_id: { type: String, required: true },
|
||||
data: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
price: { type: ShippingOptionPrice, required: true },
|
||||
requirements: { type: [ShippingOptionRequirement], default: [] },
|
||||
|
||||
182
packages/medusa/src/services/__mocks__/order.js
Normal file
182
packages/medusa/src/services/__mocks__/order.js
Normal file
@@ -0,0 +1,182 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
|
||||
export const orders = {
|
||||
testOrder: {
|
||||
_id: IdMap.getId("test-order"),
|
||||
email: "virgil@vandijk.dk",
|
||||
billing_address: {
|
||||
first_name: "Virgil",
|
||||
last_name: "Van Dijk",
|
||||
address_1: "24 Dunks Drive",
|
||||
city: "Los Angeles",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "93011",
|
||||
},
|
||||
shipping_address: {
|
||||
first_name: "Virgil",
|
||||
last_name: "Van Dijk",
|
||||
address_1: "24 Dunks Drive",
|
||||
city: "Los Angeles",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "93011",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("validId"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
region: IdMap.getId("testRegion"),
|
||||
customer_id: IdMap.getId("testCustomer"),
|
||||
payment_method: {
|
||||
provider_id: "default_provider",
|
||||
data: {},
|
||||
},
|
||||
shipping_method: [
|
||||
{
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("validId"),
|
||||
data: {},
|
||||
items: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
processedOrder: {
|
||||
_id: IdMap.getId("processed-order"),
|
||||
email: "oliver@test.dk",
|
||||
billing_address: {
|
||||
first_name: "Oli",
|
||||
last_name: "Medusa",
|
||||
address_1: "testaddress",
|
||||
city: "LA",
|
||||
country_code: "US",
|
||||
postal_code: "90002",
|
||||
},
|
||||
shipping_address: {
|
||||
first_name: "Oli",
|
||||
last_name: "Medusa",
|
||||
address_1: "testaddress",
|
||||
city: "LA",
|
||||
country_code: "US",
|
||||
postal_code: "90002",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("validId"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
region: IdMap.getId("region-france"),
|
||||
customer_id: IdMap.getId("test-customer"),
|
||||
payment_method: {
|
||||
provider_id: "default_provider",
|
||||
},
|
||||
shipping_methods: [
|
||||
{
|
||||
_id: IdMap.getId("expensiveShipping"),
|
||||
name: "Expensive Shipping",
|
||||
price: 100,
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("default"),
|
||||
},
|
||||
{
|
||||
_id: IdMap.getId("freeShipping"),
|
||||
name: "Free Shipping",
|
||||
price: 10,
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("profile1"),
|
||||
},
|
||||
],
|
||||
fulfillment_status: "fulfilled",
|
||||
payment_status: "captured",
|
||||
status: "completed",
|
||||
},
|
||||
}
|
||||
|
||||
export const OrderServiceMock = {
|
||||
create: jest.fn().mockImplementation(data => {
|
||||
return Promise.resolve(orders.testOrder)
|
||||
}),
|
||||
update: jest.fn().mockImplementation(data => Promise.resolve()),
|
||||
retrieve: jest.fn().mockImplementation(orderId => {
|
||||
if (orderId === IdMap.getId("test-order")) {
|
||||
return Promise.resolve(orders.testOrder)
|
||||
}
|
||||
if (orderId === IdMap.getId("processed-order")) {
|
||||
return Promise.resolve(orders.processedOrder)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
decorate: jest.fn().mockImplementation(order => {
|
||||
order.decorated = true
|
||||
return order
|
||||
}),
|
||||
cancel: jest.fn().mockImplementation(order => {
|
||||
if (order === IdMap.getId("test-order")) {
|
||||
orders.testOrder.status = "cancelled"
|
||||
return Promise.resolve(orders.testOrder)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
archive: jest.fn().mockImplementation(order => {
|
||||
if (order === IdMap.getId("processed-order")) {
|
||||
orders.processedOrder.status = "archived"
|
||||
return Promise.resolve(orders.processedOrder)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
createFulfillment: jest.fn().mockImplementation(order => {
|
||||
if (order === IdMap.getId("test-order")) {
|
||||
orders.testOrder.fulfillment_status = "fulfilled"
|
||||
return Promise.resolve(orders.testOrder)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
capturePayment: jest.fn().mockImplementation(order => {
|
||||
if (order === IdMap.getId("test-order")) {
|
||||
orders.testOrder.payment_status = "captured"
|
||||
return Promise.resolve(orders.testOrder)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
return: jest.fn().mockImplementation(order => {
|
||||
if (order === IdMap.getId("test-order")) {
|
||||
return Promise.resolve(orders.testOrder)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return OrderServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
@@ -10,6 +10,8 @@ export const DefaultProviderMock = {
|
||||
|
||||
return Promise.resolve("initial")
|
||||
}),
|
||||
capturePayment: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
refundPayment: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
}
|
||||
|
||||
export const PaymentProviderServiceMock = {
|
||||
|
||||
@@ -17,6 +17,7 @@ export const regions = {
|
||||
payment_providers: ["default_provider", "france-provider"],
|
||||
fulfillment_providers: ["default_provider"],
|
||||
currency_code: "eur",
|
||||
tax_rate: 0.25,
|
||||
},
|
||||
regionUs: {
|
||||
_id: IdMap.getId("region-us"),
|
||||
|
||||
@@ -4,7 +4,13 @@ export const profiles = {
|
||||
default: {
|
||||
_id: IdMap.getId("default"),
|
||||
name: "default_profile",
|
||||
products: [],
|
||||
products: [IdMap.getId("product")],
|
||||
shipping_options: [],
|
||||
},
|
||||
other: {
|
||||
_id: IdMap.getId("profile1"),
|
||||
name: "other_profile",
|
||||
products: [IdMap.getId("product")],
|
||||
shipping_options: [],
|
||||
},
|
||||
}
|
||||
@@ -16,7 +22,13 @@ export const ShippingProfileServiceMock = {
|
||||
create: jest.fn().mockImplementation(data => {
|
||||
return Promise.resolve(data)
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation(profileId => {
|
||||
retrieve: jest.fn().mockImplementation(data => {
|
||||
if (data === IdMap.getId("default")) {
|
||||
return Promise.resolve(profiles.default)
|
||||
}
|
||||
if (data === IdMap.getId("profile1")) {
|
||||
return Promise.resolve(profiles.other)
|
||||
}
|
||||
return Promise.resolve()
|
||||
}),
|
||||
list: jest.fn().mockImplementation(selector => {
|
||||
|
||||
@@ -10,6 +10,12 @@ export const TotalsServiceMock = {
|
||||
}
|
||||
return 0
|
||||
}),
|
||||
getRefundTotal: jest.fn().mockImplementation((order, lineItems) => {
|
||||
if (order._id === IdMap.getId("processed-order")) {
|
||||
return 1230
|
||||
}
|
||||
return 0
|
||||
}),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
|
||||
460
packages/medusa/src/services/__tests__/order.js
Normal file
460
packages/medusa/src/services/__tests__/order.js
Normal file
@@ -0,0 +1,460 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { OrderModelMock, orders } from "../../models/__mocks__/order"
|
||||
import OrderService from "../order"
|
||||
import { PaymentProviderServiceMock } from "../__mocks__/payment-provider"
|
||||
import { FulfillmentProviderServiceMock } from "../__mocks__/fulfillment-provider"
|
||||
import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile"
|
||||
import { TotalsServiceMock } from "../__mocks__/totals"
|
||||
|
||||
describe("OrderService", () => {
|
||||
describe("create", () => {
|
||||
const orderService = new OrderService({
|
||||
orderModel: OrderModelMock,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls order model functions", async () => {
|
||||
await orderService.create({
|
||||
email: "oliver@test.dk",
|
||||
})
|
||||
|
||||
expect(OrderModelMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(OrderModelMock.create).toHaveBeenCalledWith({
|
||||
email: "oliver@test.dk",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
let result
|
||||
const orderService = new OrderService({
|
||||
orderModel: OrderModelMock,
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
result = await orderService.retrieve(IdMap.getId("test-order"))
|
||||
})
|
||||
|
||||
it("calls order model functions", async () => {
|
||||
expect(OrderModelMock.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(OrderModelMock.findOne).toHaveBeenCalledWith({
|
||||
_id: IdMap.getId("test-order"),
|
||||
})
|
||||
})
|
||||
|
||||
it("returns correct order", async () => {
|
||||
expect(result._id).toEqual(IdMap.getId("test-order"))
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
const orderService = new OrderService({
|
||||
orderModel: OrderModelMock,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls order model functions", async () => {
|
||||
await orderService.update(IdMap.getId("test-order"), {
|
||||
email: "oliver@test.dk",
|
||||
})
|
||||
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("test-order") },
|
||||
{
|
||||
$set: {
|
||||
email: "oliver@test.dk",
|
||||
},
|
||||
},
|
||||
{ runValidators: true }
|
||||
)
|
||||
})
|
||||
|
||||
it("throws on invalid billing address", async () => {
|
||||
const address = {
|
||||
last_name: "James",
|
||||
address_1: "24 Dunks Drive",
|
||||
city: "Los Angeles",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "93011",
|
||||
}
|
||||
|
||||
try {
|
||||
await orderService.update(IdMap.getId("test-order"), {
|
||||
billing_address: address,
|
||||
})
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual("The address is not valid")
|
||||
}
|
||||
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it("throws on invalid shipping address", async () => {
|
||||
const address = {
|
||||
last_name: "James",
|
||||
address_1: "24 Dunks Drive",
|
||||
city: "Los Angeles",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "93011",
|
||||
}
|
||||
|
||||
try {
|
||||
await orderService.update(IdMap.getId("test-order"), {
|
||||
shipping_address: address,
|
||||
})
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual("The address is not valid")
|
||||
}
|
||||
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it("throws if metadata update are attempted", async () => {
|
||||
try {
|
||||
await orderService.update(IdMap.getId("test-order"), {
|
||||
metadata: { test: "foo" },
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual(
|
||||
"Use setMetadata to update metadata fields"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("throws if address updates are attempted after fulfillment", async () => {
|
||||
try {
|
||||
await orderService.update(IdMap.getId("fulfilled-order"), {
|
||||
billing_address: {
|
||||
first_name: "Lebron",
|
||||
last_name: "James",
|
||||
address_1: "24 Dunks Drive",
|
||||
city: "Los Angeles",
|
||||
country_code: "US",
|
||||
province: "CA",
|
||||
postal_code: "93011",
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual(
|
||||
"Can't update shipping, billing, items and payment method when order is processed"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("throws if payment method update is attempted after fulfillment", async () => {
|
||||
try {
|
||||
await orderService.update(IdMap.getId("fulfilled-order"), {
|
||||
payment_method: {
|
||||
provider_id: "test",
|
||||
profile_id: "test",
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual(
|
||||
"Can't update shipping, billing, items and payment method when order is processed"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("throws if items update is attempted after fulfillment", async () => {
|
||||
try {
|
||||
await orderService.update(IdMap.getId("fulfilled-order"), {
|
||||
items: [],
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual(
|
||||
"Can't update shipping, billing, items and payment method when order is processed"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("cancel", () => {
|
||||
const orderService = new OrderService({
|
||||
orderModel: OrderModelMock,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls order model functions", async () => {
|
||||
await orderService.cancel(IdMap.getId("not-fulfilled-order"))
|
||||
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("not-fulfilled-order") },
|
||||
{ $set: { status: "cancelled" } }
|
||||
)
|
||||
})
|
||||
|
||||
it("throws if order is fulfilled", async () => {
|
||||
try {
|
||||
await orderService.cancel(IdMap.getId("fulfilled-order"))
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual("Can't cancel a fulfilled order")
|
||||
}
|
||||
})
|
||||
|
||||
it("throws if order payment is captured", async () => {
|
||||
try {
|
||||
await orderService.cancel(IdMap.getId("payed-order"))
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual(
|
||||
"Can't cancel an order with payment processed"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("capturePayment", () => {
|
||||
const orderService = new OrderService({
|
||||
orderModel: OrderModelMock,
|
||||
paymentProviderService: PaymentProviderServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls order model functions", async () => {
|
||||
await orderService.capturePayment(IdMap.getId("test-order"))
|
||||
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("test-order") },
|
||||
{ $set: { payment_status: "captured" } }
|
||||
)
|
||||
})
|
||||
|
||||
it("throws if payment is already processed", async () => {
|
||||
try {
|
||||
await orderService.capturePayment(IdMap.getId("payed-order"))
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual("Payment already captured")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("createFulfillment", () => {
|
||||
const orderService = new OrderService({
|
||||
orderModel: OrderModelMock,
|
||||
paymentProviderService: PaymentProviderServiceMock,
|
||||
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||
shippingProfileService: ShippingProfileServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls order model functions", async () => {
|
||||
await orderService.createFulfillment(IdMap.getId("test-order"))
|
||||
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("test-order") },
|
||||
{ $set: { fulfillment_status: "fulfilled" } }
|
||||
)
|
||||
})
|
||||
|
||||
it("throws if payment is already processed", async () => {
|
||||
try {
|
||||
await orderService.createFulfillment(IdMap.getId("fulfilled-order"))
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual("Order is already fulfilled")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("return", () => {
|
||||
const orderService = new OrderService({
|
||||
orderModel: OrderModelMock,
|
||||
paymentProviderService: PaymentProviderServiceMock,
|
||||
totalsService: TotalsServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls order model functions", async () => {
|
||||
await orderService.return(IdMap.getId("processed-order"), [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("validId"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
])
|
||||
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("processed-order") },
|
||||
{
|
||||
$set: {
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
content: {
|
||||
product: {
|
||||
_id: IdMap.getId("validId"),
|
||||
},
|
||||
quantity: 1,
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
},
|
||||
description: "This is a new line",
|
||||
quantity: 10,
|
||||
returned_quantity: 10,
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
title: "merge line",
|
||||
},
|
||||
],
|
||||
fulfillment_status: "returned",
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("calls order model functions and sets partially_fulfilled", async () => {
|
||||
await orderService.return(IdMap.getId("order-refund"), [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 100,
|
||||
variant: {
|
||||
_id: IdMap.getId("eur-8-us-10"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 2,
|
||||
},
|
||||
])
|
||||
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("order-refund") },
|
||||
{
|
||||
$set: {
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
content: {
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
unit_price: 100,
|
||||
variant: {
|
||||
_id: IdMap.getId("eur-8-us-10"),
|
||||
},
|
||||
},
|
||||
description: "This is a new line",
|
||||
quantity: 10,
|
||||
returned_quantity: 2,
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
title: "merge line",
|
||||
},
|
||||
{
|
||||
_id: IdMap.getId("existingLine2"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 100,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
fulfillment_status: "partially_fulfilled",
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("throws if payment is already processed", async () => {
|
||||
try {
|
||||
await orderService.return(IdMap.getId("fulfilled-order"))
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual(
|
||||
"Can't return an order with payment unprocessed"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("throws if return is attempted on unfulfilled order", async () => {
|
||||
try {
|
||||
await orderService.return(IdMap.getId("not-fulfilled-order"))
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual(
|
||||
"Can't return an unfulfilled or already returned order"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("archive", () => {
|
||||
const orderService = new OrderService({
|
||||
orderModel: OrderModelMock,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls order model functions", async () => {
|
||||
await orderService.archive(IdMap.getId("processed-order"))
|
||||
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("processed-order") },
|
||||
{ $set: { status: "archived" } }
|
||||
)
|
||||
})
|
||||
|
||||
it("throws if order is unprocessed", async () => {
|
||||
try {
|
||||
await orderService.archive(IdMap.getId("test-order"))
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual("Can't archive an unprocessed order")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,9 @@ import TotalsService from "../totals"
|
||||
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
||||
import { discounts } from "../../models/__mocks__/discount"
|
||||
import { carts } from "../__mocks__/cart"
|
||||
import { orders } from "../../models/__mocks__/order"
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { RegionServiceMock } from "../__mocks__/region"
|
||||
|
||||
describe("TotalsService", () => {
|
||||
describe("getAllocationItemDiscounts", () => {
|
||||
@@ -23,9 +25,25 @@ describe("TotalsService", () => {
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
lineItem: IdMap.getId("existingLine"),
|
||||
lineItem: {
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 10,
|
||||
variant: {
|
||||
_id: IdMap.getId("eur-10-us-12"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
variant: IdMap.getId("eur-10-us-12"),
|
||||
amount: 1,
|
||||
amount: 10,
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -38,9 +56,25 @@ describe("TotalsService", () => {
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
lineItem: IdMap.getId("existingLine"),
|
||||
lineItem: {
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 10,
|
||||
variant: {
|
||||
_id: IdMap.getId("eur-10-us-12"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
variant: IdMap.getId("eur-10-us-12"),
|
||||
amount: 9,
|
||||
amount: 90,
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -70,41 +104,41 @@ describe("TotalsService", () => {
|
||||
carts.discountCart.discounts.push(discounts.total10Percent)
|
||||
res = await totalsService.getDiscountTotal(carts.discountCart)
|
||||
|
||||
expect(res).toEqual(252)
|
||||
expect(res).toEqual(28)
|
||||
})
|
||||
|
||||
it("calculate item fixed discount", async () => {
|
||||
carts.discountCart.discounts.push(discounts.item2Fixed)
|
||||
res = await totalsService.getDiscountTotal(carts.discountCart)
|
||||
|
||||
expect(res).toEqual(278)
|
||||
expect(res).toEqual(20)
|
||||
})
|
||||
|
||||
it("calculate item percentage discount", async () => {
|
||||
carts.discountCart.discounts.push(discounts.item10Percent)
|
||||
res = await totalsService.getDiscountTotal(carts.discountCart)
|
||||
|
||||
expect(res).toEqual(279)
|
||||
expect(res).toEqual(10)
|
||||
})
|
||||
|
||||
it("calculate total fixed discount", async () => {
|
||||
carts.discountCart.discounts.push(discounts.total10Fixed)
|
||||
res = await totalsService.getDiscountTotal(carts.discountCart)
|
||||
|
||||
expect(res).toEqual(270)
|
||||
expect(res).toEqual(10)
|
||||
})
|
||||
|
||||
it("ignores discount if expired", async () => {
|
||||
carts.discountCart.discounts.push(discounts.expiredDiscount)
|
||||
res = await totalsService.getDiscountTotal(carts.discountCart)
|
||||
|
||||
expect(res).toEqual(280)
|
||||
expect(res).toEqual(0)
|
||||
})
|
||||
|
||||
it("returns cart subtotal if no discounts are applied", async () => {
|
||||
it("returns 0 if no discounts are applied", async () => {
|
||||
res = await totalsService.getDiscountTotal(carts.discountCart)
|
||||
|
||||
expect(res).toEqual(280)
|
||||
expect(res).toEqual(0)
|
||||
})
|
||||
|
||||
it("returns 0 if no items are in cart", async () => {
|
||||
@@ -113,4 +147,219 @@ describe("TotalsService", () => {
|
||||
expect(res).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getRefundTotal", () => {
|
||||
let res
|
||||
const totalsService = new TotalsService({
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
regionService: RegionServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
orders.orderToRefund.discounts = []
|
||||
})
|
||||
|
||||
it("calculates refund", async () => {
|
||||
res = await totalsService.getRefundTotal(orders.orderToRefund, [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
])
|
||||
|
||||
expect(res).toEqual(1537.5)
|
||||
})
|
||||
|
||||
it("calculates refund with total precentage discount", async () => {
|
||||
orders.orderToRefund.discounts.push(discounts.total10Percent)
|
||||
res = await totalsService.getRefundTotal(orders.orderToRefund, [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
])
|
||||
|
||||
expect(res).toEqual(1107)
|
||||
})
|
||||
|
||||
it("calculates refund with total fixed discount", async () => {
|
||||
orders.orderToRefund.discounts.push(discounts.total10Fixed)
|
||||
res = await totalsService.getRefundTotal(orders.orderToRefund, [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 3,
|
||||
},
|
||||
])
|
||||
|
||||
expect(res).toEqual(359)
|
||||
})
|
||||
|
||||
it("calculates refund with item fixed discount", async () => {
|
||||
orders.orderToRefund.discounts.push(discounts.item2Fixed)
|
||||
res = await totalsService.getRefundTotal(orders.orderToRefund, [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("eur-8-us-10"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 3,
|
||||
},
|
||||
])
|
||||
|
||||
expect(res).toEqual(363)
|
||||
})
|
||||
|
||||
it("calculates refund with item percentage discount", async () => {
|
||||
orders.orderToRefund.discounts.push(discounts.item10Percent)
|
||||
res = await totalsService.getRefundTotal(orders.orderToRefund, [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("eur-8-us-10"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 3,
|
||||
},
|
||||
])
|
||||
|
||||
expect(res).toEqual(332.1)
|
||||
})
|
||||
|
||||
it("throws if line items to return is not in order", async () => {
|
||||
try {
|
||||
await totalsService.getRefundTotal(orders.orderToRefund, [
|
||||
{
|
||||
_id: IdMap.getId("notInOrder"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("eur-8-us-10"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 3,
|
||||
},
|
||||
])
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual("Line items does not exist on order")
|
||||
}
|
||||
})
|
||||
})
|
||||
describe("getShippingTotal", () => {
|
||||
let res
|
||||
const totalsService = new TotalsService({
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calculates shipping", async () => {
|
||||
res = await totalsService.getShippingTotal(orders.testOrder)
|
||||
|
||||
expect(res).toEqual(110)
|
||||
})
|
||||
})
|
||||
describe("getTaxTotal", () => {
|
||||
let res
|
||||
const totalsService = new TotalsService({
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
regionService: RegionServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
orders.orderToRefund.discounts = []
|
||||
})
|
||||
|
||||
it("calculates tax", async () => {
|
||||
res = await totalsService.getTaxTotal(orders.testOrder)
|
||||
|
||||
expect(res).toEqual(335)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getTotal", () => {
|
||||
let res
|
||||
const totalsService = new TotalsService({
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
regionService: RegionServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calculates total", async () => {
|
||||
res = await totalsService.getTotal(orders.testOrder)
|
||||
|
||||
expect(res).toEqual(1230 + 335 + 110)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -725,7 +725,8 @@ class CartService extends BaseService {
|
||||
* also have additional details in the data field such as an id for a package
|
||||
* shop.
|
||||
* @param {string} cartId - the id of the cart to add shipping method to
|
||||
* @param {ShippingMethod} method - the shipping method to add to the cart
|
||||
* @param {string} optionId - id of shipping option to add as valid method
|
||||
* @param {Object} data - the fulmillment data for the method
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async addShippingMethod(cartId, optionId, data) {
|
||||
|
||||
474
packages/medusa/src/services/order.js
Normal file
474
packages/medusa/src/services/order.js
Normal file
@@ -0,0 +1,474 @@
|
||||
import _ from "lodash"
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
|
||||
class OrderService extends BaseService {
|
||||
constructor({
|
||||
orderModel,
|
||||
paymentProviderService,
|
||||
shippingProfileService,
|
||||
fulfillmentProviderService,
|
||||
lineItemService,
|
||||
totalsService,
|
||||
eventBusService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
/** @private @const {OrderModel} */
|
||||
this.orderModel_ = orderModel
|
||||
|
||||
/** @private @const {PaymentProviderService} */
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
|
||||
/** @private @const {ShippingProvileService} */
|
||||
this.shippingProfileService_ = shippingProfileService
|
||||
|
||||
/** @private @const {FulfillmentProviderService} */
|
||||
this.fulfillmentProviderService_ = fulfillmentProviderService
|
||||
|
||||
/** @private @const {LineItemService} */
|
||||
this.lineItemService_ = lineItemService
|
||||
|
||||
/** @private @const {TotalsService} */
|
||||
this.totalsService_ = totalsService
|
||||
|
||||
/** @private @const {EventBus} */
|
||||
this.eventBus_ = eventBusService
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to validate order ids. Throws an error if the cast fails
|
||||
* @param {string} rawId - the raw order id to validate.
|
||||
* @return {string} the validated id
|
||||
*/
|
||||
validateId_(rawId) {
|
||||
const schema = Validator.objectId()
|
||||
const { value, error } = schema.validate(rawId)
|
||||
if (error) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"The order id could not be casted to an ObjectId"
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to validate order addresses. Can be used to both
|
||||
* validate shipping and billing address.
|
||||
* @param {Address} address - the address to validate
|
||||
* @return {Address} the validated address
|
||||
*/
|
||||
validateAddress_(address) {
|
||||
const { value, error } = Validator.address().validate(address)
|
||||
if (error) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"The address is not valid"
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to validate email.
|
||||
* @param {string} email - the email to vaildate
|
||||
* @return {string} the validate email
|
||||
*/
|
||||
validateEmail_(email) {
|
||||
const schema = Validator.string().email()
|
||||
const { value, error } = schema.validate(email)
|
||||
if (error) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"The email is not valid"
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
async partitionItems_(shipping_methods, items) {
|
||||
let updatedMethods = []
|
||||
// partition order items to their dedicated shipping method
|
||||
await Promise.all(
|
||||
shipping_methods.map(async method => {
|
||||
const { profile_id } = method
|
||||
const profile = await this.shippingProfileService_.retrieve(profile_id)
|
||||
// for each method find the items in the order, that are associated
|
||||
// with the profile on the current shipping method
|
||||
if (shipping_methods.length === 1) {
|
||||
method.items = items
|
||||
} else {
|
||||
method.items = items.filter(({ content }) => {
|
||||
if (Array.isArray(content)) {
|
||||
// we require bundles to have same shipping method, therefore:
|
||||
return profile.products.includes(content[0].product._id)
|
||||
} else {
|
||||
return profile.products.includes(content.product._id)
|
||||
}
|
||||
})
|
||||
}
|
||||
updatedMethods.push(method)
|
||||
})
|
||||
)
|
||||
return updatedMethods
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an order by id.
|
||||
* @param {string} orderId - id of order to retrieve
|
||||
* @return {Promise<Order>} the order document
|
||||
*/
|
||||
async retrieve(orderId) {
|
||||
const validatedId = this.validateId_(orderId)
|
||||
const order = await this.orderModel_
|
||||
.findOne({ _id: validatedId })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Order with ${orderId} was not found`
|
||||
)
|
||||
}
|
||||
return order
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an order
|
||||
* @param {object} order - the order to create
|
||||
* @return {Promise} resolves to the creation result.
|
||||
*/
|
||||
async create(order) {
|
||||
return this.orderModel_.create(order).catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an order. Metadata updates should
|
||||
* use dedicated method, e.g. `setMetadata` etc. The function
|
||||
* will throw errors if metadata updates are attempted.
|
||||
* @param {string} orderId - the id of the order. Must be a string that
|
||||
* can be casted to an ObjectId
|
||||
* @param {object} update - an object with the update values.
|
||||
* @return {Promise} resolves to the update result.
|
||||
*/
|
||||
async update(orderId, update) {
|
||||
const order = await this.retrieve(orderId)
|
||||
|
||||
if (
|
||||
(update.shipping_address ||
|
||||
update.billing_address ||
|
||||
update.payment_method ||
|
||||
update.items) &&
|
||||
(order.fulfillment_status !== "not_fulfilled" ||
|
||||
order.payment_status !== "awaiting" ||
|
||||
order.status !== "pending")
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Can't update shipping, billing, items and payment method when order is processed"
|
||||
)
|
||||
}
|
||||
|
||||
if (update.metadata) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Use setMetadata to update metadata fields"
|
||||
)
|
||||
}
|
||||
|
||||
if (update.status || update.fulfillment_status || update.payment_status) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Can't update order statuses. This will happen automatically. Use metadata in order for additional statuses"
|
||||
)
|
||||
}
|
||||
|
||||
const updateFields = { ...update }
|
||||
|
||||
if (update.shipping_address) {
|
||||
updateFields.shipping_address = this.validateAddress_(
|
||||
update.shipping_address
|
||||
)
|
||||
}
|
||||
|
||||
if (update.billing_address) {
|
||||
updateFields.billing_address = this.validateAddress_(
|
||||
update.billing_address
|
||||
)
|
||||
}
|
||||
|
||||
if (update.items) {
|
||||
updateFields.items = update.items.map(item =>
|
||||
this.lineItemService_.validate(item)
|
||||
)
|
||||
}
|
||||
|
||||
return this.orderModel_
|
||||
.updateOne(
|
||||
{ _id: order._id },
|
||||
{ $set: updateFields },
|
||||
{ runValidators: true }
|
||||
)
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels an order.
|
||||
* Throws if fulfillment process has been initiated.
|
||||
* Throws if payment process has been initiated.
|
||||
* @param {string} orderId - id of order to cancel.
|
||||
* @return {Promise} result of the update operation.
|
||||
*/
|
||||
async cancel(orderId) {
|
||||
const order = await this.retrieve(orderId)
|
||||
|
||||
if (order.fulfillment_status !== "not_fulfilled") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Can't cancel a fulfilled order"
|
||||
)
|
||||
}
|
||||
|
||||
if (order.payment_status !== "awaiting") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Can't cancel an order with payment processed"
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: cancel payment method
|
||||
|
||||
return this.orderModel_.updateOne(
|
||||
{
|
||||
_id: orderId,
|
||||
},
|
||||
{
|
||||
$set: { status: "cancelled" },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures payment for an order.
|
||||
* @param {string} orderId - id of order to capture payment for.
|
||||
* @return {Promise} result of the update operation.
|
||||
*/
|
||||
async capturePayment(orderId) {
|
||||
const order = await this.retrieve(orderId)
|
||||
|
||||
if (order.payment_status !== "awaiting") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Payment already captured"
|
||||
)
|
||||
}
|
||||
|
||||
// prepare update object
|
||||
const updateFields = { payment_status: "captured" }
|
||||
const completed = order.fulfillment_status !== "not_fulfilled"
|
||||
if (completed) {
|
||||
updateFields.status = "completed"
|
||||
}
|
||||
|
||||
const { provider_id, data } = order.payment_method
|
||||
const paymentProvider = await this.paymentProviderService_.retrieveProvider(
|
||||
provider_id
|
||||
)
|
||||
|
||||
await paymentProvider.capturePayment(data)
|
||||
|
||||
return this.orderModel_.updateOne(
|
||||
{
|
||||
_id: orderId,
|
||||
},
|
||||
{
|
||||
$set: updateFields,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates fulfillments for an order.
|
||||
* In a situation where the order has more than one shipping method,
|
||||
* we need to partition the order items, such that they can be sent
|
||||
* to their respective fulfillment provider.
|
||||
* @param {string} orderId - id of order to cancel.
|
||||
* @return {Promise} result of the update operation.
|
||||
*/
|
||||
async createFulfillment(orderId) {
|
||||
const order = await this.retrieve(orderId)
|
||||
|
||||
if (order.fulfillment_status !== "not_fulfilled") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Order is already fulfilled"
|
||||
)
|
||||
}
|
||||
|
||||
const { shipping_methods, items } = order
|
||||
|
||||
// prepare update object
|
||||
const updateFields = { fulfillment_status: "fulfilled" }
|
||||
const completed = order.payment_status !== "awaiting"
|
||||
if (completed) {
|
||||
updateFields.status = "completed"
|
||||
}
|
||||
|
||||
// partition order items to their dedicated shipping method
|
||||
order.shipping_methods = await this.partitionItems_(shipping_methods, items)
|
||||
|
||||
await Promise.all(
|
||||
order.shipping_methods.map(method => {
|
||||
const provider = this.fulfillmentProviderService_.retrieveProvider(
|
||||
method.provider_id
|
||||
)
|
||||
provider.createOrder(method.data, method.items)
|
||||
})
|
||||
)
|
||||
|
||||
return this.orderModel_.updateOne(
|
||||
{
|
||||
_id: orderId,
|
||||
},
|
||||
{
|
||||
$set: updateFields,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return either the entire or part of an order.
|
||||
* @param {string} orderId - the order to return.
|
||||
* @param {string[]} lineItems - the line items to return
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async return(orderId, lineItems) {
|
||||
const order = await this.retrieve(orderId)
|
||||
|
||||
if (
|
||||
order.fulfillment_status === "not_fulfilled" ||
|
||||
order.fulfillment_status === "returned"
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Can't return an unfulfilled or already returned order"
|
||||
)
|
||||
}
|
||||
|
||||
if (order.payment_status !== "captured") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Can't return an order with payment unprocessed"
|
||||
)
|
||||
}
|
||||
|
||||
const { provider_id, data } = order.payment_method
|
||||
const paymentProvider = this.paymentProviderService_.retrieveProvider(
|
||||
provider_id
|
||||
)
|
||||
|
||||
const amount = this.totalsService_.getRefundTotal(order, lineItems)
|
||||
await paymentProvider.refundPayment(data, amount)
|
||||
|
||||
lineItems.map(item => {
|
||||
const returnedItem = order.items.find(({ _id }) => _id === item._id)
|
||||
if (returnedItem) {
|
||||
returnedItem.returned_quantity = item.quantity
|
||||
}
|
||||
})
|
||||
|
||||
const fullReturn = order.items.every(
|
||||
item => item.quantity === item.returned_quantity
|
||||
)
|
||||
|
||||
return this.orderModel_.updateOne(
|
||||
{
|
||||
_id: orderId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
items: order.items,
|
||||
fulfillment_status: fullReturn ? "returned" : "partially_fulfilled",
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Archives an order. It only alloved, if the order has been fulfilled
|
||||
* and payment has been captured.
|
||||
* @param {string} orderId - the order to archive
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async archive(orderId) {
|
||||
const order = await this.retrieve(orderId)
|
||||
|
||||
if (order.status !== ("completed" || "refunded")) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Can't archive an unprocessed order"
|
||||
)
|
||||
}
|
||||
|
||||
return this.orderModel_.updateOne(
|
||||
{
|
||||
_id: orderId,
|
||||
},
|
||||
{
|
||||
$set: { status: "archived" },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorates an order.
|
||||
* @param {Order} order - the order to decorate.
|
||||
* @param {string[]} fields - the fields to include.
|
||||
* @param {string[]} expandFields - fields to expand.
|
||||
* @return {Order} return the decorated order.
|
||||
*/
|
||||
async decorate(order, fields, expandFields = []) {
|
||||
const requiredFields = ["_id", "metadata"]
|
||||
const decorated = _.pick(order, fields.concat(requiredFields))
|
||||
return decorated
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated method to set metadata for an order.
|
||||
* To ensure that plugins does not overwrite each
|
||||
* others metadata fields, setMetadata is provided.
|
||||
* @param {string} orderId - the order to decorate.
|
||||
* @param {string} key - key for metadata field
|
||||
* @param {string} value - value for metadata field.
|
||||
* @return {Promise} resolves to the updated result.
|
||||
*/
|
||||
setMetadata(orderId, key, value) {
|
||||
const validatedId = this.validateId_(orderId)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Key type is invalid. Metadata keys must be strings"
|
||||
)
|
||||
}
|
||||
|
||||
const keyPath = `metadata.${key}`
|
||||
return this.orderModel_
|
||||
.updateOne({ _id: validatedId }, { $set: { [keyPath]: value } })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default OrderService
|
||||
@@ -1,28 +1,47 @@
|
||||
import _ from "lodash"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
/**
|
||||
* A service that calculates total and subtotals for orders, carts etc..
|
||||
* @implements BaseService
|
||||
*/
|
||||
class TotalsService extends BaseService {
|
||||
constructor({ productVariantService }) {
|
||||
constructor({ productVariantService, regionService }) {
|
||||
super()
|
||||
/** @private @const {ProductVariantService} */
|
||||
this.productVariantService_ = productVariantService
|
||||
|
||||
/** @private @const {RegionService} */
|
||||
this.regionService_ = regionService
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates subtotal of a given cart
|
||||
* @param {Cart} Cart - the cart to calculate subtotal for
|
||||
* Calculates subtotal of a given cart or order.
|
||||
* @param {Cart || Order} object - cart or order to calculate subtotal for
|
||||
* @return {int} the calculated subtotal
|
||||
*/
|
||||
getSubtotal(cart) {
|
||||
async getTotal(object) {
|
||||
const subtotal = this.getSubtotal(object)
|
||||
const taxTotal = await this.getTaxTotal(object)
|
||||
const discountTotal = await this.getDiscountTotal(object)
|
||||
const shippingTotal = this.getShippingTotal(object)
|
||||
|
||||
return subtotal + taxTotal + shippingTotal - discountTotal
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates subtotal of a given cart or order.
|
||||
* @param {Cart || Order} object - cart or order to calculate subtotal for
|
||||
* @return {int} the calculated subtotal
|
||||
*/
|
||||
getSubtotal(object) {
|
||||
let subtotal = 0
|
||||
if (!cart.items) {
|
||||
if (!object.items) {
|
||||
return subtotal
|
||||
}
|
||||
|
||||
cart.items.map(item => {
|
||||
object.items.map(item => {
|
||||
if (Array.isArray(item.content)) {
|
||||
const temp = _.sumBy(item.content, c => c.unit_price * c.quantity)
|
||||
subtotal += temp * item.quantity
|
||||
@@ -34,6 +53,137 @@ class TotalsService extends BaseService {
|
||||
return subtotal
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates shipping total
|
||||
* @param {Cart | Object} object - cart or order to calculate subtotal for
|
||||
* @return {int} shipping total
|
||||
*/
|
||||
getShippingTotal(order) {
|
||||
const { shipping_methods } = order
|
||||
return shipping_methods.reduce((acc, next) => {
|
||||
return acc + next.price
|
||||
}, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates tax total
|
||||
* Currently based on the Danish tax system
|
||||
* @param {Cart | Object} object - cart or order to calculate subtotal for
|
||||
* @return {int} tax total
|
||||
*/
|
||||
async getTaxTotal(object) {
|
||||
const subtotal = this.getSubtotal(object)
|
||||
const shippingTotal = this.getShippingTotal(object)
|
||||
const region = await this.regionService_.retrieve(object.region)
|
||||
const { tax_rate } = region
|
||||
return (subtotal + shippingTotal) * tax_rate
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates refund total of line items.
|
||||
* If any of the items to return have been discounted, we need to
|
||||
* apply the discount again before refunding them.
|
||||
* @param {Order} order - cart or order to calculate subtotal for
|
||||
* @param {[LineItem]} lineItems -
|
||||
* @return {int} the calculated subtotal
|
||||
*/
|
||||
async getRefundTotal(order, lineItems) {
|
||||
const discount = order.discounts.find(
|
||||
({ discount_rule }) => discount_rule.type !== "free_shipping"
|
||||
)
|
||||
|
||||
if (_.differenceBy(lineItems, order.items, "_id").length !== 0) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Line items does not exist on order"
|
||||
)
|
||||
}
|
||||
|
||||
const subtotal = this.getSubtotal({ items: lineItems })
|
||||
|
||||
const region = await this.regionService_.retrieve(order.region)
|
||||
|
||||
// if nothing is discounted, return the subtotal of line items
|
||||
if (!discount) {
|
||||
return subtotal * (1 + region.tax_rate)
|
||||
}
|
||||
|
||||
const { value, type, allocation } = discount.discount_rule
|
||||
|
||||
if (type === "percentage" && allocation === "total") {
|
||||
const discountTotal = (subtotal / 100) * value
|
||||
return subtotal - discountTotal
|
||||
}
|
||||
|
||||
if (type === "fixed" && allocation === "total") {
|
||||
return subtotal - value
|
||||
}
|
||||
|
||||
if (type === "percentage" && allocation === "item") {
|
||||
// Find discounted items
|
||||
const itemPercentageDiscounts = await this.getAllocationItemDiscounts(
|
||||
discount,
|
||||
{ items: lineItems },
|
||||
"percentage"
|
||||
)
|
||||
|
||||
// Find discount total by taking each discounted item, reducing it by
|
||||
// its discount value. Then summing all those items together.
|
||||
const discountRefundTotal = _.sumBy(
|
||||
itemPercentageDiscounts,
|
||||
d => d.lineItem.content.unit_price * d.lineItem.quantity - d.amount
|
||||
)
|
||||
|
||||
// Find the items that weren't discounted
|
||||
const notDiscountedItems = _.differenceBy(
|
||||
lineItems,
|
||||
Array.from(itemPercentageDiscounts, el => el.lineItem),
|
||||
"_id"
|
||||
)
|
||||
|
||||
// If all items were discounted, we return the total of the discounted
|
||||
// items
|
||||
if (!notDiscountedItems) {
|
||||
return discountRefundTotal
|
||||
}
|
||||
|
||||
// Otherwise, we find the total those not discounted
|
||||
const notDiscRefundTotal = this.getSubtotal({ items: notDiscountedItems })
|
||||
|
||||
// Finally, return the sum of discounted and not discounted items
|
||||
return notDiscRefundTotal + discountRefundTotal
|
||||
}
|
||||
|
||||
// See immediate `if`-statement above for a elaboration on the following
|
||||
// calculations. This time with fixed discount type.
|
||||
if (type === "fixed" && allocation === "item") {
|
||||
const itemPercentageDiscounts = await this.getAllocationItemDiscounts(
|
||||
discount,
|
||||
{ items: lineItems },
|
||||
"fixed"
|
||||
)
|
||||
|
||||
const discountRefundTotal = _.sumBy(
|
||||
itemPercentageDiscounts,
|
||||
d => d.lineItem.content.unit_price * d.lineItem.quantity - d.amount
|
||||
)
|
||||
|
||||
const notDiscountedItems = _.differenceBy(
|
||||
lineItems,
|
||||
Array.from(itemPercentageDiscounts, el => el.lineItem),
|
||||
"_id"
|
||||
)
|
||||
|
||||
if (!notDiscountedItems) {
|
||||
return notDiscRefundTotal
|
||||
}
|
||||
|
||||
const notDiscRefundTotal = this.getSubtotal({ items: notDiscountedItems })
|
||||
|
||||
return notDiscRefundTotal + discountRefundTotal
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates either fixed or percentage discount of a variant
|
||||
* @param {string} lineItem - id of line item
|
||||
@@ -49,13 +199,16 @@ class TotalsService extends BaseService {
|
||||
return {
|
||||
lineItem,
|
||||
variant,
|
||||
amount: (variantPrice / 100) * value,
|
||||
amount: ((variantPrice * lineItem.quantity) / 100) * value,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
lineItem,
|
||||
variant,
|
||||
amount: value >= variantPrice ? variantPrice : value,
|
||||
amount:
|
||||
value >= variantPrice * lineItem.quantity
|
||||
? variantPrice * lineItem.quantity
|
||||
: value * lineItem.quantity,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,16 +228,16 @@ class TotalsService extends BaseService {
|
||||
const discounts = []
|
||||
for (const item of cart.items) {
|
||||
if (discount.discount_rule.valid_for.length > 0) {
|
||||
discount.discount_rule.valid_for.map(v => {
|
||||
discount.discount_rule.valid_for.map(variant => {
|
||||
// Discounts do not apply to bundles, hence:
|
||||
if (Array.isArray(item.content)) {
|
||||
return discounts
|
||||
} else {
|
||||
if (item.content.variant._id === v) {
|
||||
if (item.content.variant._id === variant) {
|
||||
discounts.push(
|
||||
this.calculateDiscount_(
|
||||
item._id,
|
||||
v,
|
||||
item,
|
||||
variant,
|
||||
item.content.unit_price,
|
||||
discount.discount_rule.value,
|
||||
discount.discount_rule.type
|
||||
@@ -99,17 +252,16 @@ class TotalsService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates discount total of a cart using the discounts provided in the
|
||||
* cart.discounts array. This will be subtracted from the cart subtotal,
|
||||
* which is returned from the function.
|
||||
* Calculates the total discount amount for each of the different supported
|
||||
* discount types. If discounts aren't present or invalid returns 0.
|
||||
* @param {Cart} Cart - the cart to calculate discounts for
|
||||
* @return {int} the subtotal after discounts are applied
|
||||
* @return {int} the total discounts amount
|
||||
*/
|
||||
async getDiscountTotal(cart) {
|
||||
let subtotal = this.getSubtotal(cart)
|
||||
|
||||
if (!cart.discounts) {
|
||||
return subtotal
|
||||
return 0
|
||||
}
|
||||
|
||||
// filter out invalid discounts
|
||||
@@ -135,14 +287,13 @@ class TotalsService extends BaseService {
|
||||
)
|
||||
|
||||
if (!discount) {
|
||||
return subtotal
|
||||
return 0
|
||||
}
|
||||
|
||||
const { type, allocation, value } = discount.discount_rule
|
||||
|
||||
if (type === "percentage" && allocation === "total") {
|
||||
subtotal -= (subtotal / 100) * value
|
||||
return subtotal
|
||||
return (subtotal / 100) * value
|
||||
}
|
||||
|
||||
if (type === "percentage" && allocation === "item") {
|
||||
@@ -152,13 +303,11 @@ class TotalsService extends BaseService {
|
||||
"percentage"
|
||||
)
|
||||
const totalDiscount = _.sumBy(itemPercentageDiscounts, d => d.amount)
|
||||
subtotal -= totalDiscount
|
||||
return subtotal
|
||||
return totalDiscount
|
||||
}
|
||||
|
||||
if (type === "fixed" && allocation === "total") {
|
||||
subtotal -= value
|
||||
return subtotal
|
||||
return value
|
||||
}
|
||||
|
||||
if (type === "fixed" && allocation === "item") {
|
||||
@@ -168,11 +317,10 @@ class TotalsService extends BaseService {
|
||||
"fixed"
|
||||
)
|
||||
const totalDiscount = _.sumBy(itemFixedDiscounts, d => d.amount)
|
||||
subtotal -= totalDiscount
|
||||
return subtotal
|
||||
return totalDiscount
|
||||
}
|
||||
|
||||
return subtotal
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user