Adds OrderService and /orders endpoints
Adds OrderService Adds endpoints for orders - both store and admin
This commit is contained in:
1 parent
7ca03579db
commit
8255a839f6
39 files changed
+2648
-47
No files matched your search
@@ -8,6 +8,7 @@ import regionRoutes from "./regions"
|
|||||||
import shippingOptionRoutes from "./shipping-options"
|
import shippingOptionRoutes from "./shipping-options"
|
||||||
import shippingProfileRoutes from "./shipping-profiles"
|
import shippingProfileRoutes from "./shipping-profiles"
|
||||||
import discountRoutes from "./discounts"
|
import discountRoutes from "./discounts"
|
||||||
|
import orderRoutes from "./orders"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ export default (app, container) => {
|
|||||||
shippingOptionRoutes(route)
|
shippingOptionRoutes(route)
|
||||||
shippingProfileRoutes(route)
|
shippingProfileRoutes(route)
|
||||||
discountRoutes(route)
|
discountRoutes(route)
|
||||||
|
orderRoutes(route)
|
||||||
// productVariantRoutes(route)
|
// productVariantRoutes(route)
|
||||||
|
|
||||||
return app
|
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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 productRoutes from "./products"
|
||||||
import cartRoutes from "./carts"
|
import cartRoutes from "./carts"
|
||||||
|
import orderRoutes from "./orders"
|
||||||
import customerRoutes from "./customers"
|
import customerRoutes from "./customers"
|
||||||
import shippingOptionRoutes from "./shipping-options"
|
import shippingOptionRoutes from "./shipping-options"
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ export default app => {
|
|||||||
|
|
||||||
customerRoutes(route)
|
customerRoutes(route)
|
||||||
productRoutes(route)
|
productRoutes(route)
|
||||||
|
orderRoutes(route)
|
||||||
cartRoutes(route)
|
cartRoutes(route)
|
||||||
shippingOptionRoutes(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"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 mongoose from "mongoose"
|
||||||
import { BaseModel } from "medusa-interfaces"
|
import { BaseModel } from "medusa-interfaces"
|
||||||
|
|
||||||
@@ -8,19 +5,24 @@ import LineItemSchema from "./schemas/line-item"
|
|||||||
import PaymentMethodSchema from "./schemas/payment-method"
|
import PaymentMethodSchema from "./schemas/payment-method"
|
||||||
import ShippingMethodSchema from "./schemas/shipping-method"
|
import ShippingMethodSchema from "./schemas/shipping-method"
|
||||||
import AddressSchema from "./schemas/address"
|
import AddressSchema from "./schemas/address"
|
||||||
|
import DiscountModel from "./discount"
|
||||||
|
|
||||||
class OrderModel extends BaseModel {
|
class OrderModel extends BaseModel {
|
||||||
static modelName = "Order"
|
static modelName = "Order"
|
||||||
|
|
||||||
static schema = {
|
static schema = {
|
||||||
canceled: { type: Boolean, default: false },
|
// pending, completed, archived, cancelled
|
||||||
archived: { type: Boolean, default: false },
|
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 },
|
email: { type: String, required: true },
|
||||||
billing_address: { type: AddressSchema, required: true },
|
billing_address: { type: AddressSchema, required: true },
|
||||||
shipping_address: { type: AddressSchema, required: true },
|
shipping_address: { type: AddressSchema, required: true },
|
||||||
items: { type: [LineItemSchema], required: true },
|
items: { type: [LineItemSchema], required: true },
|
||||||
region: { type: String, required: true },
|
region: { type: String, required: true },
|
||||||
discounts: { type: [String], default: [] },
|
discounts: { type: [DiscountModel.schema], default: [] },
|
||||||
customer_id: { type: String, required: true },
|
customer_id: { type: String, required: true },
|
||||||
payment_method: { type: PaymentMethodSchema, required: true },
|
payment_method: { type: PaymentMethodSchema, required: true },
|
||||||
shipping_methods: { type: [ShippingMethodSchema], 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.
|
// cards etc. the unit_price field is provided to give more granular control.
|
||||||
content: { type: mongoose.Schema.Types.Mixed, required: true },
|
content: { type: mongoose.Schema.Types.Mixed, required: true },
|
||||||
quantity: { type: Number, required: true },
|
quantity: { type: Number, required: true },
|
||||||
|
returned: { type: Boolean, default: false },
|
||||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||||
})
|
})
|
||||||
@@ -2,6 +2,8 @@ import mongoose from "mongoose"
|
|||||||
|
|
||||||
export default new mongoose.Schema({
|
export default new mongoose.Schema({
|
||||||
provider_id: { type: String, required: true },
|
provider_id: { type: String, required: true },
|
||||||
|
profile_id: { type: String, required: true },
|
||||||
|
price: { type: Number, required: true },
|
||||||
data: { type: mongoose.Schema.Types.Mixed, default: {} },
|
data: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||||
items: { 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 },
|
name: { type: String, required: true },
|
||||||
region_id: { type: String, required: true },
|
region_id: { type: String, required: true },
|
||||||
provider_id: { type: String, required: true },
|
provider_id: { type: String, required: true },
|
||||||
|
profile_id: { type: String, required: true },
|
||||||
data: { type: mongoose.Schema.Types.Mixed, default: {} },
|
data: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||||
price: { type: ShippingOptionPrice, required: true },
|
price: { type: ShippingOptionPrice, required: true },
|
||||||
requirements: { type: [ShippingOptionRequirement], default: [] },
|
requirements: { type: [ShippingOptionRequirement], default: [] },
|
||||||
|
|||||||
@@ -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")
|
return Promise.resolve("initial")
|
||||||
}),
|
}),
|
||||||
|
capturePayment: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
|
refundPayment: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PaymentProviderServiceMock = {
|
export const PaymentProviderServiceMock = {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const regions = {
|
|||||||
payment_providers: ["default_provider", "france-provider"],
|
payment_providers: ["default_provider", "france-provider"],
|
||||||
fulfillment_providers: ["default_provider"],
|
fulfillment_providers: ["default_provider"],
|
||||||
currency_code: "eur",
|
currency_code: "eur",
|
||||||
|
tax_rate: 0.25,
|
||||||
},
|
},
|
||||||
regionUs: {
|
regionUs: {
|
||||||
_id: IdMap.getId("region-us"),
|
_id: IdMap.getId("region-us"),
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ export const profiles = {
|
|||||||
default: {
|
default: {
|
||||||
_id: IdMap.getId("default"),
|
_id: IdMap.getId("default"),
|
||||||
name: "default_profile",
|
name: "default_profile",
|
||||||
products: [],
|
products: [IdMap.getId("product")],
|
||||||
|
shipping_options: [],
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
_id: IdMap.getId("profile1"),
|
||||||
|
name: "other_profile",
|
||||||
|
products: [IdMap.getId("product")],
|
||||||
shipping_options: [],
|
shipping_options: [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -16,7 +22,13 @@ export const ShippingProfileServiceMock = {
|
|||||||
create: jest.fn().mockImplementation(data => {
|
create: jest.fn().mockImplementation(data => {
|
||||||
return Promise.resolve(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()
|
return Promise.resolve()
|
||||||
}),
|
}),
|
||||||
list: jest.fn().mockImplementation(selector => {
|
list: jest.fn().mockImplementation(selector => {
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ export const TotalsServiceMock = {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}),
|
}),
|
||||||
|
getRefundTotal: jest.fn().mockImplementation((order, lineItems) => {
|
||||||
|
if (order._id === IdMap.getId("processed-order")) {
|
||||||
|
return 1230
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
const mock = jest.fn().mockImplementation(() => {
|
const mock = jest.fn().mockImplementation(() => {
|
||||||
|
|||||||
@@ -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 { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
||||||
import { discounts } from "../../models/__mocks__/discount"
|
import { discounts } from "../../models/__mocks__/discount"
|
||||||
import { carts } from "../__mocks__/cart"
|
import { carts } from "../__mocks__/cart"
|
||||||
|
import { orders } from "../../models/__mocks__/order"
|
||||||
import { IdMap } from "medusa-test-utils"
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
import { RegionServiceMock } from "../__mocks__/region"
|
||||||
|
|
||||||
describe("TotalsService", () => {
|
describe("TotalsService", () => {
|
||||||
describe("getAllocationItemDiscounts", () => {
|
describe("getAllocationItemDiscounts", () => {
|
||||||
@@ -23,9 +25,25 @@ describe("TotalsService", () => {
|
|||||||
|
|
||||||
expect(res).toEqual([
|
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"),
|
variant: IdMap.getId("eur-10-us-12"),
|
||||||
amount: 1,
|
amount: 10,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -38,9 +56,25 @@ describe("TotalsService", () => {
|
|||||||
|
|
||||||
expect(res).toEqual([
|
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"),
|
variant: IdMap.getId("eur-10-us-12"),
|
||||||
amount: 9,
|
amount: 90,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -70,41 +104,41 @@ describe("TotalsService", () => {
|
|||||||
carts.discountCart.discounts.push(discounts.total10Percent)
|
carts.discountCart.discounts.push(discounts.total10Percent)
|
||||||
res = await totalsService.getDiscountTotal(carts.discountCart)
|
res = await totalsService.getDiscountTotal(carts.discountCart)
|
||||||
|
|
||||||
expect(res).toEqual(252)
|
expect(res).toEqual(28)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("calculate item fixed discount", async () => {
|
it("calculate item fixed discount", async () => {
|
||||||
carts.discountCart.discounts.push(discounts.item2Fixed)
|
carts.discountCart.discounts.push(discounts.item2Fixed)
|
||||||
res = await totalsService.getDiscountTotal(carts.discountCart)
|
res = await totalsService.getDiscountTotal(carts.discountCart)
|
||||||
|
|
||||||
expect(res).toEqual(278)
|
expect(res).toEqual(20)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("calculate item percentage discount", async () => {
|
it("calculate item percentage discount", async () => {
|
||||||
carts.discountCart.discounts.push(discounts.item10Percent)
|
carts.discountCart.discounts.push(discounts.item10Percent)
|
||||||
res = await totalsService.getDiscountTotal(carts.discountCart)
|
res = await totalsService.getDiscountTotal(carts.discountCart)
|
||||||
|
|
||||||
expect(res).toEqual(279)
|
expect(res).toEqual(10)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("calculate total fixed discount", async () => {
|
it("calculate total fixed discount", async () => {
|
||||||
carts.discountCart.discounts.push(discounts.total10Fixed)
|
carts.discountCart.discounts.push(discounts.total10Fixed)
|
||||||
res = await totalsService.getDiscountTotal(carts.discountCart)
|
res = await totalsService.getDiscountTotal(carts.discountCart)
|
||||||
|
|
||||||
expect(res).toEqual(270)
|
expect(res).toEqual(10)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("ignores discount if expired", async () => {
|
it("ignores discount if expired", async () => {
|
||||||
carts.discountCart.discounts.push(discounts.expiredDiscount)
|
carts.discountCart.discounts.push(discounts.expiredDiscount)
|
||||||
res = await totalsService.getDiscountTotal(carts.discountCart)
|
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)
|
res = await totalsService.getDiscountTotal(carts.discountCart)
|
||||||
|
|
||||||
expect(res).toEqual(280)
|
expect(res).toEqual(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns 0 if no items are in cart", async () => {
|
it("returns 0 if no items are in cart", async () => {
|
||||||
@@ -113,4 +147,219 @@ describe("TotalsService", () => {
|
|||||||
expect(res).toEqual(0)
|
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
|
* also have additional details in the data field such as an id for a package
|
||||||
* shop.
|
* shop.
|
||||||
* @param {string} cartId - the id of the cart to add shipping method to
|
* @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
|
* @return {Promise} the result of the update operation
|
||||||
*/
|
*/
|
||||||
async addShippingMethod(cartId, optionId, data) {
|
async addShippingMethod(cartId, optionId, data) {
|
||||||
|
|||||||
@@ -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 _ from "lodash"
|
||||||
import { BaseService } from "medusa-interfaces"
|
import { BaseService } from "medusa-interfaces"
|
||||||
|
import { MedusaError } from "medusa-core-utils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that calculates total and subtotals for orders, carts etc..
|
* A service that calculates total and subtotals for orders, carts etc..
|
||||||
* @implements BaseService
|
* @implements BaseService
|
||||||
*/
|
*/
|
||||||
class TotalsService extends BaseService {
|
class TotalsService extends BaseService {
|
||||||
constructor({ productVariantService }) {
|
constructor({ productVariantService, regionService }) {
|
||||||
super()
|
super()
|
||||||
/** @private @const {ProductVariantService} */
|
/** @private @const {ProductVariantService} */
|
||||||
this.productVariantService_ = productVariantService
|
this.productVariantService_ = productVariantService
|
||||||
|
|
||||||
|
/** @private @const {RegionService} */
|
||||||
|
this.regionService_ = regionService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates subtotal of a given cart
|
* Calculates subtotal of a given cart or order.
|
||||||
* @param {Cart} Cart - the cart to calculate subtotal for
|
* @param {Cart || Order} object - cart or order to calculate subtotal for
|
||||||
* @return {int} the calculated subtotal
|
* @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
|
let subtotal = 0
|
||||||
if (!cart.items) {
|
if (!object.items) {
|
||||||
return subtotal
|
return subtotal
|
||||||
}
|
}
|
||||||
|
|
||||||
cart.items.map(item => {
|
object.items.map(item => {
|
||||||
if (Array.isArray(item.content)) {
|
if (Array.isArray(item.content)) {
|
||||||
const temp = _.sumBy(item.content, c => c.unit_price * c.quantity)
|
const temp = _.sumBy(item.content, c => c.unit_price * c.quantity)
|
||||||
subtotal += temp * item.quantity
|
subtotal += temp * item.quantity
|
||||||
@@ -34,6 +53,137 @@ class TotalsService extends BaseService {
|
|||||||
return subtotal
|
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
|
* Calculates either fixed or percentage discount of a variant
|
||||||
* @param {string} lineItem - id of line item
|
* @param {string} lineItem - id of line item
|
||||||
@@ -49,13 +199,16 @@ class TotalsService extends BaseService {
|
|||||||
return {
|
return {
|
||||||
lineItem,
|
lineItem,
|
||||||
variant,
|
variant,
|
||||||
amount: (variantPrice / 100) * value,
|
amount: ((variantPrice * lineItem.quantity) / 100) * value,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
lineItem,
|
lineItem,
|
||||||
variant,
|
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 = []
|
const discounts = []
|
||||||
for (const item of cart.items) {
|
for (const item of cart.items) {
|
||||||
if (discount.discount_rule.valid_for.length > 0) {
|
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:
|
// Discounts do not apply to bundles, hence:
|
||||||
if (Array.isArray(item.content)) {
|
if (Array.isArray(item.content)) {
|
||||||
return discounts
|
return discounts
|
||||||
} else {
|
} else {
|
||||||
if (item.content.variant._id === v) {
|
if (item.content.variant._id === variant) {
|
||||||
discounts.push(
|
discounts.push(
|
||||||
this.calculateDiscount_(
|
this.calculateDiscount_(
|
||||||
item._id,
|
item,
|
||||||
v,
|
variant,
|
||||||
item.content.unit_price,
|
item.content.unit_price,
|
||||||
discount.discount_rule.value,
|
discount.discount_rule.value,
|
||||||
discount.discount_rule.type
|
discount.discount_rule.type
|
||||||
@@ -99,17 +252,16 @@ class TotalsService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates discount total of a cart using the discounts provided in the
|
* Calculates the total discount amount for each of the different supported
|
||||||
* cart.discounts array. This will be subtracted from the cart subtotal,
|
* discount types. If discounts aren't present or invalid returns 0.
|
||||||
* which is returned from the function.
|
|
||||||
* @param {Cart} Cart - the cart to calculate discounts for
|
* @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) {
|
async getDiscountTotal(cart) {
|
||||||
let subtotal = this.getSubtotal(cart)
|
let subtotal = this.getSubtotal(cart)
|
||||||
|
|
||||||
if (!cart.discounts) {
|
if (!cart.discounts) {
|
||||||
return subtotal
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter out invalid discounts
|
// filter out invalid discounts
|
||||||
@@ -135,14 +287,13 @@ class TotalsService extends BaseService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!discount) {
|
if (!discount) {
|
||||||
return subtotal
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type, allocation, value } = discount.discount_rule
|
const { type, allocation, value } = discount.discount_rule
|
||||||
|
|
||||||
if (type === "percentage" && allocation === "total") {
|
if (type === "percentage" && allocation === "total") {
|
||||||
subtotal -= (subtotal / 100) * value
|
return (subtotal / 100) * value
|
||||||
return subtotal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "percentage" && allocation === "item") {
|
if (type === "percentage" && allocation === "item") {
|
||||||
@@ -152,13 +303,11 @@ class TotalsService extends BaseService {
|
|||||||
"percentage"
|
"percentage"
|
||||||
)
|
)
|
||||||
const totalDiscount = _.sumBy(itemPercentageDiscounts, d => d.amount)
|
const totalDiscount = _.sumBy(itemPercentageDiscounts, d => d.amount)
|
||||||
subtotal -= totalDiscount
|
return totalDiscount
|
||||||
return subtotal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "fixed" && allocation === "total") {
|
if (type === "fixed" && allocation === "total") {
|
||||||
subtotal -= value
|
return value
|
||||||
return subtotal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "fixed" && allocation === "item") {
|
if (type === "fixed" && allocation === "item") {
|
||||||
@@ -168,11 +317,10 @@ class TotalsService extends BaseService {
|
|||||||
"fixed"
|
"fixed"
|
||||||
)
|
)
|
||||||
const totalDiscount = _.sumBy(itemFixedDiscounts, d => d.amount)
|
const totalDiscount = _.sumBy(itemFixedDiscounts, d => d.amount)
|
||||||
subtotal -= totalDiscount
|
return totalDiscount
|
||||||
return subtotal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return subtotal
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user