feature: swaps (#137)
* feat: swap schema * chore(refactor): move returns to separate service * swap order creates return * fix: adds returns/fulfillments/shipments and cart creation to swaps * search products by variants * feat: swaps ready * fix: test passing * chore: include customer expand fields on order * add tests * fix: add brightpearl plugin support * fix: adds swap link template setting * fix: tests * fix(medusa-plugin-brightpearl): add swap status option * docs: add swap description * fix: adds GET swap in store api * chore: unmock axios * fix: passing klarna tests * fix: pr
This commit is contained in:
1 parent
c6996dd530
commit
fd04233238
56 files changed
+3266
-564
No files matched your search
@@ -48,10 +48,13 @@ export default (rootDirectory) => {
|
||||
"/webshipper/shipments",
|
||||
bodyParser.raw({ type: "application/vnd.api+json" }),
|
||||
async (req, res) => {
|
||||
const webshipperService = req.scope.resolve(
|
||||
"webshipperFulfillmentService"
|
||||
)
|
||||
const eventBus = req.scope.resolve("eventBusService")
|
||||
const logger = req.scope.resolve("logger")
|
||||
|
||||
const secret = `da791d87513eb091640f9fb6c4b94384`
|
||||
const secret = webshipperService.options_.webhook_secret
|
||||
const hmac = crypto.createHmac("sha256", secret)
|
||||
const digest = hmac.update(req.body).digest("base64")
|
||||
const hash = req.header("x-webshipper-hmac-sha256")
|
||||
|
||||
@@ -4,12 +4,13 @@ import Webshipper from "../utils/webshipper"
|
||||
class WebshipperFulfillmentService extends FulfillmentService {
|
||||
static identifier = "webshipper"
|
||||
|
||||
constructor({ logger, orderService }, options) {
|
||||
constructor({ logger, swapService, orderService }, options) {
|
||||
super()
|
||||
|
||||
this.logger_ = logger
|
||||
this.orderService_ = orderService
|
||||
this.options_ = options
|
||||
this.swapService_ = swapService
|
||||
this.client_ = new Webshipper({
|
||||
account: this.options_.account,
|
||||
token: this.options_.api_token,
|
||||
@@ -227,15 +228,23 @@ class WebshipperFulfillmentService extends FulfillmentService {
|
||||
})
|
||||
}
|
||||
|
||||
let visible_ref = `${fromOrder.display_id}-${
|
||||
fromOrder.fulfillments.length + 1
|
||||
}`
|
||||
let ext_ref = `${fromOrder._id}.${fromOrder.fulfillments.length}`
|
||||
|
||||
if (fromOrder.is_swap) {
|
||||
ext_ref = `S${fromOrder._id}.${fromOrder.fulfillments.length}`
|
||||
visible_ref = `S-${fromOrder.display_id}`
|
||||
}
|
||||
|
||||
const { shipping_address } = fromOrder
|
||||
const newOrder = {
|
||||
type: "orders",
|
||||
attributes: {
|
||||
status: "pending",
|
||||
ext_ref: `${fromOrder._id}.${fromOrder.fulfillments.length}`,
|
||||
visible_ref: `${fromOrder.display_id}-${
|
||||
fromOrder.fulfillments.length + 1
|
||||
}`,
|
||||
ext_ref,
|
||||
visible_ref,
|
||||
order_lines: fulfillmentItems.map((item) => {
|
||||
return {
|
||||
ext_ref: item._id,
|
||||
@@ -325,14 +334,24 @@ class WebshipperFulfillmentService extends FulfillmentService {
|
||||
"."
|
||||
)
|
||||
|
||||
const order = await this.orderService_.retrieve(orderId)
|
||||
const fulfillment = order.fulfillments[fulfillmentIndex]
|
||||
if (fulfillment) {
|
||||
await this.orderService_.createShipment(
|
||||
order._id,
|
||||
if (orderId.charAt(0) === "S") {
|
||||
const swap = await this.swapService_.retrieve(orderId.substring(1))
|
||||
const fulfillment = swap.fulfillments[fulfillmentIndex]
|
||||
await this.swapService_.createShipment(
|
||||
swap._id,
|
||||
fulfillment._id,
|
||||
trackingNumbers
|
||||
)
|
||||
} else {
|
||||
const order = await this.orderService_.retrieve(orderId)
|
||||
const fulfillment = order.fulfillments[fulfillmentIndex]
|
||||
if (fulfillment) {
|
||||
await this.orderService_.createShipment(
|
||||
order._id,
|
||||
fulfillment._id,
|
||||
trackingNumbers
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ class BaseService {
|
||||
* @param {object} obj - the object to decorate.
|
||||
* @return {object} the decorated object.
|
||||
*/
|
||||
runDecorators_(obj) {
|
||||
runDecorators_(obj, fields = [], expandFields = []) {
|
||||
return this.decorators_.reduce(async (acc, next) => {
|
||||
return acc.then(res => next(res)).catch(() => acc)
|
||||
return acc.then(res => next(res, fields, expandFields)).catch(() => acc)
|
||||
}, Promise.resolve(obj))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"axios-mock-adapter": "^1.19.0",
|
||||
"client-sessions": "^0.8.0",
|
||||
"cross-env": "^5.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
@@ -36,7 +37,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-classes": "^7.9.5",
|
||||
"axios": "^0.19.2",
|
||||
"axios": "^0.21.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"medusa-core-utils": "^1.0.10",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
const mockAxios = jest.genMockFromModule("axios")
|
||||
|
||||
mockAxios.create = jest.fn(() => mockAxios)
|
||||
|
||||
export default mockAxios
|
||||
@@ -10,17 +10,23 @@ export default async (req, res) => {
|
||||
|
||||
const klarnaOrder = await klarnaProviderService.retrieveCompletedOrder(
|
||||
klarna_order_id
|
||||
).then(({ data }) => data)
|
||||
)
|
||||
|
||||
const cartId = klarnaOrder.merchant_data
|
||||
try {
|
||||
const order = await orderService.retrieveByCartId(cartId)
|
||||
await klarnaProviderService.acknowledgeOrder(klarnaOrder.order_id, order._id)
|
||||
await klarnaProviderService.acknowledgeOrder(
|
||||
klarnaOrder.order_id,
|
||||
order._id
|
||||
)
|
||||
} catch (err) {
|
||||
if (err.type === MedusaError.Types.NOT_FOUND) {
|
||||
const cart = await cartService.retrieve(cartId)
|
||||
const order = await orderService.createFromCart(cart)
|
||||
await klarnaProviderService.acknowledgeOrder(klarnaOrder.order_id, order._id)
|
||||
await klarnaProviderService.acknowledgeOrder(
|
||||
klarnaOrder.order_id,
|
||||
order._id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import MockAdapter from "axios-mock-adapter"
|
||||
|
||||
import KlarnaProviderService from "../klarna-provider"
|
||||
import mockAxios from "../../__mocks__/axios"
|
||||
import { carts } from "../../__mocks__/cart"
|
||||
import { TotalsServiceMock } from "../../__mocks__/totals"
|
||||
import { RegionServiceMock } from "../../__mocks__/region"
|
||||
|
||||
describe("KlarnaProviderService", () => {
|
||||
beforeEach(() => {
|
||||
mockAxios.mockClear()
|
||||
})
|
||||
|
||||
describe("createPayment", () => {
|
||||
const klarnaProviderService = new KlarnaProviderService(
|
||||
{
|
||||
@@ -27,22 +24,19 @@ describe("KlarnaProviderService", () => {
|
||||
}
|
||||
)
|
||||
|
||||
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||
mockServer.onPost("/checkout/v3/orders").reply(() => {
|
||||
return [200, { order_id: "123456789", order_amount: 100 }]
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("creates Klarna order", async () => {
|
||||
mockAxios.post = jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
order_id: "123456789",
|
||||
order_amount: 100,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const result = await klarnaProviderService.createPayment(carts.frCart)
|
||||
expect(mockAxios.post).toHaveBeenCalledTimes(1)
|
||||
|
||||
// expect(mockAxios.post).toHaveBeenCalledTimes(1)
|
||||
expect(result).toEqual({
|
||||
order_id: "123456789",
|
||||
order_amount: 100,
|
||||
@@ -67,21 +61,14 @@ describe("KlarnaProviderService", () => {
|
||||
}
|
||||
)
|
||||
|
||||
it("returns Klarna order", async () => {
|
||||
mockAxios.get.mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
order_id: "123456789",
|
||||
},
|
||||
})
|
||||
})
|
||||
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||
mockServer.onGet("/checkout/v3/orders/123456789").reply(() => {
|
||||
return [200, { order_id: "123456789" }]
|
||||
})
|
||||
|
||||
it("returns Klarna order", async () => {
|
||||
result = await klarnaProviderService.retrievePayment({
|
||||
payment_method: {
|
||||
data: {
|
||||
id: "123456789",
|
||||
},
|
||||
},
|
||||
order_id: "123456789",
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -107,20 +94,13 @@ describe("KlarnaProviderService", () => {
|
||||
}
|
||||
)
|
||||
|
||||
it("returns completed Klarna order", async () => {
|
||||
mockAxios.get.mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
order_id: "123456789",
|
||||
})
|
||||
})
|
||||
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||
mockServer.onGet("/ordermanagement/v1/orders/123456789").reply(() => {
|
||||
return [200, { order_id: "123456789" }]
|
||||
})
|
||||
|
||||
result = await klarnaProviderService.retrieveCompletedOrder({
|
||||
payment_method: {
|
||||
data: {
|
||||
id: "123456789",
|
||||
},
|
||||
},
|
||||
})
|
||||
it("returns completed Klarna order", async () => {
|
||||
result = await klarnaProviderService.retrieveCompletedOrder("123456789")
|
||||
|
||||
expect(result).toEqual({
|
||||
order_id: "123456789",
|
||||
@@ -150,24 +130,21 @@ describe("KlarnaProviderService", () => {
|
||||
},
|
||||
}
|
||||
)
|
||||
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||
mockServer.onPost("/checkout/v3/orders/123456789").reply(() => {
|
||||
return [
|
||||
200,
|
||||
{
|
||||
order_id: "123456789",
|
||||
order_amount: 1000,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
it("returns updated Klarna order", async () => {
|
||||
mockAxios.post.mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
order_id: "123456789",
|
||||
order_amount: 1000,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
result = await klarnaProviderService.updatePayment(
|
||||
{
|
||||
payment_method: {
|
||||
data: {
|
||||
id: "123456789",
|
||||
},
|
||||
},
|
||||
order_id: "123456789",
|
||||
},
|
||||
carts.frCart
|
||||
)
|
||||
@@ -195,12 +172,14 @@ describe("KlarnaProviderService", () => {
|
||||
password: "123456789",
|
||||
}
|
||||
)
|
||||
|
||||
it("returns order id", async () => {
|
||||
mockAxios.post.mockImplementation((data) => {
|
||||
return Promise.resolve()
|
||||
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||
mockServer
|
||||
.onPost("/ordermanagement/v1/orders/123456789/cancel")
|
||||
.reply(() => {
|
||||
return [200]
|
||||
})
|
||||
|
||||
it("returns order id", async () => {
|
||||
result = await klarnaProviderService.cancelPayment({
|
||||
order_id: "123456789",
|
||||
})
|
||||
@@ -226,12 +205,20 @@ describe("KlarnaProviderService", () => {
|
||||
password: "123456789",
|
||||
}
|
||||
)
|
||||
|
||||
it("returns order id", async () => {
|
||||
mockAxios.post.mockImplementation((data) => {
|
||||
return Promise.resolve({})
|
||||
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||
mockServer
|
||||
.onPost("/ordermanagement/v1/orders/123456789/acknowledge")
|
||||
.reply(() => {
|
||||
return [200]
|
||||
})
|
||||
|
||||
mockServer
|
||||
.onPatch("/ordermanagement/v1/orders/123456789/merchant-references")
|
||||
.reply(() => {
|
||||
return [200]
|
||||
})
|
||||
|
||||
it("returns order id", async () => {
|
||||
result = await klarnaProviderService.acknowledgeOrder("123456789")
|
||||
|
||||
expect(result).toEqual("123456789")
|
||||
@@ -255,12 +242,14 @@ describe("KlarnaProviderService", () => {
|
||||
password: "123456789",
|
||||
}
|
||||
)
|
||||
|
||||
it("returns order id", async () => {
|
||||
mockAxios.post.mockImplementation((data) => {
|
||||
return Promise.resolve()
|
||||
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||
mockServer
|
||||
.onPost("/ordermanagement/v1/orders/123456789/merchant-references")
|
||||
.reply(() => {
|
||||
return [200]
|
||||
})
|
||||
|
||||
it("returns order id", async () => {
|
||||
result = await klarnaProviderService.addOrderToKlarnaOrder(
|
||||
"123456789",
|
||||
"order123456789"
|
||||
@@ -287,20 +276,23 @@ describe("KlarnaProviderService", () => {
|
||||
password: "123456789",
|
||||
}
|
||||
)
|
||||
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||
mockServer.onGet("/ordermanagement/v1/orders/123456789").reply(() => {
|
||||
return [
|
||||
200,
|
||||
{
|
||||
order: { order_amount: 1000 },
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
mockServer
|
||||
.onPost("/ordermanagement/v1/orders/123456789/captures")
|
||||
.reply(() => {
|
||||
return [200]
|
||||
})
|
||||
|
||||
it("returns order id", async () => {
|
||||
mockAxios.get.mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
order: { order_amount: 1000 },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
mockAxios.post.mockImplementation((data) => {
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
result = await klarnaProviderService.capturePayment({
|
||||
order_id: "123456789",
|
||||
})
|
||||
@@ -326,12 +318,14 @@ describe("KlarnaProviderService", () => {
|
||||
password: "123456789",
|
||||
}
|
||||
)
|
||||
|
||||
it("returns order id", async () => {
|
||||
mockAxios.post.mockImplementation((data) => {
|
||||
return Promise.resolve()
|
||||
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||
mockServer
|
||||
.onPost("/ordermanagement/v1/orders/123456789/refunds")
|
||||
.reply(() => {
|
||||
return [200]
|
||||
})
|
||||
|
||||
it("returns order id", async () => {
|
||||
result = await klarnaProviderService.refundPayment(
|
||||
{
|
||||
order_id: "123456789",
|
||||
|
||||
@@ -279,9 +279,9 @@ class KlarnaProviderService extends PaymentService {
|
||||
*/
|
||||
async retrieveCompletedOrder(klarnaOrderId) {
|
||||
try {
|
||||
return this.klarna_.get(
|
||||
`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}`
|
||||
)
|
||||
return this.klarna_
|
||||
.get(`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}`)
|
||||
.then(({ data }) => data)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -964,43 +964,17 @@
|
||||
exec-sh "^0.3.2"
|
||||
minimist "^1.2.0"
|
||||
|
||||
"@hapi/address@^2.1.2":
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
|
||||
integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==
|
||||
"@hapi/hoek@^9.0.0":
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6"
|
||||
integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw==
|
||||
|
||||
"@hapi/formula@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-1.2.0.tgz#994649c7fea1a90b91a0a1e6d983523f680e10cd"
|
||||
integrity sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA==
|
||||
|
||||
"@hapi/hoek@^8.2.4", "@hapi/hoek@^8.3.0":
|
||||
version "8.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06"
|
||||
integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==
|
||||
|
||||
"@hapi/joi@^16.1.8":
|
||||
version "16.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-16.1.8.tgz#84c1f126269489871ad4e2decc786e0adef06839"
|
||||
integrity sha512-wAsVvTPe+FwSrsAurNt5vkg3zo+TblvC5Bb1zMVK6SJzZqw9UrJnexxR+76cpePmtUZKHAPxcQ2Bf7oVHyahhg==
|
||||
"@hapi/topo@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7"
|
||||
integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==
|
||||
dependencies:
|
||||
"@hapi/address" "^2.1.2"
|
||||
"@hapi/formula" "^1.2.0"
|
||||
"@hapi/hoek" "^8.2.4"
|
||||
"@hapi/pinpoint" "^1.0.2"
|
||||
"@hapi/topo" "^3.1.3"
|
||||
|
||||
"@hapi/pinpoint@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-1.0.2.tgz#025b7a36dbbf4d35bf1acd071c26b20ef41e0d13"
|
||||
integrity sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ==
|
||||
|
||||
"@hapi/topo@^3.1.3":
|
||||
version "3.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.6.tgz#68d935fa3eae7fdd5ab0d7f953f3205d8b2bfc29"
|
||||
integrity sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^8.3.0"
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||
version "1.0.0"
|
||||
@@ -1185,6 +1159,23 @@
|
||||
"@types/yargs" "^15.0.0"
|
||||
chalk "^3.0.0"
|
||||
|
||||
"@sideway/address@^4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.0.tgz#0b301ada10ac4e0e3fa525c90615e0b61a72b78d"
|
||||
integrity sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@sideway/formula@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
|
||||
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
|
||||
|
||||
"@sideway/pinpoint@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
|
||||
integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
|
||||
|
||||
"@sinonjs/commons@^1.7.0":
|
||||
version "1.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
|
||||
@@ -1472,12 +1463,20 @@ aws4@^1.8.0:
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
|
||||
integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
|
||||
|
||||
axios@^0.19.2:
|
||||
version "0.19.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
|
||||
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
|
||||
axios-mock-adapter@^1.19.0:
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.19.0.tgz#9d72e321a6c5418e1eff067aa99761a86c5188a4"
|
||||
integrity sha512-D+0U4LNPr7WroiBDvWilzTMYPYTuZlbo6BI8YHZtj7wYQS8NkARlP9KBt8IWWHTQJ0q/8oZ0ClPBtKCCkx8cQg==
|
||||
dependencies:
|
||||
follow-redirects "1.5.10"
|
||||
fast-deep-equal "^3.1.3"
|
||||
is-buffer "^2.0.3"
|
||||
|
||||
axios@^0.21.0:
|
||||
version "0.21.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.0.tgz#26df088803a2350dff2c27f96fef99fe49442aca"
|
||||
integrity sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==
|
||||
dependencies:
|
||||
follow-redirects "^1.10.0"
|
||||
|
||||
babel-jest@^25.5.1:
|
||||
version "25.5.1"
|
||||
@@ -2006,7 +2005,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@3.1.0, debug@=3.1.0:
|
||||
debug@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
|
||||
@@ -2477,6 +2476,11 @@ fast-deep-equal@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
|
||||
integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
|
||||
|
||||
fast-deep-equal@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-json-stable-stringify@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||
@@ -2581,12 +2585,10 @@ flatted@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
|
||||
integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
|
||||
|
||||
follow-redirects@1.5.10:
|
||||
version "1.5.10"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
|
||||
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
|
||||
dependencies:
|
||||
debug "=3.1.0"
|
||||
follow-redirects@^1.10.0:
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
|
||||
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
|
||||
|
||||
for-in@^1.0.2:
|
||||
version "1.0.2"
|
||||
@@ -2991,6 +2993,11 @@ is-buffer@^1.1.5:
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
|
||||
|
||||
is-buffer@^2.0.3:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
|
||||
integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
|
||||
|
||||
is-callable@^1.1.4, is-callable@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
|
||||
@@ -3588,6 +3595,17 @@ joi-objectid@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/joi-objectid/-/joi-objectid-3.0.1.tgz#63ace7860f8e1a993a28d40c40ffd8eff01a3668"
|
||||
integrity sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ==
|
||||
|
||||
joi@^17.2.1:
|
||||
version "17.3.0"
|
||||
resolved "https://registry.yarnpkg.com/joi/-/joi-17.3.0.tgz#f1be4a6ce29bc1716665819ac361dfa139fff5d2"
|
||||
integrity sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
"@hapi/topo" "^5.0.0"
|
||||
"@sideway/address" "^4.1.0"
|
||||
"@sideway/formula" "^3.0.0"
|
||||
"@sideway/pinpoint" "^2.0.0"
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
@@ -3839,26 +3857,21 @@ media-typer@0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||
|
||||
medusa-core-utils@^0.3.0:
|
||||
version "0.1.39"
|
||||
resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-0.1.39.tgz#d57816c9bd43f9a92883650c1e66add1665291df"
|
||||
integrity sha512-R8+U1ile7if+nR6Cjh5exunx0ETV0OfkWUUBUpz1KmHSDv0V0CcvQqU9lcZesPFDEbu3Y2iEjsCqidVA4nG2nQ==
|
||||
medusa-core-utils@^1.0.10:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.0.10.tgz#6175f1e428318205742621f78d387254fc85b8a1"
|
||||
integrity sha512-z12ITzPl5UpDJTILNXcMGD4yujSRkuvVtxkrvqCmA44IEyRj1hWZ0dGbVzuHj2wIahDHFFg66oq91IQH5QVsyg==
|
||||
dependencies:
|
||||
"@hapi/joi" "^16.1.8"
|
||||
joi "^17.2.1"
|
||||
joi-objectid "^3.0.1"
|
||||
|
||||
medusa-interfaces@^0.3.0:
|
||||
version "0.1.39"
|
||||
resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-0.1.39.tgz#633db3c8c6afd7fec9ae24496737369d840105cf"
|
||||
integrity sha512-byKIcK7o3L4shmGn+pgZAUyLrT991zCqK4jWXIleQJbGImQy6TmdXido+tEzFptVBJWMIQ8BWnP/b7r29D8EXA==
|
||||
dependencies:
|
||||
mongoose "^5.8.0"
|
||||
|
||||
medusa-test-utils@^0.3.0:
|
||||
version "0.1.39"
|
||||
resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-0.1.39.tgz#b7c166006a2fa4f02e52ab3bfafc19a3ae787f3e"
|
||||
integrity sha512-M/Br8/HYvl7x2oLnme4NxdQwoyV0XUyOWiCyvPp7q1HUTB684lhJf1MikZVrcSjsh2L1rpyi3GRbKdf4cpJWvw==
|
||||
medusa-test-utils@^1.0.11:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.0.11.tgz#bae901efa90426fb64818de700dc6e163820160f"
|
||||
integrity sha512-CSNb70sXOfKTndzxWtPMYq+KeYaFkSLAPUyhuwDNVvkbne0faOYF5OMCV8r3aLoQ3D1Tvjrb/XB9Qt17ycjuPw==
|
||||
dependencies:
|
||||
"@babel/plugin-transform-classes" "^7.9.5"
|
||||
medusa-core-utils "^1.0.10"
|
||||
mongoose "^5.8.0"
|
||||
|
||||
memory-pager@^1.0.2:
|
||||
|
||||
@@ -11,6 +11,7 @@ Sends orders to Brightpearl, listens for stock movements, handles returns.
|
||||
event_owner: [the id of the user who will own goods out events] (required),
|
||||
warehouse: [the warehouse id to allocate orders from] (required)
|
||||
default_status_id: [the status id to assign new orders with] (optional: defaults to 1)
|
||||
swap_status_id: [the status id to assign new swaps] (optional: defaults to 1)
|
||||
payment_method_code: [the method code to register payments with] (optional: defaults to 1220)
|
||||
sales_account_code: [nominal code to assign line items to] (optional: defaults to 4000)
|
||||
shipping_account_code: [nominal code to assign shipping line to] (optional: defaults to 4040)
|
||||
|
||||
@@ -10,6 +10,7 @@ class BrightpearlService extends BaseService {
|
||||
productVariantService,
|
||||
regionService,
|
||||
orderService,
|
||||
swapService,
|
||||
discountService,
|
||||
},
|
||||
options
|
||||
@@ -23,6 +24,7 @@ class BrightpearlService extends BaseService {
|
||||
this.totalsService_ = totalsService
|
||||
this.discountService_ = discountService
|
||||
this.oauthService_ = oauthService
|
||||
this.swapService_ = swapService
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
@@ -456,10 +458,9 @@ class BrightpearlService extends BaseService {
|
||||
.create(order)
|
||||
.then(async (salesOrderId) => {
|
||||
const order = await client.orders.retrieve(salesOrderId)
|
||||
const resResult = await client.warehouses.createReservation(
|
||||
order,
|
||||
this.options.warehouse
|
||||
)
|
||||
await client.warehouses
|
||||
.createReservation(order, this.options.warehouse)
|
||||
.catch(() => {})
|
||||
return salesOrderId
|
||||
})
|
||||
.then((salesOrderId) => {
|
||||
@@ -471,6 +472,190 @@ class BrightpearlService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
async createSwapPayment(fromSwap) {
|
||||
const client = await this.getClient()
|
||||
const soId =
|
||||
fromSwap.metadata && fromSwap.metadata.brightpearl_sales_order_id
|
||||
|
||||
if (!soId) {
|
||||
return
|
||||
}
|
||||
|
||||
const paymentType = "RECEIPT"
|
||||
const paymentMethod = fromSwap.payment_method
|
||||
const payment = {
|
||||
transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref
|
||||
transactionCode: fromSwap._id,
|
||||
paymentMethodCode: this.options.payment_method_code || "1220",
|
||||
orderId: soId,
|
||||
paymentDate: new Date(),
|
||||
currencyIsoCode: fromSwap.currency_code,
|
||||
amountPaid: fromSwap.amount_paid,
|
||||
paymentType,
|
||||
}
|
||||
|
||||
await client.payments.create(payment)
|
||||
}
|
||||
|
||||
async createSwapOrder(fromOrder, fromSwap) {
|
||||
const client = await this.getClient()
|
||||
let customer = await this.retrieveCustomerByEmail(fromOrder.email)
|
||||
|
||||
if (!customer) {
|
||||
customer = await this.createCustomer(fromOrder)
|
||||
}
|
||||
|
||||
const authData = await this.getAuthData()
|
||||
|
||||
const sIndex = fromOrder.swaps.findIndex((s) => fromSwap._id.equals(s))
|
||||
|
||||
const { shipping_address } = fromSwap
|
||||
const order = {
|
||||
currency: {
|
||||
code: fromOrder.currency_code,
|
||||
},
|
||||
ref: `${fromOrder.display_id}-S${sIndex + 1}`,
|
||||
externalRef: `${fromOrder._id}.${fromSwap._id}`,
|
||||
channelId: this.options.channel_id || `1`,
|
||||
installedIntegrationInstanceId: authData.installation_instance_id,
|
||||
statusId:
|
||||
this.options.swap_status_id || this.options.default_status_id || `3`,
|
||||
customer: {
|
||||
id: customer.contactId,
|
||||
address: {
|
||||
addressFullName: `${shipping_address.first_name} ${shipping_address.last_name}`,
|
||||
addressLine1: shipping_address.address_1,
|
||||
addressLine2: shipping_address.address_2,
|
||||
postalCode: shipping_address.postal_code,
|
||||
countryIsoCode: shipping_address.country_code,
|
||||
telephone: shipping_address.phone,
|
||||
email: fromOrder.email,
|
||||
},
|
||||
},
|
||||
delivery: {
|
||||
shippingMethodId: 0,
|
||||
address: {
|
||||
addressFullName: `${shipping_address.first_name} ${shipping_address.last_name}`,
|
||||
addressLine1: shipping_address.address_1,
|
||||
addressLine2: shipping_address.address_2,
|
||||
postalCode: shipping_address.postal_code,
|
||||
countryIsoCode: shipping_address.country_code,
|
||||
telephone: shipping_address.phone,
|
||||
email: fromOrder.email,
|
||||
},
|
||||
},
|
||||
rows: await this.getBrightpearlRows({
|
||||
region_id: fromOrder.region_id,
|
||||
discounts: [],
|
||||
tax_rate: fromOrder.tax_rate,
|
||||
items: fromSwap.additional_items,
|
||||
shipping_methods: fromSwap.shipping_methods,
|
||||
return_shipping: fromSwap.return_shipping,
|
||||
}),
|
||||
}
|
||||
|
||||
return client.orders.create(order).then(async (salesOrderId) => {
|
||||
const order = await client.orders.retrieve(salesOrderId)
|
||||
await client.warehouses.createReservation(order, this.options.warehouse)
|
||||
|
||||
const paymentMethod = fromOrder.payment_method
|
||||
const paymentType = "RECEIPT"
|
||||
const payment = {
|
||||
transactionRef: `${paymentMethod._id}.${paymentType}-${fromSwap._id}`,
|
||||
transactionCode: fromOrder._id,
|
||||
paymentMethodCode: this.options.payment_method_code || "1220",
|
||||
orderId: salesOrderId,
|
||||
currencyIsoCode: fromOrder.currency_code,
|
||||
amountPaid: fromSwap.return.refund_amount,
|
||||
paymentDate: new Date(),
|
||||
paymentType,
|
||||
}
|
||||
|
||||
await client.payments.create(payment)
|
||||
|
||||
return this.swapService_.setMetadata(
|
||||
fromSwap._id,
|
||||
"brightpearl_sales_order_id",
|
||||
salesOrderId
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async createSwapCredit(fromOrder, fromSwap) {
|
||||
const region = await this.regionService_.retrieve(fromOrder.region_id)
|
||||
const client = await this.getClient()
|
||||
const authData = await this.getAuthData()
|
||||
const orderId = fromOrder.metadata.brightpearl_sales_order_id
|
||||
const sIndex = fromOrder.swaps.findIndex((s) => fromSwap._id.equals(s))
|
||||
|
||||
if (orderId) {
|
||||
const parentSo = await client.orders.retrieve(orderId)
|
||||
const order = {
|
||||
currency: parentSo.currency,
|
||||
ref: `${parentSo.ref}-S${sIndex + 1}`,
|
||||
externalRef: `${parentSo.externalRef}.${fromSwap._id}`,
|
||||
channelId: this.options.channel_id || `1`,
|
||||
installedIntegrationInstanceId: authData.installation_instance_id,
|
||||
customer: parentSo.customer,
|
||||
delivery: parentSo.delivery,
|
||||
parentId: orderId,
|
||||
rows: fromSwap.return.items.map((i) => {
|
||||
const parentRow = parentSo.rows.find((row) => {
|
||||
return row.externalRef === i.item_id
|
||||
})
|
||||
return {
|
||||
net: this.totalsService_.rounded(
|
||||
(parentRow.net / parentRow.quantity) * i.quantity
|
||||
),
|
||||
tax: this.totalsService_.rounded(
|
||||
(parentRow.tax / parentRow.quantity) * i.quantity
|
||||
),
|
||||
productId: parentRow.productId,
|
||||
taxCode: parentRow.taxCode,
|
||||
externalRef: parentRow.externalRef,
|
||||
nominalCode: parentRow.nominalCode,
|
||||
quantity: i.quantity,
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
if (fromSwap.return_shipping && fromSwap.return_shipping.price) {
|
||||
order.rows.push({
|
||||
name: "Return Shipping",
|
||||
quantity: 1,
|
||||
taxCode: region.tax_code,
|
||||
net: -1 * fromSwap.return_shipping.price,
|
||||
tax: -1 * fromSwap.return_shipping.price * fromOrder.tax_rate,
|
||||
nominalCode: this.options.shipping_account_code || "4040",
|
||||
})
|
||||
}
|
||||
|
||||
const total = order.rows.reduce((acc, next) => {
|
||||
return acc + next.net + next.tax
|
||||
}, 0)
|
||||
|
||||
return client.orders
|
||||
.createCredit(order)
|
||||
.then(async (creditId) => {
|
||||
const paymentMethod = fromOrder.payment_method
|
||||
const paymentType = "PAYMENT"
|
||||
const payment = {
|
||||
transactionRef: `${paymentMethod._id}.${paymentType}-${fromSwap._id}`,
|
||||
transactionCode: fromSwap._id,
|
||||
paymentMethodCode: this.options.payment_method_code || "1220",
|
||||
orderId: creditId,
|
||||
currencyIsoCode: fromSwap.currency_code,
|
||||
amountPaid: total,
|
||||
paymentDate: new Date(),
|
||||
paymentType,
|
||||
}
|
||||
|
||||
await client.payments.create(payment)
|
||||
})
|
||||
.catch((err) => console.log(err.response.data.errors))
|
||||
}
|
||||
}
|
||||
|
||||
async createPayment(fromOrder) {
|
||||
const client = await this.getClient()
|
||||
const soId =
|
||||
@@ -484,7 +669,7 @@ class BrightpearlService extends BaseService {
|
||||
const payment = {
|
||||
transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref
|
||||
transactionCode: fromOrder._id,
|
||||
paymentMethodCode: "1220",
|
||||
paymentMethodCode: this.options.payment_method_code || "1220",
|
||||
orderId: soId,
|
||||
paymentDate: new Date(),
|
||||
currencyIsoCode: fromOrder.currency_code,
|
||||
@@ -570,6 +755,7 @@ class BrightpearlService extends BaseService {
|
||||
nominalCode: this.options.shipping_account_code || "4040",
|
||||
})
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
@@ -630,6 +816,16 @@ class BrightpearlService extends BaseService {
|
||||
})
|
||||
.filter((i) => !!i)
|
||||
|
||||
// Orders with a concatenated externalReference are swap orders
|
||||
const [orderId, swapId] = order.externalRef.split(".")
|
||||
|
||||
if (swapId) {
|
||||
const order = await this.orderService_.retrieve(orderId)
|
||||
return this.swapService_.createFulfillment(order, swapId, {
|
||||
goods_out_note: id,
|
||||
})
|
||||
}
|
||||
|
||||
return this.orderService_.createFulfillment(order.externalRef, lines, {
|
||||
goods_out_note: id,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
class OrderSubscriber {
|
||||
constructor({ eventBusService, orderService, brightpearlService }) {
|
||||
constructor({
|
||||
eventBusService,
|
||||
orderService,
|
||||
swapService,
|
||||
brightpearlService,
|
||||
}) {
|
||||
this.orderService_ = orderService
|
||||
this.brightpearlService_ = brightpearlService
|
||||
this.swapService_ = swapService
|
||||
|
||||
eventBusService.subscribe("order.refund_created", this.registerRefund)
|
||||
|
||||
@@ -15,6 +21,11 @@ class OrderSubscriber {
|
||||
)
|
||||
|
||||
eventBusService.subscribe("order.shipment_created", this.registerShipment)
|
||||
eventBusService.subscribe("swap.shipment_created", this.registerShipment)
|
||||
|
||||
// Before we initiate a swap we wait for the payment and the return
|
||||
eventBusService.subscribe("swap.payment_completed", this.registerSwap)
|
||||
eventBusService.subscribe("order.swap_received", this.registerSwap)
|
||||
}
|
||||
|
||||
sendToBrightpearl = (order) => {
|
||||
@@ -25,8 +36,38 @@ class OrderSubscriber {
|
||||
return this.brightpearlService_.createPayment(order)
|
||||
}
|
||||
|
||||
registerSwap = async (data) => {
|
||||
const { order, swap, swap_id } = data
|
||||
|
||||
if (!order && !swap) {
|
||||
return
|
||||
}
|
||||
|
||||
let fromOrder = order
|
||||
if (!fromOrder) {
|
||||
fromOrder = await this.orderService_.retrieve(swap.order_id)
|
||||
}
|
||||
|
||||
let fromSwap
|
||||
if (swap) {
|
||||
fromSwap = await this.swapService_.retrieve(swap._id)
|
||||
} else {
|
||||
fromSwap = await this.swapService_.retrieve(swap_id)
|
||||
}
|
||||
|
||||
if (!(fromSwap.is_paid && fromSwap.return.status === "received")) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.brightpearlService_.createSwapCredit(fromOrder, fromSwap)
|
||||
await this.brightpearlService_.createSwapOrder(fromOrder, fromSwap)
|
||||
|
||||
const paySwap = await this.swapService_.retrieve(fromSwap._id)
|
||||
await this.brightpearlService_.createSwapPayment(paySwap)
|
||||
}
|
||||
|
||||
registerShipment = async (data) => {
|
||||
const { order_id, shipment } = data
|
||||
const { shipment } = data
|
||||
const noteId = shipment.metadata.goods_out_note
|
||||
if (noteId) {
|
||||
await this.brightpearlService_.registerGoodsOutTrackingNumber(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const customerService = req.scope.resolve("customerService")
|
||||
let customer = await customerService.retrieve(id)
|
||||
customer = await customerService.decorate(
|
||||
@@ -12,9 +13,14 @@ export default async (req, res) => {
|
||||
"shipping_addresses",
|
||||
"phone",
|
||||
],
|
||||
["orders"]
|
||||
[]
|
||||
)
|
||||
|
||||
customer.orders = await Promise.all(customer.orders.map(async oId => {
|
||||
const order = await orderService.retrieve(oId)
|
||||
return orderService.decorate(order, [], [])
|
||||
}))
|
||||
|
||||
res.json({ customer })
|
||||
} catch (err) {
|
||||
throw err
|
||||
|
||||
@@ -16,11 +16,18 @@ export default async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const customerService = req.scope.resolve("customerService")
|
||||
await customerService.update(id, value)
|
||||
|
||||
const customer = await customerService.retrieve(id)
|
||||
const data = await customerService.decorate(customer)
|
||||
data.orders = await Promise.all(
|
||||
customer.orders.map(async oId => {
|
||||
const order = await orderService.retrieve(oId)
|
||||
return orderService.decorate(order, [], [])
|
||||
})
|
||||
)
|
||||
res.status(200).json({ customer: data })
|
||||
} catch (err) {
|
||||
throw err
|
||||
|
||||
@@ -4,7 +4,11 @@ export default async (req, res) => {
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
let order = await orderService.archive(id)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
order = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
res.json({ order })
|
||||
} catch (error) {
|
||||
throw error
|
||||
|
||||
@@ -4,7 +4,11 @@ export default async (req, res) => {
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
let order = await orderService.cancel(id)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
order = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
res.json({ order })
|
||||
} catch (error) {
|
||||
throw error
|
||||
|
||||
@@ -4,7 +4,11 @@ export default async (req, res) => {
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
let order = await orderService.capturePayment(id)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
order = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
res.json({ order })
|
||||
} catch (error) {
|
||||
throw error
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
export default async (req, res) => {
|
||||
const { id, swap_id } = req.params
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const swapService = req.scope.resolve("swapService")
|
||||
|
||||
const order = await orderService.retrieve(id)
|
||||
|
||||
await swapService.capturePayment(swap_id)
|
||||
|
||||
// Decorate the order
|
||||
const data = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
|
||||
res.json({ order: data })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ export default async (req, res) => {
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
let order = await orderService.completeOrder(id)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
order = await orderService.decorate(order, [], ["region", "customer", "swaps"])
|
||||
res.json({ order })
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
|
||||
@@ -26,8 +26,14 @@ export default async (req, res) => {
|
||||
value.items,
|
||||
value.metadata
|
||||
)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
res.json({ order })
|
||||
|
||||
const data = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
|
||||
res.json({ order: data })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -22,7 +22,11 @@ export default async (req, res) => {
|
||||
value.fulfillment_id,
|
||||
value.tracking_numbers
|
||||
)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
order = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
res.json({ order })
|
||||
} catch (error) {
|
||||
throw error
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id, swap_id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
fulfillment_id: Validator.string().required(),
|
||||
tracking_numbers: Validator.array()
|
||||
.items(Validator.string())
|
||||
.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 swapService = req.scope.resolve("swapService")
|
||||
|
||||
const order = await orderService.retrieve(id)
|
||||
|
||||
await swapService.createShipment(
|
||||
swap_id,
|
||||
value.fulfillment_id,
|
||||
value.tracking_numbers
|
||||
)
|
||||
|
||||
// Decorate the order
|
||||
const data = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
|
||||
res.json({ order: data })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
return_items: Validator.array()
|
||||
.items({
|
||||
item_id: Validator.string().required(),
|
||||
quantity: Validator.number().required(),
|
||||
})
|
||||
.required(),
|
||||
return_shipping: Validator.object()
|
||||
.keys({
|
||||
id: Validator.string().optional(),
|
||||
price: Validator.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
additional_items: Validator.array().items({
|
||||
variant_id: Validator.string().required(),
|
||||
quantity: Validator.number().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 swapService = req.scope.resolve("swapService")
|
||||
|
||||
let order = await orderService.retrieve(id)
|
||||
|
||||
// Phase 1: Create swap and add it to the order
|
||||
const swap = await swapService.create(
|
||||
order,
|
||||
value.return_items,
|
||||
value.additional_items,
|
||||
value.return_shipping
|
||||
)
|
||||
|
||||
await orderService.registerSwapCreated(id, swap._id)
|
||||
|
||||
// --> swap_created
|
||||
// Phase 2: Create a return request from the swap
|
||||
await swapService.requestReturn(order, swap._id)
|
||||
|
||||
// --> return_request_created
|
||||
// Phase 3: Create a cart that can be used to pay for the swap difference
|
||||
await swapService.createCart(order, swap._id)
|
||||
|
||||
// --> finished
|
||||
|
||||
order = await orderService.retrieve(id)
|
||||
const data = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
|
||||
res.status(200).json({ order: data })
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id, swap_id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
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 swapService = req.scope.resolve("swapService")
|
||||
|
||||
// Fetch the order
|
||||
const order = await orderService.retrieve(id)
|
||||
|
||||
// Receive the return
|
||||
await swapService.createFulfillment(order, swap_id, value.metadata)
|
||||
|
||||
// Decorate the order
|
||||
const data = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
|
||||
res.status(200).json({ order: data })
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,11 @@ export default async (req, res) => {
|
||||
const customerService = req.scope.resolve("customerService")
|
||||
|
||||
let order = await orderService.retrieve(id)
|
||||
order = await orderService.decorate(order, [], ["region", "customer"])
|
||||
|
||||
if (order.customer_id) {
|
||||
order.customer = await customerService.retrieve(order.customer_id)
|
||||
}
|
||||
order = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
|
||||
res.json({ order })
|
||||
} catch (error) {
|
||||
|
||||
@@ -95,6 +95,43 @@ export default app => {
|
||||
middlewares.wrap(require("./archive-order").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a swap, requests a return and prepares a cart for payment.
|
||||
*/
|
||||
route.post("/:id/swaps", middlewares.wrap(require("./create-swap").default))
|
||||
|
||||
/**
|
||||
* Receives the inventory to return from a swap
|
||||
*/
|
||||
route.post(
|
||||
"/:id/swaps/:swap_id/receive",
|
||||
middlewares.wrap(require("./receive-swap").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Fulfills a swap.
|
||||
*/
|
||||
route.post(
|
||||
"/:id/swaps/:swap_id/fulfillments",
|
||||
middlewares.wrap(require("./fulfill-swap").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Marks a swap fulfillment as shipped.
|
||||
*/
|
||||
route.post(
|
||||
"/:id/swaps/:swap_id/shipments",
|
||||
middlewares.wrap(require("./create-swap-shipment").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Captures the payment associated with a swap
|
||||
*/
|
||||
route.post(
|
||||
"/:id/swaps/:swap_id/capture",
|
||||
middlewares.wrap(require("./capture-swap").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Set metadata key / value pair.
|
||||
*/
|
||||
|
||||
@@ -25,14 +25,18 @@ export default async (req, res) => {
|
||||
if (typeof value.refund !== "undefined" && value.refund < 0) {
|
||||
refundAmount = 0
|
||||
}
|
||||
let order = await orderService.return(
|
||||
let order = await orderService.receiveReturn(
|
||||
id,
|
||||
return_id,
|
||||
value.items,
|
||||
refundAmount,
|
||||
true
|
||||
)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
order = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
|
||||
res.status(200).json({ order })
|
||||
} catch (err) {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id, swap_id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
items: Validator.array()
|
||||
.items({
|
||||
item_id: Validator.string().required(),
|
||||
quantity: Validator.number().required(),
|
||||
})
|
||||
.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 swapService = req.scope.resolve("swapService")
|
||||
|
||||
// Fetch the order
|
||||
let order = await orderService.retrieve(id)
|
||||
|
||||
// Receive the return
|
||||
await swapService.receiveReturn(order, swap_id, value.items)
|
||||
|
||||
// Register swap reception
|
||||
order = await orderService.registerSwapReceived(id, swap_id)
|
||||
|
||||
// Decorate the order
|
||||
const data = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
|
||||
res.status(200).json({ order: data })
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,11 @@ export default async (req, res) => {
|
||||
value.reason,
|
||||
value.note
|
||||
)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
order = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
|
||||
res.status(200).json({ order })
|
||||
} catch (err) {
|
||||
|
||||
@@ -66,7 +66,11 @@ export default async (req, res) => {
|
||||
)
|
||||
}
|
||||
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
order = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
|
||||
res.status(200).json({ order })
|
||||
} catch (err) {
|
||||
|
||||
@@ -16,7 +16,11 @@ export default async (req, res) => {
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
let order = await orderService.setMetadata(id, value.key, value.value)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
order = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
|
||||
res.status(200).json({ order })
|
||||
} catch (err) {
|
||||
|
||||
@@ -32,7 +32,11 @@ export default async (req, res) => {
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
let order = await orderService.update(id, value)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
order = await orderService.decorate(
|
||||
order,
|
||||
[],
|
||||
["region", "customer", "swaps"]
|
||||
)
|
||||
|
||||
res.status(200).json({ order })
|
||||
} catch (err) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from "lodash"
|
||||
|
||||
export default async (req, res) => {
|
||||
try {
|
||||
const variantService = req.scope.resolve("productVariantService")
|
||||
const productService = req.scope.resolve("productService")
|
||||
const queryBuilderService = req.scope.resolve("queryBuilderService")
|
||||
|
||||
@@ -14,6 +15,22 @@ export default async (req, res) => {
|
||||
query.is_giftcard = req.query.is_giftcard === "true"
|
||||
}
|
||||
|
||||
let variantMatches = []
|
||||
if ("q" in req.query) {
|
||||
let textQ = req.query.q
|
||||
variantMatches = await variantService.list({
|
||||
$or: [
|
||||
{ sku: new RegExp(textQ, "i") },
|
||||
{ title: new RegExp(textQ, "i") },
|
||||
],
|
||||
})
|
||||
|
||||
query.$or = [
|
||||
...query.$or,
|
||||
{ variants: { $in: variantMatches.map(({ _id }) => _id.toString()) } },
|
||||
]
|
||||
}
|
||||
|
||||
const limit = parseInt(req.query.limit) || 0
|
||||
const offset = parseInt(req.query.offset) || 0
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { MedusaError, Validator } from "medusa-core-utils"
|
||||
export default async (req, res) => {
|
||||
const schema = Validator.object().keys({
|
||||
name: Validator.string(),
|
||||
swap_link_template: Validator.string(),
|
||||
default_currency: Validator.string(),
|
||||
currencies: Validator.array().items(Validator.string()),
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import orderRoutes from "./orders"
|
||||
import customerRoutes from "./customers"
|
||||
import shippingOptionRoutes from "./shipping-options"
|
||||
import regionRoutes from "./regions"
|
||||
import swapRoutes from "./swaps"
|
||||
|
||||
const route = Router()
|
||||
|
||||
@@ -33,6 +34,7 @@ export default (app, container, config) => {
|
||||
cartRoutes(route, container)
|
||||
shippingOptionRoutes(route)
|
||||
regionRoutes(route)
|
||||
swapRoutes(route)
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const schema = Validator.object().keys({
|
||||
cart_id: Validator.string().required(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
const swapService = req.scope.resolve("swapService")
|
||||
|
||||
try {
|
||||
const swap = await swapService.retrieveByCartId(value.cart_id)
|
||||
const data = await swapService.registerCartCompletion(
|
||||
swap._id,
|
||||
value.cart_id
|
||||
)
|
||||
|
||||
res.status(200).json({ swap: data })
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default async (req, res) => {
|
||||
const { cart_id } = req.params
|
||||
|
||||
try {
|
||||
const swapService = req.scope.resolve("swapService")
|
||||
const swap = await swapService.retrieveByCartId(cart_id)
|
||||
res.json({ swap })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Router } from "express"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default app => {
|
||||
app.use("/swaps", route)
|
||||
|
||||
route.get(
|
||||
"/:cart_id",
|
||||
middlewares.wrap(require("./get-swap-by-cart").default)
|
||||
)
|
||||
route.post("/", middlewares.wrap(require("./create-swap").default))
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -74,6 +74,7 @@ export const orders = {
|
||||
processedOrder: {
|
||||
_id: IdMap.getId("processed-order"),
|
||||
email: "oliver@test.dk",
|
||||
tax_rate: 0,
|
||||
billing_address: {
|
||||
first_name: "Oli",
|
||||
last_name: "Medusa",
|
||||
|
||||
@@ -29,6 +29,8 @@ class CartModel extends BaseModel {
|
||||
shipping_options: { type: [ShippingMethodSchema], default: [] },
|
||||
payment_method: { type: PaymentMethodSchema },
|
||||
shipping_methods: { type: [ShippingMethodSchema], default: [] },
|
||||
is_swap: { type: Boolean, default: false },
|
||||
created: { type: String, default: Date.now },
|
||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import PaymentMethodSchema from "./schemas/payment-method"
|
||||
import ShippingMethodSchema from "./schemas/shipping-method"
|
||||
import AddressSchema from "./schemas/address"
|
||||
import DiscountSchema from "./schemas/discount"
|
||||
import ShipmentSchema from "./schemas/shipment"
|
||||
import ReturnSchema from "./schemas/return"
|
||||
import RefundSchema from "./schemas/refund"
|
||||
import FulfillmentSchema from "./schemas/fulfillment"
|
||||
@@ -35,8 +34,9 @@ class OrderModel extends BaseModel {
|
||||
region_id: { type: String, required: true },
|
||||
discounts: { type: [DiscountSchema], default: [] },
|
||||
customer_id: { type: String },
|
||||
payment_method: { type: PaymentMethodSchema, default: {} },
|
||||
shipping_methods: { type: [ShippingMethodSchema], default: [] },
|
||||
payment_method: { type: PaymentMethodSchema, required: true },
|
||||
shipping_methods: { type: [ShippingMethodSchema], required: true },
|
||||
swaps: { type: [String], default: [] },
|
||||
documents: { type: [String], default: [] },
|
||||
created: { type: String, default: Date.now },
|
||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
|
||||
@@ -13,7 +13,7 @@ import mongoose from "mongoose"
|
||||
* @property {Object} metadata
|
||||
*/
|
||||
export default new mongoose.Schema({
|
||||
item_id: { type: String, required: true, unique: true },
|
||||
item_id: { type: String, required: true },
|
||||
content: { type: mongoose.Schema.Types.Mixed, required: true },
|
||||
quantity: { type: Number, required: true },
|
||||
is_requested: { type: Boolean, required: true },
|
||||
|
||||
@@ -9,6 +9,7 @@ class StoreModel extends BaseModel {
|
||||
currencies: { type: [String], default: [] },
|
||||
payment_providers: { type: [String], default: [] },
|
||||
fulfillment_providers: { type: [String], default: [] },
|
||||
swap_link_template: { type: String },
|
||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import mongoose from "mongoose"
|
||||
import { BaseModel } from "medusa-interfaces"
|
||||
|
||||
import LineItemSchema from "./schemas/line-item"
|
||||
import ReturnSchema from "./schemas/return"
|
||||
import FulfillmentSchema from "./schemas/fulfillment"
|
||||
import PaymentMethodSchema from "./schemas/payment-method"
|
||||
import ShippingMethodSchema from "./schemas/shipping-method"
|
||||
import AddressSchema from "./schemas/address"
|
||||
|
||||
class SwapModel extends BaseModel {
|
||||
static modelName = "Swap"
|
||||
static schema = {
|
||||
fulfillment_status: { type: String, default: "not_fulfilled" },
|
||||
payment_status: { type: String, default: "awaiting" },
|
||||
is_paid: { type: Boolean, default: false },
|
||||
return: { type: ReturnSchema },
|
||||
return_items: { type: [mongoose.Schema.Types.Mixed], required: true },
|
||||
return_shipping: { type: mongoose.Schema.Types.Mixed },
|
||||
fulfillments: { type: [FulfillmentSchema] },
|
||||
additional_items: { type: [LineItemSchema], required: true },
|
||||
payment_method: { type: PaymentMethodSchema },
|
||||
shipping_methods: { type: [ShippingMethodSchema] },
|
||||
shipping_address: { type: AddressSchema },
|
||||
amount_paid: { type: Number },
|
||||
region_id: { type: String },
|
||||
currency_code: { type: String },
|
||||
order_id: { type: String },
|
||||
cart_id: { type: String },
|
||||
created: { type: String, default: Date.now },
|
||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
}
|
||||
}
|
||||
|
||||
export default SwapModel
|
||||
@@ -115,6 +115,7 @@ export const orders = {
|
||||
profile_id: IdMap.getId("profile1"),
|
||||
},
|
||||
],
|
||||
tax_rate: 0,
|
||||
fulfillment_status: "fulfilled",
|
||||
payment_status: "captured",
|
||||
status: "completed",
|
||||
|
||||
@@ -148,6 +148,58 @@ describe("CartService", () => {
|
||||
region_id: IdMap.getId("testRegion"),
|
||||
})
|
||||
})
|
||||
|
||||
it("creates a cart with a prefilled shipping address", async () => {
|
||||
const res = cartService.create({
|
||||
region_id: IdMap.getId("testRegion"),
|
||||
shipping_address: {
|
||||
first_name: "LeBron",
|
||||
last_name: "James",
|
||||
address_1: "Dunk St",
|
||||
city: "Dunkville",
|
||||
province: "CA",
|
||||
postal_code: "12345",
|
||||
country_code: "PT",
|
||||
},
|
||||
})
|
||||
|
||||
await expect(res).rejects.toThrow("Shipping country not in region")
|
||||
})
|
||||
|
||||
it("creates a cart with a prefilled shipping address", async () => {
|
||||
await cartService.create({
|
||||
region_id: IdMap.getId("testRegion"),
|
||||
shipping_address: {
|
||||
first_name: "LeBron",
|
||||
last_name: "James",
|
||||
address_1: "Dunk St",
|
||||
city: "Dunkville",
|
||||
province: "CA",
|
||||
postal_code: "12345",
|
||||
country_code: "US",
|
||||
},
|
||||
})
|
||||
|
||||
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
|
||||
expect(EventBusServiceMock.emit).toHaveBeenCalledWith(
|
||||
"cart.created",
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
expect(CartModelMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(CartModelMock.create).toHaveBeenCalledWith({
|
||||
region_id: IdMap.getId("testRegion"),
|
||||
shipping_address: {
|
||||
first_name: "LeBron",
|
||||
last_name: "James",
|
||||
address_1: "Dunk St",
|
||||
city: "Dunkville",
|
||||
province: "CA",
|
||||
postal_code: "12345",
|
||||
country_code: "US",
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("addLineItem", () => {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { IdMap } from "medusa-test-utils"
|
||||
import { OrderModelMock, orders } from "../../models/__mocks__/order"
|
||||
import { carts } from "../../models/__mocks__/cart"
|
||||
import OrderService from "../order"
|
||||
import ReturnService from "../return"
|
||||
import FulfillmentService from "../fulfillment"
|
||||
import {
|
||||
PaymentProviderServiceMock,
|
||||
DefaultProviderMock,
|
||||
@@ -434,11 +436,15 @@ describe("OrderService", () => {
|
||||
})
|
||||
|
||||
describe("createFulfillment", () => {
|
||||
const fulfillmentService = new FulfillmentService({
|
||||
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||
shippingProfileService: ShippingProfileServiceMock,
|
||||
totalsService: TotalsServiceMock,
|
||||
})
|
||||
const orderService = new OrderService({
|
||||
orderModel: OrderModelMock,
|
||||
paymentProviderService: PaymentProviderServiceMock,
|
||||
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||
shippingProfileService: ShippingProfileServiceMock,
|
||||
fulfillmentService,
|
||||
eventBusService: EventBusServiceMock,
|
||||
})
|
||||
|
||||
@@ -509,8 +515,7 @@ describe("OrderService", () => {
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
fulfilled_quantity: 10,
|
||||
fulfilled: true,
|
||||
fulfilled_quantity: 0,
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
@@ -560,9 +565,17 @@ describe("OrderService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("return", () => {
|
||||
describe("receiveReturn", () => {
|
||||
const returnService = new ReturnService({
|
||||
totalsService: TotalsServiceMock,
|
||||
shippingOptionService: ShippingOptionServiceMock,
|
||||
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||
})
|
||||
const orderService = new OrderService({
|
||||
orderModel: OrderModelMock,
|
||||
returnService,
|
||||
shippingOptionService: ShippingOptionServiceMock,
|
||||
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||
paymentProviderService: PaymentProviderServiceMock,
|
||||
totalsService: TotalsServiceMock,
|
||||
eventBusService: EventBusServiceMock,
|
||||
@@ -573,7 +586,7 @@ describe("OrderService", () => {
|
||||
})
|
||||
|
||||
it("calls order model functions", async () => {
|
||||
await orderService.return(
|
||||
await orderService.receiveReturn(
|
||||
IdMap.getId("returned-order"),
|
||||
IdMap.getId("return"),
|
||||
[
|
||||
@@ -671,7 +684,7 @@ describe("OrderService", () => {
|
||||
})
|
||||
|
||||
it("return with custom refund", async () => {
|
||||
await orderService.return(
|
||||
await orderService.receiveReturn(
|
||||
IdMap.getId("returned-order"),
|
||||
IdMap.getId("return"),
|
||||
[
|
||||
@@ -770,7 +783,7 @@ describe("OrderService", () => {
|
||||
})
|
||||
|
||||
it("calls order model functions and sets partially_returned", async () => {
|
||||
await orderService.return(
|
||||
await orderService.receiveReturn(
|
||||
IdMap.getId("order-refund"),
|
||||
IdMap.getId("return"),
|
||||
[
|
||||
@@ -882,7 +895,7 @@ describe("OrderService", () => {
|
||||
})
|
||||
|
||||
it("sets requires_action on additional items", async () => {
|
||||
await orderService.return(
|
||||
await orderService.receiveReturn(
|
||||
IdMap.getId("order-refund"),
|
||||
IdMap.getId("return"),
|
||||
[
|
||||
@@ -930,17 +943,17 @@ describe("OrderService", () => {
|
||||
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("order-refund") },
|
||||
{ _id: IdMap.getId("order-refund"), "returns._id": originalReturn._id },
|
||||
{
|
||||
$set: {
|
||||
returns: [toSet],
|
||||
"returns.$": toSet,
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("sets requires_action on unmatcing quantities", async () => {
|
||||
await orderService.return(
|
||||
await orderService.receiveReturn(
|
||||
IdMap.getId("order-refund"),
|
||||
IdMap.getId("return"),
|
||||
[
|
||||
@@ -966,10 +979,10 @@ describe("OrderService", () => {
|
||||
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("order-refund") },
|
||||
{ _id: IdMap.getId("order-refund"), "returns._id": originalReturn._id },
|
||||
{
|
||||
$set: {
|
||||
returns: [toSet],
|
||||
"returns.$": toSet,
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -977,8 +990,14 @@ describe("OrderService", () => {
|
||||
})
|
||||
|
||||
describe("requestReturn", () => {
|
||||
const returnService = new ReturnService({
|
||||
totalsService: TotalsServiceMock,
|
||||
shippingOptionService: ShippingOptionServiceMock,
|
||||
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||
})
|
||||
const orderService = new OrderService({
|
||||
orderModel: OrderModelMock,
|
||||
returnService,
|
||||
shippingOptionService: ShippingOptionServiceMock,
|
||||
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||
paymentProviderService: PaymentProviderServiceMock,
|
||||
@@ -1110,9 +1129,15 @@ describe("OrderService", () => {
|
||||
})
|
||||
|
||||
describe("createShipment", () => {
|
||||
const fulfillmentService = new FulfillmentService({
|
||||
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||
shippingProfileService: ShippingProfileServiceMock,
|
||||
totalsService: TotalsServiceMock,
|
||||
})
|
||||
const orderService = new OrderService({
|
||||
orderModel: OrderModelMock,
|
||||
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||
fulfillmentService,
|
||||
eventBusService: EventBusServiceMock,
|
||||
})
|
||||
|
||||
@@ -1136,37 +1161,34 @@ describe("OrderService", () => {
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
"fulfillments.$": [
|
||||
{
|
||||
_id: IdMap.getId("fulfillment"),
|
||||
provider_id: "default_provider",
|
||||
tracking_numbers: ["1234", "2345"],
|
||||
data: {},
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
content: {
|
||||
product: {
|
||||
_id: IdMap.getId("validId"),
|
||||
},
|
||||
quantity: 1,
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
"fulfillments.$": {
|
||||
_id: IdMap.getId("fulfillment"),
|
||||
provider_id: "default_provider",
|
||||
tracking_numbers: ["1234", "2345"],
|
||||
data: {},
|
||||
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",
|
||||
fulfilled_quantity: 10,
|
||||
shipped_quantity: 10,
|
||||
quantity: 10,
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
title: "merge line",
|
||||
},
|
||||
],
|
||||
shipped_at: expect.anything(),
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
description: "This is a new line",
|
||||
fulfilled_quantity: 10,
|
||||
quantity: 10,
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
title: "merge line",
|
||||
},
|
||||
],
|
||||
shipped_at: expect.anything(),
|
||||
metadata: {},
|
||||
},
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
@@ -1181,6 +1203,7 @@ describe("OrderService", () => {
|
||||
},
|
||||
},
|
||||
description: "This is a new line",
|
||||
shipped: true,
|
||||
fulfilled_quantity: 10,
|
||||
shipped_quantity: 10,
|
||||
quantity: 10,
|
||||
@@ -1203,6 +1226,191 @@ describe("OrderService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("registerSwapCreated", () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
const orderModel = {
|
||||
findOne: jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ _id: IdMap.getId("order") })),
|
||||
updateOne: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
}
|
||||
|
||||
it("adds a swap to an order", async () => {
|
||||
const swapService = {
|
||||
retrieve: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
Promise.resolve({ _id: "1235", order_id: IdMap.getId("order") })
|
||||
),
|
||||
}
|
||||
const orderService = new OrderService({
|
||||
swapService,
|
||||
orderModel,
|
||||
eventBusService: { emit: jest.fn().mockReturnValue(Promise.resolve()) },
|
||||
})
|
||||
|
||||
const res = orderService.registerSwapCreated(IdMap.getId("order"), "1235")
|
||||
expect(res).resolves
|
||||
|
||||
await res
|
||||
expect(orderModel.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
_id: IdMap.getId("order"),
|
||||
},
|
||||
{
|
||||
$addToSet: { swaps: "1235" },
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("fails if order/swap relationship is not satisfied", async () => {
|
||||
const swapService = {
|
||||
retrieve: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
Promise.resolve({ _id: "1235", order_id: IdMap.getId("order_1") })
|
||||
),
|
||||
}
|
||||
const orderService = new OrderService({
|
||||
swapService,
|
||||
orderModel,
|
||||
eventBusService: { emit: jest.fn().mockReturnValue(Promise.resolve()) },
|
||||
})
|
||||
|
||||
const res = orderService.registerSwapCreated(IdMap.getId("order"), "1235")
|
||||
expect(res).rejects.toThrow("Swap must belong to the given order")
|
||||
})
|
||||
})
|
||||
|
||||
describe("registerSwapReceived", () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
const orderModel = {
|
||||
findOne: jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ _id: IdMap.getId("order") })),
|
||||
updateOne: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
}
|
||||
|
||||
it("fails if order/swap relationship not satisfied", async () => {
|
||||
const swapService = {
|
||||
retrieve: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
Promise.resolve({ _id: "1235", order_id: IdMap.getId("order_1") })
|
||||
),
|
||||
}
|
||||
const orderService = new OrderService({
|
||||
swapService,
|
||||
orderModel,
|
||||
eventBusService: { emit: jest.fn().mockReturnValue(Promise.resolve()) },
|
||||
})
|
||||
|
||||
const res = orderService.registerSwapReceived(
|
||||
IdMap.getId("order"),
|
||||
"1235"
|
||||
)
|
||||
await expect(res).rejects.toThrow("Swap must belong to the given order")
|
||||
})
|
||||
|
||||
it("fails if swap doesn't have status received", async () => {
|
||||
const swapService = {
|
||||
retrieve: jest.fn().mockReturnValue(
|
||||
Promise.resolve({
|
||||
_id: "1235",
|
||||
order_id: IdMap.getId("order"),
|
||||
return: { status: "requested" },
|
||||
})
|
||||
),
|
||||
}
|
||||
const orderService = new OrderService({
|
||||
swapService,
|
||||
orderModel,
|
||||
eventBusService: { emit: jest.fn().mockReturnValue(Promise.resolve()) },
|
||||
})
|
||||
|
||||
const res = orderService.registerSwapReceived(
|
||||
IdMap.getId("order"),
|
||||
"1235"
|
||||
)
|
||||
await expect(res).rejects.toThrow("Swap is not received")
|
||||
})
|
||||
|
||||
it("registers a swap as received", async () => {
|
||||
const model = {
|
||||
findOne: jest.fn().mockReturnValue(
|
||||
Promise.resolve({
|
||||
_id: IdMap.getId("order_123"),
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("1234"),
|
||||
returned_quantity: 0,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
updateOne: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
}
|
||||
const swapService = {
|
||||
retrieve: jest.fn().mockReturnValue(
|
||||
Promise.resolve({
|
||||
_id: "1235",
|
||||
order_id: IdMap.getId("order_123"),
|
||||
return: { status: "received" },
|
||||
return_items: [{ item_id: IdMap.getId("1234"), quantity: 1 }],
|
||||
})
|
||||
),
|
||||
}
|
||||
const orderService = new OrderService({
|
||||
swapService,
|
||||
orderModel: model,
|
||||
eventBusService: { emit: jest.fn().mockReturnValue(Promise.resolve()) },
|
||||
})
|
||||
|
||||
await orderService.registerSwapReceived(IdMap.getId("order"), "1235")
|
||||
|
||||
expect(model.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
_id: IdMap.getId("order_123"),
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("1234"),
|
||||
returned_quantity: 1,
|
||||
returned: true,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("fails if order/swap relationship is not satisfied", async () => {
|
||||
const swapService = {
|
||||
retrieve: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
Promise.resolve({ _id: "1235", order_id: IdMap.getId("order_1") })
|
||||
),
|
||||
}
|
||||
const orderService = new OrderService({
|
||||
swapService,
|
||||
orderModel,
|
||||
eventBusService: { emit: jest.fn().mockReturnValue(Promise.resolve()) },
|
||||
})
|
||||
|
||||
const res = orderService.registerSwapCreated(IdMap.getId("order"), "1235")
|
||||
expect(res).rejects.toThrow("Swap must belong to the given order")
|
||||
})
|
||||
})
|
||||
|
||||
describe("archive", () => {
|
||||
const orderService = new OrderService({
|
||||
orderModel: OrderModelMock,
|
||||
|
||||
@@ -0,0 +1,731 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
||||
import {
|
||||
// FulfillmentProviderServiceMock,
|
||||
DefaultProviderMock as FulfillmentProviderMock,
|
||||
} from "../__mocks__/fulfillment-provider"
|
||||
import SwapService from "../swap"
|
||||
|
||||
const generateOrder = (orderId, items, additional = {}) => {
|
||||
return {
|
||||
_id: IdMap.getId(orderId),
|
||||
items: items.map(
|
||||
({
|
||||
id,
|
||||
product_id,
|
||||
variant_id,
|
||||
fulfilled,
|
||||
returned,
|
||||
quantity,
|
||||
price,
|
||||
}) => ({
|
||||
_id: IdMap.getId(id),
|
||||
content: {
|
||||
product: {
|
||||
_id: IdMap.getId(product_id),
|
||||
},
|
||||
variant: {
|
||||
_id: IdMap.getId(variant_id),
|
||||
},
|
||||
unit_price: price,
|
||||
},
|
||||
quantity,
|
||||
fulfilled_quantity: fulfilled || 0,
|
||||
returned_quantity: returned || 0,
|
||||
})
|
||||
),
|
||||
...additional,
|
||||
}
|
||||
}
|
||||
|
||||
const testOrder = generateOrder(
|
||||
"test",
|
||||
[
|
||||
{
|
||||
id: "line",
|
||||
product_id: "product",
|
||||
variant_id: "variant",
|
||||
price: 100,
|
||||
quantity: 2,
|
||||
fulfilled: 1,
|
||||
},
|
||||
],
|
||||
{
|
||||
fulfillment_status: "fulfilled",
|
||||
payment_status: "captured",
|
||||
currency_code: "DKK",
|
||||
region_id: IdMap.getId("region"),
|
||||
tax_rate: 0,
|
||||
shipping_address: {
|
||||
first_name: "test",
|
||||
last_name: "testson",
|
||||
address_1: "1800 test st",
|
||||
city: "testville",
|
||||
province: "test",
|
||||
country_code: "us",
|
||||
postal_code: "12345",
|
||||
phone: "+18001231234",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const SwapModel = ({ create, updateOne, findOne } = {}) => {
|
||||
return {
|
||||
create: jest.fn().mockImplementation((...args) => {
|
||||
if (create) {
|
||||
return create(...args)
|
||||
}
|
||||
return Promise.resolve({ data: "swap" })
|
||||
}),
|
||||
updateOne: jest.fn().mockImplementation((...args) => {
|
||||
if (updateOne) {
|
||||
return updateOne(...args)
|
||||
}
|
||||
return Promise.resolve({ data: "swap" })
|
||||
}),
|
||||
findOne: jest.fn().mockImplementation((...args) => {
|
||||
if (findOne) {
|
||||
return findOne(...args)
|
||||
}
|
||||
return Promise.resolve({ data: "swap" })
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
describe("SwapService", () => {
|
||||
describe("validateReturnItems_", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("fails if item is returned", async () => {
|
||||
const swapService = new SwapService({})
|
||||
const res = () =>
|
||||
swapService.validateReturnItems_(
|
||||
{
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("line1"),
|
||||
quantity: 1,
|
||||
returned_quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
[{ item_id: IdMap.getId("line1"), quantity: 1 }]
|
||||
)
|
||||
|
||||
expect(res).toThrow("Cannot return more items than have been ordered")
|
||||
})
|
||||
|
||||
it("fails if item is returned", async () => {
|
||||
const swapService = new SwapService({})
|
||||
const res = () =>
|
||||
swapService.validateReturnItems_(
|
||||
{
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("line1"),
|
||||
quantity: 1,
|
||||
returned_quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
[{ item_id: IdMap.getId("line2"), quantity: 1 }]
|
||||
)
|
||||
|
||||
expect(res).toThrow("Item does not exist on order")
|
||||
})
|
||||
|
||||
it("successfully resolves", async () => {
|
||||
const swapService = new SwapService({})
|
||||
const res = swapService.validateReturnItems_(
|
||||
{
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("line1"),
|
||||
quantity: 1,
|
||||
returned_quantity: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
[{ item_id: IdMap.getId("line1"), quantity: 1 }]
|
||||
)
|
||||
|
||||
expect(res).toEqual([{ item_id: IdMap.getId("line1"), quantity: 1 }])
|
||||
})
|
||||
})
|
||||
|
||||
describe("createCart", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("success", () => {
|
||||
const existing = {
|
||||
_id: IdMap.getId("test-swap"),
|
||||
order_id: IdMap.getId("test"),
|
||||
return: {
|
||||
_id: IdMap.getId("return-swap"),
|
||||
test: "notreceived",
|
||||
refund_amount: 11,
|
||||
},
|
||||
return_items: [{ item_id: IdMap.getId("line"), quantity: 1 }],
|
||||
additional_items: [{ data: "lines" }],
|
||||
other: "data",
|
||||
}
|
||||
|
||||
const cartService = {
|
||||
create: jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ _id: IdMap.getId("swap-cart") })),
|
||||
}
|
||||
const swapModel = SwapModel({ findOne: () => Promise.resolve(existing) })
|
||||
const swapService = new SwapService({
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
swapModel,
|
||||
cartService,
|
||||
})
|
||||
|
||||
it("finds swap and calls return create cart", async () => {
|
||||
await swapService.createCart(testOrder, IdMap.getId("swap-1"))
|
||||
|
||||
expect(swapModel.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(swapModel.findOne).toHaveBeenCalledWith({
|
||||
_id: IdMap.getId("swap-1"),
|
||||
})
|
||||
|
||||
expect(cartService.create).toHaveBeenCalledTimes(1)
|
||||
expect(cartService.create).toHaveBeenCalledWith({
|
||||
email: testOrder.email,
|
||||
shipping_address: testOrder.shipping_address,
|
||||
billing_address: testOrder.billing_address,
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("line"),
|
||||
content: {
|
||||
variant: {
|
||||
_id: IdMap.getId("variant"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
unit_price: -100,
|
||||
},
|
||||
quantity: 1,
|
||||
fulfilled_quantity: 1,
|
||||
returned_quantity: 0,
|
||||
metadata: {
|
||||
is_return_line: true,
|
||||
},
|
||||
},
|
||||
...existing.additional_items,
|
||||
],
|
||||
region_id: testOrder.region_id,
|
||||
customer_id: testOrder.customer_id,
|
||||
is_swap: true,
|
||||
metadata: {
|
||||
swap_id: IdMap.getId("test-swap"),
|
||||
parent_order_id: IdMap.getId("test"),
|
||||
},
|
||||
})
|
||||
|
||||
expect(swapModel.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(swapModel.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("swap-1") },
|
||||
{ $set: { cart_id: IdMap.getId("swap-cart") } }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("failure", () => {
|
||||
const existing = {
|
||||
return: {
|
||||
_id: IdMap.getId("return-swap"),
|
||||
test: "notreceived",
|
||||
refund_amount: 11,
|
||||
},
|
||||
additional_items: [{ data: "lines" }],
|
||||
other: "data",
|
||||
}
|
||||
|
||||
it("fails if swap doesn't belong to order", async () => {
|
||||
const swapModel = SwapModel({
|
||||
findOne: () => Promise.resolve(existing),
|
||||
})
|
||||
const swapService = new SwapService({ swapModel })
|
||||
const res = swapService.createCart(testOrder, IdMap.getId("swap-1"))
|
||||
|
||||
await expect(res).rejects.toThrow(
|
||||
"The swap does not belong to the order"
|
||||
)
|
||||
})
|
||||
|
||||
it("fails if cart already created", async () => {
|
||||
const swapModel = SwapModel({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
...existing,
|
||||
order_id: IdMap.getId("test"),
|
||||
cart_id: IdMap.getId("swap-cart"),
|
||||
}),
|
||||
})
|
||||
const swapService = new SwapService({ swapModel })
|
||||
const res = swapService.createCart(testOrder, IdMap.getId("swap-1"))
|
||||
|
||||
await expect(res).rejects.toThrow(
|
||||
"A cart has already been created for the swap"
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("success", () => {
|
||||
const lineItemService = {
|
||||
generate: jest
|
||||
.fn()
|
||||
.mockImplementation((variantId, regionId, quantity, metadata) => {
|
||||
return {
|
||||
content: {
|
||||
unit_price: 100,
|
||||
variant: {
|
||||
_id: variantId,
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity,
|
||||
}
|
||||
}),
|
||||
}
|
||||
const swapModel = SwapModel()
|
||||
const swapService = new SwapService({
|
||||
swapModel,
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
lineItemService,
|
||||
})
|
||||
|
||||
it("generates lines items", async () => {
|
||||
await swapService.create(
|
||||
testOrder,
|
||||
[{ item_id: IdMap.getId("line"), quantity: 1 }],
|
||||
[{ variant_id: IdMap.getId("new-variant"), quantity: 1 }],
|
||||
{
|
||||
id: IdMap.getId("return-shipping"),
|
||||
price: 20,
|
||||
}
|
||||
)
|
||||
|
||||
expect(lineItemService.generate).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemService.generate).toHaveBeenCalledWith(
|
||||
IdMap.getId("new-variant"),
|
||||
IdMap.getId("region"),
|
||||
1
|
||||
)
|
||||
})
|
||||
|
||||
it("creates swap", async () => {
|
||||
await swapService.create(
|
||||
testOrder,
|
||||
[{ item_id: IdMap.getId("line"), quantity: 1 }],
|
||||
[{ variant_id: IdMap.getId("new-variant"), quantity: 1 }],
|
||||
{
|
||||
id: IdMap.getId("return-shipping"),
|
||||
price: 20,
|
||||
}
|
||||
)
|
||||
|
||||
expect(swapModel.create).toHaveBeenCalledWith({
|
||||
order_id: IdMap.getId("test"),
|
||||
return_items: [{ item_id: IdMap.getId("line"), quantity: 1 }],
|
||||
region_id: IdMap.getId("region"),
|
||||
currency_code: "DKK",
|
||||
return_shipping: {
|
||||
id: IdMap.getId("return-shipping"),
|
||||
price: 20,
|
||||
},
|
||||
additional_items: [
|
||||
{
|
||||
content: {
|
||||
unit_price: 100,
|
||||
variant: {
|
||||
_id: IdMap.getId("new-variant"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("receiveReturn", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("success", () => {
|
||||
const returnService = {
|
||||
receiveReturn: jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ test: "received" })),
|
||||
}
|
||||
|
||||
const existing = {
|
||||
order_id: IdMap.getId("test"),
|
||||
return: {
|
||||
_id: IdMap.getId("return-swap"),
|
||||
test: "notreceived",
|
||||
refund_amount: 11,
|
||||
},
|
||||
other: "data",
|
||||
}
|
||||
|
||||
const swapModel = SwapModel({ findOne: () => Promise.resolve(existing) })
|
||||
const swapService = new SwapService({ swapModel, returnService })
|
||||
|
||||
it("calls register return and updates return value", async () => {
|
||||
await swapService.receiveReturn(testOrder, IdMap.getId("swap"), [
|
||||
{ variant_id: IdMap.getId("1234"), quantity: 1 },
|
||||
])
|
||||
|
||||
expect(swapModel.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("swap") },
|
||||
{
|
||||
$set: {
|
||||
status: "received",
|
||||
return: { test: "received" },
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(returnService.receiveReturn).toHaveBeenCalledTimes(1)
|
||||
expect(returnService.receiveReturn).toHaveBeenCalledWith(
|
||||
testOrder,
|
||||
existing.return,
|
||||
[{ variant_id: IdMap.getId("1234"), quantity: 1 }],
|
||||
11,
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("failure", () => {
|
||||
const returnService = {
|
||||
receiveReturn: jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ status: "requires_action" })),
|
||||
}
|
||||
|
||||
const existing = {
|
||||
order_id: IdMap.getId("test"),
|
||||
return: {
|
||||
_id: IdMap.getId("return-swap"),
|
||||
test: "notreceived",
|
||||
refund_amount: 11,
|
||||
},
|
||||
other: "data",
|
||||
}
|
||||
|
||||
const swapModel = SwapModel({
|
||||
findOne: t =>
|
||||
Promise.resolve(t._id.equals(IdMap.getId("empty")) ? {} : existing),
|
||||
})
|
||||
const swapService = new SwapService({ swapModel, returnService })
|
||||
|
||||
it("fails if swap has no return request", async () => {
|
||||
const res = swapService.receiveReturn(
|
||||
testOrder,
|
||||
IdMap.getId("empty"),
|
||||
[]
|
||||
)
|
||||
await expect(res).rejects.toThrow("Swap has no return request")
|
||||
})
|
||||
|
||||
it("sets requires action if return fails", async () => {
|
||||
await swapService.receiveReturn(testOrder, IdMap.getId("swap"), [
|
||||
{ variant_id: IdMap.getId("1234"), quantity: 1 },
|
||||
])
|
||||
|
||||
expect(swapModel.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
_id: IdMap.getId("swap"),
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
status: "requires_action",
|
||||
return: { status: "requires_action" },
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(returnService.receiveReturn).toHaveBeenCalledTimes(1)
|
||||
expect(returnService.receiveReturn).toHaveBeenCalledWith(
|
||||
testOrder,
|
||||
existing.return,
|
||||
[{ variant_id: IdMap.getId("1234"), quantity: 1 }],
|
||||
11,
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("createFulfillment", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("success", () => {
|
||||
const fulfillmentService = {
|
||||
createFulfillment: jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve([{ data: "new" }])),
|
||||
}
|
||||
|
||||
const existing = {
|
||||
additional_items: [
|
||||
{
|
||||
_id: IdMap.getId("1234"),
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
shipping_methods: [{ method: "1" }],
|
||||
return: {
|
||||
_id: IdMap.getId("return-swap"),
|
||||
test: "notreceived",
|
||||
refund_amount: 11,
|
||||
},
|
||||
other: "data",
|
||||
}
|
||||
|
||||
const swapModel = SwapModel({ findOne: () => existing })
|
||||
const swapService = new SwapService({ swapModel, fulfillmentService })
|
||||
|
||||
it("creates a fulfillment", async () => {
|
||||
await swapService.createFulfillment(testOrder, IdMap.getId("swap"))
|
||||
|
||||
expect(swapModel.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
_id: IdMap.getId("swap"),
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
fulfillment_status: "fulfilled",
|
||||
fulfillments: [{ data: "new" }],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(fulfillmentService.createFulfillment).toHaveBeenCalledWith(
|
||||
{
|
||||
...existing,
|
||||
currency_code: testOrder.currency_code,
|
||||
tax_rate: testOrder.tax_rate,
|
||||
region_id: testOrder.region_id,
|
||||
display_id: testOrder.display_id,
|
||||
is_swap: true,
|
||||
billing_address: testOrder.billing_address,
|
||||
items: existing.additional_items,
|
||||
shipping_methods: existing.shipping_methods,
|
||||
},
|
||||
[{ item_id: IdMap.getId("1234"), quantity: 2 }],
|
||||
{}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("failure", () => {})
|
||||
})
|
||||
|
||||
describe("createShipment", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("success", () => {
|
||||
const fulfillmentService = {
|
||||
createShipment: jest.fn().mockImplementation((o, f) => {
|
||||
return Promise.resolve({ ...f, data: "new" })
|
||||
}),
|
||||
}
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
}
|
||||
|
||||
const existing = {
|
||||
additional_items: [
|
||||
{
|
||||
_id: IdMap.getId("1234-1"),
|
||||
quantity: 2,
|
||||
shipped_quantity: 0,
|
||||
},
|
||||
],
|
||||
fulfillments: [
|
||||
{
|
||||
_id: IdMap.getId("f1"),
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("1234-1"),
|
||||
item_id: IdMap.getId("1234-1"),
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_id: IdMap.getId("f2"),
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("1234-2"),
|
||||
item_id: IdMap.getId("1234-2"),
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
shipping_methods: [{ method: "1" }],
|
||||
return: {
|
||||
_id: IdMap.getId("return-swap"),
|
||||
test: "notreceived",
|
||||
refund_amount: 11,
|
||||
},
|
||||
other: "data",
|
||||
}
|
||||
const swapModel = SwapModel({
|
||||
updateOne: () => Promise.resolve(existing),
|
||||
findOne: () => existing,
|
||||
})
|
||||
const swapService = new SwapService({
|
||||
swapModel,
|
||||
eventBusService,
|
||||
fulfillmentService,
|
||||
})
|
||||
|
||||
it("creates a shipment", async () => {
|
||||
await swapService.createShipment(
|
||||
IdMap.getId("swap"),
|
||||
IdMap.getId("f1"),
|
||||
["1234"],
|
||||
{}
|
||||
)
|
||||
|
||||
expect(swapModel.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
_id: IdMap.getId("swap"),
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
additional_items: [
|
||||
{
|
||||
_id: IdMap.getId("1234-1"),
|
||||
quantity: 2,
|
||||
shipped: true,
|
||||
shipped_quantity: 2,
|
||||
},
|
||||
],
|
||||
fulfillment_status: "shipped",
|
||||
fulfillments: [
|
||||
{
|
||||
_id: IdMap.getId("f1"),
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("1234-1"),
|
||||
item_id: IdMap.getId("1234-1"),
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
data: "new",
|
||||
},
|
||||
{
|
||||
_id: IdMap.getId("f2"),
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("1234-2"),
|
||||
item_id: IdMap.getId("1234-2"),
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(fulfillmentService.createShipment).toHaveBeenCalledWith(
|
||||
{
|
||||
items: existing.additional_items,
|
||||
shipping_methods: existing.shipping_methods,
|
||||
},
|
||||
{
|
||||
_id: IdMap.getId("f1"),
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("1234-1"),
|
||||
item_id: IdMap.getId("1234-1"),
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
["1234"],
|
||||
{}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("failure", () => {})
|
||||
})
|
||||
|
||||
describe("requestReturn", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("success", () => {
|
||||
const existing = {
|
||||
return_items: [{ data: "returnline" }],
|
||||
return_shipping: { shipping: "return" },
|
||||
}
|
||||
|
||||
const returnService = {
|
||||
requestReturn: jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ return: "data" })),
|
||||
}
|
||||
const swapModel = SwapModel({ findOne: () => existing })
|
||||
const swapService = new SwapService({
|
||||
swapModel,
|
||||
returnService,
|
||||
})
|
||||
|
||||
it("calls requestReturn and updates", async () => {
|
||||
await swapService.requestReturn(testOrder, IdMap.getId("swap"))
|
||||
|
||||
expect(returnService.requestReturn).toHaveBeenCalledTimes(1)
|
||||
expect(returnService.requestReturn).toHaveBeenCalledWith(
|
||||
testOrder,
|
||||
existing.return_items,
|
||||
existing.return_shipping
|
||||
)
|
||||
|
||||
expect(swapModel.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
_id: IdMap.getId("swap"),
|
||||
},
|
||||
{ $set: { return: { return: "data" } } }
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -208,10 +208,19 @@ class CartService extends BaseService {
|
||||
}
|
||||
|
||||
const region = await this.regionService_.retrieve(region_id)
|
||||
if (region.countries.length === 1) {
|
||||
// Preselect the country if the region only has 1
|
||||
data.shipping_address = {
|
||||
country_code: region.countries[0],
|
||||
if (!data.shipping_address) {
|
||||
if (region.countries.length === 1) {
|
||||
// Preselect the country if the region only has 1
|
||||
data.shipping_address = {
|
||||
country_code: region.countries[0],
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!region.countries.includes(data.shipping_address.country_code)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Shipping country not in region"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +271,9 @@ class CartService extends BaseService {
|
||||
if (Array.isArray(item.content)) {
|
||||
item.content.forEach(c => products.push(`${c.product._id}`))
|
||||
} else {
|
||||
products.push(`${item.content.product._id}`)
|
||||
if (item.content.product) {
|
||||
products.push(`${item.content.product._id}`)
|
||||
}
|
||||
}
|
||||
|
||||
return products
|
||||
|
||||
@@ -13,7 +13,7 @@ class CustomerService extends BaseService {
|
||||
PASSWORD_RESET: "customer.password_reset",
|
||||
}
|
||||
|
||||
constructor({ customerModel, orderService, eventBusService }) {
|
||||
constructor({ customerModel, eventBusService }) {
|
||||
super()
|
||||
|
||||
/** @private @const {CustomerModel} */
|
||||
@@ -21,9 +21,6 @@ class CustomerService extends BaseService {
|
||||
|
||||
/** @private @const {EventBus} */
|
||||
this.eventBus_ = eventBusService
|
||||
|
||||
/** @private @const {EventBus} */
|
||||
this.orderService_ = orderService
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -372,15 +369,6 @@ class CustomerService extends BaseService {
|
||||
const requiredFields = ["_id", "metadata"]
|
||||
const decorated = _.pick(customer, fields.concat(requiredFields))
|
||||
|
||||
if (expandFields.includes("orders")) {
|
||||
decorated.orders = await Promise.all(
|
||||
customer.orders.map(async o => {
|
||||
const order = await this.orderService_.retrieve(o)
|
||||
return this.orderService_.decorate(order)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const final = await this.runDecorators_(decorated)
|
||||
return final
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import _ from "lodash"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
/**
|
||||
* Handles Fulfillments
|
||||
* @implements BaseService
|
||||
*/
|
||||
class FulfillmentService extends BaseService {
|
||||
constructor({
|
||||
totalsService,
|
||||
shippingProfileService,
|
||||
fulfillmentProviderService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
/** @private @const {TotalsService} */
|
||||
this.totalsService_ = totalsService
|
||||
|
||||
this.shippingProfileService_ = shippingProfileService
|
||||
|
||||
this.fulfillmentProviderService_ = fulfillmentProviderService
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the order line items, given an array of items.
|
||||
* @param {Order} order - the order to get line items from
|
||||
* @param {{ item_id: string, quantity: number }} items - the items to get
|
||||
* @param {function} transformer - a function to apply to each of the items
|
||||
* retrieved from the order, should return a line item. If the transformer
|
||||
* returns an undefined value the line item will be filtered from the
|
||||
* returned array.
|
||||
* @return {Promise<Array<LineItem>>} the line items generated by the transformer.
|
||||
*/
|
||||
async getFulfillmentItems_(order, items, transformer) {
|
||||
const toReturn = await Promise.all(
|
||||
items.map(async ({ item_id, quantity }) => {
|
||||
const item = order.items.find(i => i._id.equals(item_id))
|
||||
return transformer(item, quantity)
|
||||
})
|
||||
)
|
||||
|
||||
return toReturn.filter(i => !!i)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that a given quantity of a line item can be fulfilled. Fails if the
|
||||
* fulfillable quantity is lower than the requested fulfillment quantity.
|
||||
* Fulfillable quantity is calculated by subtracting the already fulfilled
|
||||
* quantity from the quantity that was originally purchased.
|
||||
* @param {LineItem} item - the line item to check has sufficient fulfillable
|
||||
* quantity.
|
||||
* @param {number} quantity - the quantity that is requested to be fulfilled.
|
||||
* @return {LineItem} a line item that has the requested fulfillment quantity
|
||||
* set.
|
||||
*/
|
||||
validateFulfillmentLineItem_(item, quantity) {
|
||||
if (!item) {
|
||||
// This will in most cases be called by a webhook so to ensure that
|
||||
// things go through smoothly in instances where extra items outside
|
||||
// of Medusa are added we allow unknown items
|
||||
return null
|
||||
}
|
||||
|
||||
if (quantity > item.quantity - item.fulfilled_quantity) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Cannot fulfill more items than have been purchased"
|
||||
)
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
quantity,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(order, itemsToFulfill, metadata = {}) {
|
||||
const lineItems = await this.getFulfillmentItems_(
|
||||
order,
|
||||
itemsToFulfill,
|
||||
this.validateFulfillmentLineItem_
|
||||
)
|
||||
|
||||
const { shipping_methods } = order
|
||||
|
||||
// partition order items to their dedicated shipping method
|
||||
const fulfillments = await this.partitionItems_(shipping_methods, lineItems)
|
||||
|
||||
return Promise.all(
|
||||
fulfillments.map(async method => {
|
||||
const provider = this.fulfillmentProviderService_.retrieveProvider(
|
||||
method.provider_id
|
||||
)
|
||||
|
||||
const data = await provider.createOrder(method.data, method.items, {
|
||||
...order,
|
||||
})
|
||||
|
||||
return {
|
||||
provider_id: method.provider_id,
|
||||
items: method.items,
|
||||
data,
|
||||
metadata,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async createShipment(order, fulfillment, trackingNumbers, metadata) {
|
||||
return {
|
||||
...fulfillment,
|
||||
tracking_numbers: trackingNumbers,
|
||||
shipped_at: Date.now(),
|
||||
metadata: {
|
||||
...fulfillment.metadata,
|
||||
...metadata,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FulfillmentService
|
||||
@@ -13,6 +13,8 @@ class OrderService extends BaseService {
|
||||
ITEMS_RETURNED: "order.items_returned",
|
||||
RETURN_ACTION_REQUIRED: "order.return_action_required",
|
||||
REFUND_CREATED: "order.refund_created",
|
||||
SWAP_CREATED: "order.swap_created",
|
||||
SWAP_RECEIVED: "order.swap_received",
|
||||
PLACED: "order.placed",
|
||||
UPDATED: "order.updated",
|
||||
CANCELED: "order.canceled",
|
||||
@@ -22,22 +24,29 @@ class OrderService extends BaseService {
|
||||
constructor({
|
||||
orderModel,
|
||||
counterService,
|
||||
customerService,
|
||||
paymentProviderService,
|
||||
shippingOptionService,
|
||||
shippingProfileService,
|
||||
discountService,
|
||||
fulfillmentProviderService,
|
||||
fulfillmentService,
|
||||
lineItemService,
|
||||
totalsService,
|
||||
regionService,
|
||||
returnService,
|
||||
swapService,
|
||||
documentService,
|
||||
eventBusService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
/** @private @constantant {OrderModel} */
|
||||
/** @private @constant {OrderModel} */
|
||||
this.orderModel_ = orderModel
|
||||
|
||||
/** @private @constant {CustomerService} */
|
||||
this.customerService_ = customerService
|
||||
|
||||
/** @private @constantant {PaymentProviderService} */
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
|
||||
@@ -56,6 +65,12 @@ class OrderService extends BaseService {
|
||||
/** @private @constant {RegionService} */
|
||||
this.regionService_ = regionService
|
||||
|
||||
/** @private @constant {ReturnService} */
|
||||
this.returnService_ = returnService
|
||||
|
||||
/** @private @constant {FulfillmentService} */
|
||||
this.fulfillmentService_ = fulfillmentService
|
||||
|
||||
/** @private @constant {DiscountService} */
|
||||
this.discountService_ = discountService
|
||||
|
||||
@@ -70,6 +85,9 @@ class OrderService extends BaseService {
|
||||
|
||||
/** @private @constant {ShippingOptionService} */
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
|
||||
/** @private @constant {SwapService} */
|
||||
this.swapService_ = swapService
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,33 +144,6 @@ class OrderService extends BaseService {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} selector - the query object for find
|
||||
* @return {Promise} the result of the find operation
|
||||
@@ -419,41 +410,49 @@ class OrderService extends BaseService {
|
||||
async createShipment(orderId, fulfillmentId, trackingNumbers, metadata = {}) {
|
||||
const order = await this.retrieve(orderId)
|
||||
|
||||
let shipment
|
||||
const updated = order.fulfillments.map(f => {
|
||||
if (f._id.equals(fulfillmentId)) {
|
||||
// For each item in the shipment, we set their status to shipped
|
||||
f.items.map(item => {
|
||||
const itemIdx = order.items.findIndex(el => el._id.equals(item._id))
|
||||
// Update item in order.items and in fullfillment.items to
|
||||
// ensure consistency
|
||||
if (item !== -1) {
|
||||
item.shipped_quantity = item.quantity
|
||||
order.items[itemIdx].shipped_quantity =
|
||||
(order.items[itemIdx].shipped_quantity || 0) + item.quantity
|
||||
}
|
||||
})
|
||||
shipment = {
|
||||
...f,
|
||||
tracking_numbers: trackingNumbers,
|
||||
shipped_at: Date.now(),
|
||||
metadata: {
|
||||
...f.metadata,
|
||||
...metadata,
|
||||
},
|
||||
}
|
||||
return shipment
|
||||
}
|
||||
return f
|
||||
})
|
||||
const shipment = order.fulfillments.find(f => f._id.equals(fulfillmentId))
|
||||
if (!shipment) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
"Could not find fulfillment"
|
||||
)
|
||||
}
|
||||
|
||||
const updated = await this.fulfillmentService_.createShipment(
|
||||
order,
|
||||
shipment,
|
||||
trackingNumbers,
|
||||
metadata
|
||||
)
|
||||
|
||||
let fulfillmentStatus = "shipped"
|
||||
for (const item of order.items) {
|
||||
if (item.quantity !== item.shipped_quantity) {
|
||||
fulfillmentStatus = "partially_shipped"
|
||||
break
|
||||
|
||||
const newItems = order.items.map(item => {
|
||||
const shipped = updated.items.find(fi => item._id.equals(fi._id))
|
||||
if (shipped) {
|
||||
// Find the new fulfilled total
|
||||
const shippedQuantity = (item.shipped_quantity || 0) + shipped.quantity
|
||||
|
||||
// If the ordered quantity is not the same as the fulfilled quantity
|
||||
// the order cannot be marked as fulfilled. We instead set it as
|
||||
// partially_fulfilled.
|
||||
if (item.quantity !== shippedQuantity) {
|
||||
fulfillmentStatus = "partially_shipped"
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
shipped: item.quantity === shippedQuantity,
|
||||
shipped_quantity: shippedQuantity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!item.shipped) {
|
||||
fulfillmentStatus = "partially_shipped"
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
// Add the shipment to the order
|
||||
return this.orderModel_
|
||||
@@ -462,7 +461,7 @@ class OrderService extends BaseService {
|
||||
{
|
||||
$set: {
|
||||
"fulfillments.$": updated,
|
||||
items: order.items,
|
||||
items: newItems,
|
||||
fulfillment_status: fulfillmentStatus,
|
||||
},
|
||||
}
|
||||
@@ -739,85 +738,62 @@ class OrderService extends BaseService {
|
||||
async createFulfillment(orderId, itemsToFulfill, metadata = {}) {
|
||||
const order = await this.retrieve(orderId)
|
||||
|
||||
const lineItems = await this.getFulfillmentItems_(
|
||||
const fulfillments = await this.fulfillmentService_.createFulfillment(
|
||||
order,
|
||||
itemsToFulfill,
|
||||
this.validateFulfillmentLineItem_
|
||||
metadata
|
||||
)
|
||||
|
||||
const { shipping_methods } = order
|
||||
|
||||
const updateFields = {}
|
||||
|
||||
// partition order items to their dedicated shipping method
|
||||
const fulfillments = await this.partitionItems_(shipping_methods, lineItems)
|
||||
|
||||
let successfullyFulfilled = []
|
||||
const results = await Promise.all(
|
||||
fulfillments.map(async method => {
|
||||
const provider = this.fulfillmentProviderService_.retrieveProvider(
|
||||
method.provider_id
|
||||
)
|
||||
for (const f of fulfillments) {
|
||||
successfullyFulfilled = [...successfullyFulfilled, ...f.items]
|
||||
}
|
||||
|
||||
const data = await provider
|
||||
.createOrder(method.data, method.items, { ...order })
|
||||
.then(res => {
|
||||
successfullyFulfilled = [...successfullyFulfilled, ...method.items]
|
||||
return res
|
||||
})
|
||||
|
||||
method.items = method.items.map(el => {
|
||||
return {
|
||||
...el,
|
||||
fulfilled_quantity: el.quantity,
|
||||
fulfilled: true,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
provider_id: method.provider_id,
|
||||
items: method.items,
|
||||
data,
|
||||
metadata,
|
||||
}
|
||||
})
|
||||
)
|
||||
const updateFields = {}
|
||||
|
||||
// Reflect the fulfillments in the items
|
||||
updateFields.items = order.items.map(i => {
|
||||
const ful = successfullyFulfilled.find(f => i._id.equals(f._id))
|
||||
if (ful) {
|
||||
// Find the new fulfilled total
|
||||
const fulfilledQuantity = i.fulfilled_quantity + ful.quantity
|
||||
|
||||
// If the ordered quantity is not the same as the fulfilled quantity
|
||||
// the order cannot be marked as fulfilled. We instead set it as
|
||||
// partially_fulfilled.
|
||||
if (i.quantity !== fulfilledQuantity) {
|
||||
updateFields.fulfillment_status = "partially_fulfilled"
|
||||
}
|
||||
|
||||
// Update the items
|
||||
return {
|
||||
...i,
|
||||
fulfilled: i.quantity === ful.quantity,
|
||||
fulfilled_quantity: i.fulfilled_quantity + ful.quantity,
|
||||
fulfilled: i.quantity === fulfilledQuantity,
|
||||
fulfilled_quantity: fulfilledQuantity,
|
||||
}
|
||||
}
|
||||
|
||||
if (!i.fulfilled) {
|
||||
updateFields.fulfillment_status = "partially_fulfilled"
|
||||
}
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
updateFields.fulfillment_status = "fulfilled"
|
||||
|
||||
for (const el of updateFields.items) {
|
||||
if (el.quantity !== el.fulfilled_quantity) {
|
||||
updateFields.fulfillment_status = "partially_fulfilled"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return this.orderModel_
|
||||
.updateOne(
|
||||
{
|
||||
_id: orderId,
|
||||
},
|
||||
{
|
||||
$addToSet: { fulfillments: { $each: results } },
|
||||
$addToSet: { fulfillments: { $each: fulfillments } },
|
||||
$set: updateFields,
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
for (const fulfillment of results) {
|
||||
for (const fulfillment of fulfillments) {
|
||||
this.eventBus_.emit(OrderService.Events.FULFILLMENT_CREATED, {
|
||||
order_id: orderId,
|
||||
fulfillment,
|
||||
@@ -883,32 +859,6 @@ class OrderService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that an order has the statuses necessary to complete a return.
|
||||
* fulfillment_status cannot be not_fulfilled or returned.
|
||||
* payment_status must be captured.
|
||||
* @param {Order} order - the order to check statuses on
|
||||
* @throws when statuses are not sufficient for returns.
|
||||
*/
|
||||
validateReturnStatuses_(order) {
|
||||
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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates documents.
|
||||
* @param {Array<Document>} docs - documents to generate
|
||||
@@ -941,73 +891,13 @@ class OrderService extends BaseService {
|
||||
async requestReturn(orderId, items, shippingMethod, refundAmount) {
|
||||
const order = await this.retrieve(orderId)
|
||||
|
||||
// Throws if the order doesn't have the necessary status for return
|
||||
this.validateReturnStatuses_(order)
|
||||
|
||||
let toRefund = refundAmount
|
||||
if (typeof refundAmount !== "undefined") {
|
||||
const total = await this.totalsService_.getTotal(order)
|
||||
const refunded = await this.totalsService_.getRefundedTotal(order)
|
||||
const refundable = total - refunded
|
||||
if (refundAmount > refundable) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Cannot refund more than the original payment"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
toRefund = await this.totalsService_.getRefundTotal(order, returnLines)
|
||||
}
|
||||
|
||||
const returnLines = await this.getFulfillmentItems_(
|
||||
const returnRequest = await this.returnService_.requestReturn(
|
||||
order,
|
||||
items,
|
||||
this.validateReturnLineItem_
|
||||
shippingMethod,
|
||||
refundAmount
|
||||
)
|
||||
|
||||
let fulfillmentData = {}
|
||||
let shipping_method = {}
|
||||
if (typeof shippingMethod !== "undefined") {
|
||||
shipping_method = await this.shippingOptionService_.retrieve(
|
||||
shippingMethod.id
|
||||
)
|
||||
const provider = await this.fulfillmentProviderService_.retrieveProvider(
|
||||
shipping_method.provider_id
|
||||
)
|
||||
fulfillmentData = await provider.createReturn(
|
||||
shipping_method.data,
|
||||
returnLines,
|
||||
order
|
||||
)
|
||||
|
||||
if (typeof shippingMethod.price !== "undefined") {
|
||||
shipping_method.price = shippingMethod.price
|
||||
} else {
|
||||
shipping_method.price = await this.shippingOptionService_.getPrice(
|
||||
shipping_method,
|
||||
{
|
||||
...order,
|
||||
items: returnLines,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
toRefund = Math.max(0, toRefund - shipping_method.price)
|
||||
}
|
||||
|
||||
const newReturn = {
|
||||
shipping_method,
|
||||
refund_amount: toRefund,
|
||||
items: returnLines.map(i => ({
|
||||
item_id: i._id,
|
||||
content: i.content,
|
||||
quantity: i.quantity,
|
||||
is_requested: true,
|
||||
metadata: i.metadata,
|
||||
})),
|
||||
shipping_data: fulfillmentData,
|
||||
}
|
||||
|
||||
return this.orderModel_
|
||||
.updateOne(
|
||||
{
|
||||
@@ -1015,14 +905,14 @@ class OrderService extends BaseService {
|
||||
},
|
||||
{
|
||||
$push: {
|
||||
returns: newReturn,
|
||||
returns: returnRequest,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
this.eventBus_.emit(OrderService.Events.RETURN_REQUESTED, {
|
||||
order: result,
|
||||
return: newReturn,
|
||||
return: returnRequest,
|
||||
})
|
||||
return result
|
||||
})
|
||||
@@ -1040,7 +930,13 @@ class OrderService extends BaseService {
|
||||
* @param {string[]} lineItems - the line items to return
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async return(orderId, returnId, items, refundAmount, allowMismatch = false) {
|
||||
async receiveReturn(
|
||||
orderId,
|
||||
returnId,
|
||||
items,
|
||||
refundAmount,
|
||||
allowMismatch = false
|
||||
) {
|
||||
const order = await this.retrieve(orderId)
|
||||
const returnRequest = order.returns.find(r => r._id.equals(returnId))
|
||||
if (!returnRequest) {
|
||||
@@ -1050,99 +946,24 @@ class OrderService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
if (returnRequest.status === "received") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Return with id ${returnId} has already been received`
|
||||
)
|
||||
}
|
||||
|
||||
const returnLines = await this.getFulfillmentItems_(
|
||||
const updatedReturn = await this.returnService_.receiveReturn(
|
||||
order,
|
||||
returnRequest,
|
||||
items,
|
||||
this.validateReturnLineItem_
|
||||
refundAmount,
|
||||
allowMismatch
|
||||
)
|
||||
|
||||
const newLines = returnLines.map(l => {
|
||||
const existing = returnRequest.items.find(i => l._id.equals(i.item_id))
|
||||
if (existing) {
|
||||
return {
|
||||
...existing,
|
||||
quantity: l.quantity,
|
||||
requested_quantity: existing.quantity,
|
||||
is_requested: l.quantity === existing.quantity,
|
||||
is_registered: true,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
item_id: l._id,
|
||||
content: l.content,
|
||||
quantity: l.quantity,
|
||||
is_requested: false,
|
||||
is_registered: true,
|
||||
metadata: l.metadata,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const isMatching = newLines.every(l => l.is_requested)
|
||||
if (!isMatching && !allowMismatch) {
|
||||
// Should update status
|
||||
const newReturns = order.returns.map(r => {
|
||||
if (r._id.equals(returnId)) {
|
||||
return {
|
||||
...r,
|
||||
status: "requires_action",
|
||||
items: newLines,
|
||||
}
|
||||
} else {
|
||||
return r
|
||||
}
|
||||
})
|
||||
if (updatedReturn.status === "requires_action") {
|
||||
return this.orderModel_
|
||||
.updateOne(
|
||||
{
|
||||
_id: orderId,
|
||||
"returns._id": updatedReturn._id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
returns: newReturns,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
this.eventBus_.emit(OrderService.Events.RETURN_ACTION_REQUIRED, {
|
||||
order: result,
|
||||
return: result.returns.find(r => r._id.equals(returnId)),
|
||||
})
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
const toRefund = refundAmount || returnRequest.refund_amount
|
||||
const total = await this.totalsService_.getTotal(order)
|
||||
const refunded = await this.totalsService_.getRefundedTotal(order)
|
||||
|
||||
if (toRefund > total - refunded) {
|
||||
const newReturns = order.returns.map(r => {
|
||||
if (r._id.equals(returnId)) {
|
||||
return {
|
||||
...r,
|
||||
status: "requires_action",
|
||||
items: newLines,
|
||||
}
|
||||
} else {
|
||||
return r
|
||||
}
|
||||
})
|
||||
return this.orderModel_
|
||||
.updateOne(
|
||||
{
|
||||
_id: orderId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
returns: newReturns,
|
||||
"returns.$": updatedReturn,
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -1157,7 +978,7 @@ class OrderService extends BaseService {
|
||||
|
||||
let isFullReturn = true
|
||||
const newItems = order.items.map(i => {
|
||||
const isReturn = returnLines.find(r => r._id.equals(i._id))
|
||||
const isReturn = updatedReturn.items.find(r => i._id.equals(r.item_id))
|
||||
if (isReturn) {
|
||||
const returnedQuantity = i.returned_quantity + isReturn.quantity
|
||||
let returned = i.quantity === returnedQuantity
|
||||
@@ -1177,18 +998,9 @@ class OrderService extends BaseService {
|
||||
}
|
||||
})
|
||||
|
||||
const newReturns = order.returns.map(r => {
|
||||
if (r._id.equals(returnId)) {
|
||||
return {
|
||||
...r,
|
||||
status: "received",
|
||||
items: newLines,
|
||||
refund_amount: toRefund,
|
||||
}
|
||||
} else {
|
||||
return r
|
||||
}
|
||||
})
|
||||
const newReturns = order.returns.map(r =>
|
||||
r._id.equals(returnId) ? updatedReturn : r
|
||||
)
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
@@ -1198,15 +1010,15 @@ class OrderService extends BaseService {
|
||||
},
|
||||
}
|
||||
|
||||
if (toRefund > 0) {
|
||||
if (updatedReturn.refund_amount > 0) {
|
||||
const { provider_id, data } = order.payment_method
|
||||
const paymentProvider = this.paymentProviderService_.retrieveProvider(
|
||||
provider_id
|
||||
)
|
||||
await paymentProvider.refundPayment(data, toRefund)
|
||||
await paymentProvider.refundPayment(data, updatedReturn.refund_amount)
|
||||
update.$push = {
|
||||
refunds: {
|
||||
amount: toRefund,
|
||||
amount: updatedReturn.refund_amount,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1214,7 +1026,7 @@ class OrderService extends BaseService {
|
||||
return this.orderModel_.updateOne({ _id: orderId }, update).then(result => {
|
||||
this.eventBus_.emit(OrderService.Events.ITEMS_RETURNED, {
|
||||
order: result,
|
||||
return: result.returns.find(r => r._id.equals(returnId)),
|
||||
return: updatedReturn,
|
||||
})
|
||||
return result
|
||||
})
|
||||
@@ -1375,6 +1187,22 @@ class OrderService extends BaseService {
|
||||
|
||||
o.created = order._id.getTimestamp()
|
||||
|
||||
if (expandFields.includes("swaps")) {
|
||||
if (order.swaps) {
|
||||
o.swaps = await Promise.all(
|
||||
order.swaps.map(sId => {
|
||||
return this.swapService_.retrieve(sId)
|
||||
})
|
||||
)
|
||||
} else {
|
||||
o.swaps = []
|
||||
}
|
||||
}
|
||||
|
||||
if (expandFields.includes("customer")) {
|
||||
o.customer = await this.customerService_.retrieve(order.customer_id)
|
||||
}
|
||||
|
||||
if (expandFields.includes("region")) {
|
||||
o.region = await this.regionService_.retrieve(order.region_id)
|
||||
}
|
||||
@@ -1422,6 +1250,87 @@ class OrderService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a swap to the order. The swap must belong to the order for it to
|
||||
* be registered.
|
||||
* @param {string} id - the id of the order to register the swap to.
|
||||
* @param {string} swapId - the id of the swap to add to the order.
|
||||
* @returns {Promise<Order>} the resulting order
|
||||
*/
|
||||
async registerSwapCreated(id, swapId) {
|
||||
const order = await this.retrieve(id)
|
||||
const swap = await this.swapService_.retrieve(swapId)
|
||||
|
||||
if (!order._id.equals(swap.order_id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Swap must belong to the given order"
|
||||
)
|
||||
}
|
||||
|
||||
return this.orderModel_
|
||||
.updateOne({ _id: order._id }, { $addToSet: { swaps: swapId } })
|
||||
.then(result => {
|
||||
this.eventBus_.emit(OrderService.Events.SWAP_CREATED, {
|
||||
order: result,
|
||||
swap_id: swapId,
|
||||
})
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the swap return items as received so that they cannot be used
|
||||
* as a part of other swaps/returns.
|
||||
* @param {string} id - the id of the order with the swap.
|
||||
* @param {string} swapId - the id of the swap that has been received.
|
||||
* @returns {Promise<Order>} the resulting order
|
||||
*/
|
||||
async registerSwapReceived(id, swapId) {
|
||||
const order = await this.retrieve(id)
|
||||
const swap = await this.swapService_.retrieve(swapId)
|
||||
|
||||
if (!order._id.equals(swap.order_id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Swap must belong to the given order"
|
||||
)
|
||||
}
|
||||
|
||||
if (swap.return.status !== "received") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Swap is not received"
|
||||
)
|
||||
}
|
||||
|
||||
const newItems = order.items.map(i => {
|
||||
const isReturn = swap.return_items.find(ri => i._id.equals(ri.item_id))
|
||||
|
||||
if (isReturn) {
|
||||
const returnedQuantity = i.returned_quantity + isReturn.quantity
|
||||
const returned = returnedQuantity === i.quantity
|
||||
return {
|
||||
...i,
|
||||
returned_quantity: returnedQuantity,
|
||||
returned,
|
||||
}
|
||||
}
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
return this.orderModel_
|
||||
.updateOne({ _id: order._id }, { $set: { items: newItems } })
|
||||
.then(result => {
|
||||
this.eventBus_.emit(OrderService.Events.SWAP_RECEIVED, {
|
||||
order: result,
|
||||
swap_id: swapId,
|
||||
})
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated method to delete metadata for an order.
|
||||
* @param {string} orderId - the order to delete metadata from.
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
import _ from "lodash"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
/**
|
||||
* Handles Returns
|
||||
* @implements BaseService
|
||||
*/
|
||||
class ReturnService extends BaseService {
|
||||
constructor({
|
||||
totalsService,
|
||||
shippingOptionService,
|
||||
fulfillmentProviderService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
/** @private @const {TotalsService} */
|
||||
this.totalsService_ = totalsService
|
||||
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
|
||||
this.fulfillmentProviderService_ = fulfillmentProviderService
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the order line items, given an array of items.
|
||||
* @param {Order} order - the order to get line items from
|
||||
* @param {{ item_id: string, quantity: number }} items - the items to get
|
||||
* @param {function} transformer - a function to apply to each of the items
|
||||
* retrieved from the order, should return a line item. If the transformer
|
||||
* returns an undefined value the line item will be filtered from the
|
||||
* returned array.
|
||||
* @return {Promise<Array<LineItem>>} the line items generated by the transformer.
|
||||
*/
|
||||
async getFulfillmentItems_(order, items, transformer) {
|
||||
const toReturn = await Promise.all(
|
||||
items.map(async ({ item_id, quantity }) => {
|
||||
const item = order.items.find(i => i._id.equals(item_id))
|
||||
return transformer(item, quantity)
|
||||
})
|
||||
)
|
||||
|
||||
return toReturn.filter(i => !!i)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that an order has the statuses necessary to complete a return.
|
||||
* fulfillment_status cannot be not_fulfilled or returned.
|
||||
* payment_status must be captured.
|
||||
* @param {Order} order - the order to check statuses on
|
||||
* @throws when statuses are not sufficient for returns.
|
||||
*/
|
||||
validateReturnStatuses_(order) {
|
||||
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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that a given quantity of a line item can be returned. Fails if the
|
||||
* item is undefined or if the returnable quantity of the item is lower, than
|
||||
* the quantity that is requested to be returned.
|
||||
* @param {LineItem?} item - the line item to check has sufficient returnable
|
||||
* quantity.
|
||||
* @param {number} quantity - the quantity that is requested to be returned.
|
||||
* @return {LineItem} a line item where the quantity is set to the requested
|
||||
* return quantity.
|
||||
*/
|
||||
validateReturnLineItem_(item, quantity) {
|
||||
if (!item) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Return contains invalid line item"
|
||||
)
|
||||
}
|
||||
|
||||
const returnable = item.quantity - item.returned_quantity
|
||||
if (quantity > returnable) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Cannot return more items than have been purchased"
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
quantity,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a return request for an order, with given items, and a shipping
|
||||
* method. If no refundAmount is provided the refund amount is calculated from
|
||||
* the return lines and the shipping cost.
|
||||
* @param {String} orderId - the id of the order to create a return for.
|
||||
* @param {Array<{item_id: String, quantity: Int}>} items - the line items to
|
||||
* return
|
||||
* @param {ShippingMethod?} shippingMethod - the shipping method used for the
|
||||
* return
|
||||
* @param {Number?} refundAmount - the amount to refund when the return is
|
||||
* received.
|
||||
* @returns {Promise<Order>} the resulting order.
|
||||
*/
|
||||
async requestReturn(order, items, shippingMethod, refundAmount) {
|
||||
// Throws if the order doesn't have the necessary status for return
|
||||
this.validateReturnStatuses_(order)
|
||||
|
||||
const returnLines = await this.getFulfillmentItems_(
|
||||
order,
|
||||
items,
|
||||
this.validateReturnLineItem_
|
||||
)
|
||||
|
||||
let toRefund = refundAmount
|
||||
if (typeof refundAmount !== "undefined") {
|
||||
const total = await this.totalsService_.getTotal(order)
|
||||
const refunded = await this.totalsService_.getRefundedTotal(order)
|
||||
const refundable = total - refunded
|
||||
if (refundAmount > refundable) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Cannot refund more than the original payment"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
toRefund = await this.totalsService_.getRefundTotal(order, returnLines)
|
||||
}
|
||||
|
||||
let fulfillmentData = {}
|
||||
let shipping_method = {}
|
||||
if (typeof shippingMethod !== "undefined") {
|
||||
shipping_method = await this.shippingOptionService_.retrieve(
|
||||
shippingMethod.id
|
||||
)
|
||||
const provider = await this.fulfillmentProviderService_.retrieveProvider(
|
||||
shipping_method.provider_id
|
||||
)
|
||||
fulfillmentData = await provider.createReturn(
|
||||
shipping_method.data,
|
||||
returnLines,
|
||||
order
|
||||
)
|
||||
|
||||
if (typeof shippingMethod.price !== "undefined") {
|
||||
shipping_method.price = shippingMethod.price
|
||||
} else {
|
||||
shipping_method.price = await this.shippingOptionService_.getPrice(
|
||||
shipping_method,
|
||||
{
|
||||
...order,
|
||||
items: returnLines,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
toRefund = Math.max(
|
||||
0,
|
||||
toRefund - shipping_method.price * (1 + order.tax_rate)
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
shipping_method,
|
||||
refund_amount: toRefund,
|
||||
items: returnLines.map(i => ({
|
||||
item_id: i._id,
|
||||
content: i.content,
|
||||
quantity: i.quantity,
|
||||
is_requested: true,
|
||||
metadata: i.metadata,
|
||||
})),
|
||||
shipping_data: fulfillmentData,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a previously requested return as received. This will create a
|
||||
* refund to the customer. If the returned items don't match the requested
|
||||
* items the return status will be updated to requires_action. This behaviour
|
||||
* is useful in sitautions where a custom refund amount is requested, but the
|
||||
* retuned items are not matching the requested items. Setting the
|
||||
* allowMismatch argument to true, will process the return, ignoring any
|
||||
* mismatches.
|
||||
* @param {string} orderId - the order to return.
|
||||
* @param {string[]} lineItems - the line items to return
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async receiveReturn(
|
||||
order,
|
||||
returnRequest,
|
||||
items,
|
||||
refundAmount,
|
||||
allowMismatch = false
|
||||
) {
|
||||
if (returnRequest.status === "received") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Return with id ${returnId} has already been received`
|
||||
)
|
||||
}
|
||||
|
||||
const returnLines = await this.getFulfillmentItems_(
|
||||
order,
|
||||
items,
|
||||
this.validateReturnLineItem_
|
||||
)
|
||||
|
||||
const newLines = returnLines.map(l => {
|
||||
const existing = returnRequest.items.find(i => l._id.equals(i.item_id))
|
||||
if (existing) {
|
||||
return {
|
||||
...existing,
|
||||
quantity: l.quantity,
|
||||
requested_quantity: existing.quantity,
|
||||
is_requested: l.quantity === existing.quantity,
|
||||
is_registered: true,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
item_id: l._id,
|
||||
content: l.content,
|
||||
quantity: l.quantity,
|
||||
is_requested: false,
|
||||
is_registered: true,
|
||||
metadata: l.metadata,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const isMatching = newLines.every(l => l.is_requested)
|
||||
if (!isMatching && !allowMismatch) {
|
||||
// Should update status
|
||||
return {
|
||||
...returnRequest,
|
||||
status: "requires_action",
|
||||
items: newLines,
|
||||
}
|
||||
}
|
||||
|
||||
const toRefund = refundAmount || returnRequest.refund_amount
|
||||
const total = await this.totalsService_.getTotal(order)
|
||||
const refunded = await this.totalsService_.getRefundedTotal(order)
|
||||
|
||||
if (toRefund > total - refunded) {
|
||||
return {
|
||||
...returnRequest,
|
||||
status: "requires_action",
|
||||
items: newLines,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...returnRequest,
|
||||
status: "received",
|
||||
items: newLines,
|
||||
refund_amount: toRefund,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ReturnService
|
||||
@@ -394,8 +394,11 @@ class ShippingProfileService extends BaseService {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (!acc.includes(next.content.product._id)) {
|
||||
acc.push(next.content.product._id)
|
||||
// We may have line items that are not associated with a product
|
||||
if (next.content.product) {
|
||||
if (!acc.includes(next.content.product._id)) {
|
||||
acc.push(next.content.product._id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,673 @@
|
||||
import _ from "lodash"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
|
||||
/**
|
||||
* Handles swaps
|
||||
* @implements BaseService
|
||||
*/
|
||||
class SwapService extends BaseService {
|
||||
static Events = {
|
||||
SHIPMENT_CREATED: "swap.shipment_created",
|
||||
PAYMENT_COMPLETED: "swap.payment_completed",
|
||||
PAYMENT_CAPTURED: "swap.payment_captured",
|
||||
PAYMENT_CAPTURE_FAILED: "swap.payment_capture_failed",
|
||||
}
|
||||
|
||||
constructor({
|
||||
swapModel,
|
||||
eventBusService,
|
||||
cartService,
|
||||
totalsService,
|
||||
returnService,
|
||||
lineItemService,
|
||||
paymentProviderService,
|
||||
shippingOptionService,
|
||||
fulfillmentService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
/** @private @const {SwapModel} */
|
||||
this.swapModel_ = swapModel
|
||||
|
||||
/** @private @const {TotalsService} */
|
||||
this.totalsService_ = totalsService
|
||||
|
||||
/** @private @const {LineItemService} */
|
||||
this.lineItemService_ = lineItemService
|
||||
|
||||
/** @private @const {ReturnService} */
|
||||
this.returnService_ = returnService
|
||||
|
||||
/** @private @const {PaymentProviderService} */
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
|
||||
/** @private @const {CartService} */
|
||||
this.cartService_ = cartService
|
||||
|
||||
/** @private @const {FulfillmentService} */
|
||||
this.fulfillmentService_ = fulfillmentService
|
||||
|
||||
/** @private @const {ShippingOptionService} */
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
|
||||
/** @private @const {EventBusService} */
|
||||
this.eventBus_ = eventBusService
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to validate user ids. Throws an error if the cast fails
|
||||
* @param {string} rawId - the raw user id to validate.
|
||||
* @return {string} the validated id
|
||||
*/
|
||||
validateId_(rawId) {
|
||||
const schema = Validator.objectId()
|
||||
const { value, error } = schema.validate(rawId.toString())
|
||||
if (error) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"The swapId could not be casted to an ObjectId"
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a swap with the given id.
|
||||
* @param {string} id - the id of the swap to retrieve
|
||||
* @return {Promise<Swap>} the swap
|
||||
*/
|
||||
async retrieve(id) {
|
||||
const validatedId = this.validateId_(id)
|
||||
const swap = await this.swapModel_.findOne({ _id: validatedId })
|
||||
|
||||
if (!swap) {
|
||||
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Swap was not found")
|
||||
}
|
||||
|
||||
return swap
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a swap based on its associated cart id
|
||||
* @param {string} cartId - the cart id that the swap's cart has
|
||||
* @return {Promise<Swap>} the swap
|
||||
*/
|
||||
async retrieveByCartId(cartId) {
|
||||
const swap = await this.swapModel_.findOne({ cart_id: cartId })
|
||||
|
||||
if (!swap) {
|
||||
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Swap was not found")
|
||||
}
|
||||
|
||||
return swap
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef OrderLike
|
||||
* @property {Array<LineItem>} items - the items on the order
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef ReturnItem
|
||||
* @property {string} item_id - the id of the item in the order to return from.
|
||||
* @property {number} quantity - the amount of the item to return.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Goes through a list of return items to ensure that they exist on the
|
||||
* original order. If the item exists it is verified that the quantity to
|
||||
* return is not higher than the original quantity ordered.
|
||||
* @param {OrderLike} order - the order to return from
|
||||
* @param {Array<ReturnItem>} returnItems - the items to return
|
||||
* @return {Array<ReturnItems>} the validated returnItems
|
||||
*/
|
||||
validateReturnItems_(order, returnItems) {
|
||||
return returnItems.map(({ item_id, quantity }) => {
|
||||
const item = order.items.find(i => i._id.equals(item_id))
|
||||
|
||||
// The item must exist in the order
|
||||
if (!item) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Item does not exist on order"
|
||||
)
|
||||
}
|
||||
|
||||
// Item's cannot be returned multiple times
|
||||
if (item.quantity < item.returned_quantity + quantity) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Cannot return more items than have been ordered"
|
||||
)
|
||||
}
|
||||
|
||||
return { item_id, quantity }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef PreliminaryLineItem
|
||||
* @property {string} variant_id - the id of the variant to create an item from
|
||||
* @property {number} quantity - the amount of the variant to add to the line item
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a swap from an order, with given return items, additional items
|
||||
* and an optional return shipping method.
|
||||
* @param {Order} order - the order to base the swap off.
|
||||
* @param {Array<ReturnItem>} returnItems - the items to return in the swap.
|
||||
* @param {Array<PreliminaryLineItem>} additionalItems - the items to send to
|
||||
* the customer.
|
||||
* @param {ReturnShipping?} returnShipping - an optional shipping method for
|
||||
* returning the returnItems.
|
||||
* @returns {Promise<Swap>} the newly created swap.
|
||||
*/
|
||||
async create(order, returnItems, additionalItems, returnShipping) {
|
||||
if (
|
||||
order.fulfillment_status === "not_fulfilled" ||
|
||||
order.payment_status !== "captured"
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Order cannot be swapped"
|
||||
)
|
||||
}
|
||||
|
||||
const newItems = await Promise.all(
|
||||
additionalItems.map(({ variant_id, quantity }) => {
|
||||
return this.lineItemService_.generate(
|
||||
variant_id,
|
||||
order.region_id,
|
||||
quantity
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const validatedReturnItems = this.validateReturnItems_(order, returnItems)
|
||||
|
||||
return this.swapModel_.create({
|
||||
order_id: order._id,
|
||||
region_id: order.region_id,
|
||||
currency_code: order.currency_code,
|
||||
return_items: validatedReturnItems,
|
||||
return_shipping: returnShipping,
|
||||
additional_items: newItems,
|
||||
})
|
||||
}
|
||||
|
||||
async capturePayment(swapId) {
|
||||
const swap = await this.retrieve(swapId)
|
||||
|
||||
if (swap.payment_status !== "awaiting") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Payment already captured"
|
||||
)
|
||||
}
|
||||
|
||||
const updateFields = { payment_status: "captured" }
|
||||
|
||||
const { provider_id, data } = swap.payment_method
|
||||
const paymentProvider = await this.paymentProviderService_.retrieveProvider(
|
||||
provider_id
|
||||
)
|
||||
|
||||
try {
|
||||
await paymentProvider.capturePayment(data)
|
||||
} catch (error) {
|
||||
return this.swapModel_
|
||||
.updateOne(
|
||||
{
|
||||
_id: swapId,
|
||||
},
|
||||
{
|
||||
$set: { payment_status: "requires_action" },
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
this.eventBus_.emit(SwapService.Events.PAYMENT_CAPTURE_FAILED, result)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
return this.swapModel_
|
||||
.updateOne(
|
||||
{
|
||||
_id: swapId,
|
||||
},
|
||||
{
|
||||
$set: updateFields,
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
this.eventBus_.emit(SwapService.Events.PAYMENT_CAPTURED, result)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cart from the given swap and order. The cart can be used to pay
|
||||
* for differences associated with the swap. The swap represented by the
|
||||
* swapId must belong to the order. Fails if there is already a cart on the
|
||||
* swap.
|
||||
* @param {Order} order - the order to create the cart from
|
||||
* @param {string} swapId - the id of the swap to create the cart from
|
||||
* @returns {Promise<Swap>} the swap with its cart_id prop set to the id of
|
||||
* the new cart.
|
||||
*/
|
||||
async createCart(order, swapId) {
|
||||
const swap = await this.retrieve(swapId)
|
||||
|
||||
if (!order._id.equals(swap.order_id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"The swap does not belong to the order"
|
||||
)
|
||||
}
|
||||
|
||||
if (swap.cart_id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"A cart has already been created for the swap"
|
||||
)
|
||||
}
|
||||
|
||||
// Add return lines to the cart to ensure that the total calculation is
|
||||
// correct.
|
||||
const returnLines = swap.return_items.map(r => {
|
||||
const lineItem = order.items.find(i => i._id.equals(r.item_id))
|
||||
|
||||
return {
|
||||
...lineItem,
|
||||
content: {
|
||||
...lineItem.content,
|
||||
unit_price: -1 * lineItem.content.unit_price,
|
||||
},
|
||||
quantity: r.quantity,
|
||||
metadata: {
|
||||
...lineItem.metadata,
|
||||
is_return_line: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// If the swap has a return shipping method the price has to be added to the
|
||||
// cart.
|
||||
if (swap.return_shipping) {
|
||||
returnLines.push({
|
||||
title: "Return shipping",
|
||||
quantity: 1,
|
||||
has_shipping: true,
|
||||
content: {
|
||||
unit_price: swap.return_shipping.price,
|
||||
quantity: 1,
|
||||
},
|
||||
metadata: {
|
||||
is_return_line: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const cart = await this.cartService_.create({
|
||||
email: order.email,
|
||||
billing_address: order.billing_address,
|
||||
shipping_address: order.shipping_address,
|
||||
items: [...returnLines, ...swap.additional_items],
|
||||
region_id: order.region_id,
|
||||
customer_id: order.customer_id,
|
||||
is_swap: true,
|
||||
metadata: {
|
||||
swap_id: swap._id,
|
||||
parent_order_id: order._id,
|
||||
},
|
||||
})
|
||||
|
||||
return this.swapModel_.updateOne(
|
||||
{ _id: swapId },
|
||||
{ $set: { cart_id: cart._id } }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
async registerCartCompletion(swapId, cartId) {
|
||||
const swap = await this.retrieve(swapId)
|
||||
const cart = await this.cartService_.retrieve(cartId)
|
||||
|
||||
// If we already registered the cart completion we just return
|
||||
if (swap.is_paid) {
|
||||
return swap
|
||||
}
|
||||
|
||||
if (!cart._id.equals(swap.cart_id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Cart does not belong to swap"
|
||||
)
|
||||
}
|
||||
|
||||
let paymentSession = {}
|
||||
let paymentData = {}
|
||||
const { payment_method, payment_sessions } = cart
|
||||
|
||||
if (!payment_method) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Cart does not contain a payment method"
|
||||
)
|
||||
}
|
||||
|
||||
if (!payment_sessions || !payment_sessions.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"cart must have payment sessions"
|
||||
)
|
||||
}
|
||||
|
||||
paymentSession = payment_sessions.find(
|
||||
ps => ps.provider_id === payment_method.provider_id
|
||||
)
|
||||
|
||||
// Throw if payment method does not exist
|
||||
if (!paymentSession) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Cart does not have an authorized payment session"
|
||||
)
|
||||
}
|
||||
|
||||
const paymentProvider = this.paymentProviderService_.retrieveProvider(
|
||||
paymentSession.provider_id
|
||||
)
|
||||
const paymentStatus = await paymentProvider.getStatus(paymentSession.data)
|
||||
|
||||
// If payment status is not authorized, we throw
|
||||
if (paymentStatus !== "authorized" && paymentStatus !== "succeeded") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Payment method is not authorized"
|
||||
)
|
||||
}
|
||||
|
||||
paymentData = await paymentProvider.retrievePayment(paymentSession.data)
|
||||
|
||||
let payment = {}
|
||||
if (paymentSession.provider_id) {
|
||||
payment = {
|
||||
provider_id: paymentSession.provider_id,
|
||||
data: paymentData,
|
||||
}
|
||||
}
|
||||
|
||||
const total = await this.totalsService_.getTotal(cart)
|
||||
|
||||
return this.swapModel_
|
||||
.updateOne(
|
||||
{ _id: swap._id },
|
||||
{
|
||||
shipping_address: cart.shipping_address,
|
||||
shipping_methods: cart.shipping_methods,
|
||||
is_paid: true,
|
||||
amount_paid: total,
|
||||
payment_method: payment,
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
this.eventBus_.emit(SwapService.Events.PAYMENT_COMPLETED, {
|
||||
swap: result,
|
||||
})
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a return based off an order and a swap.
|
||||
* @param {Order} order - the order to create the return from.
|
||||
* @param {string} swapId - the id to create the return from
|
||||
* @returns {Promise<Swap>} the swap
|
||||
*/
|
||||
async requestReturn(order, swapId) {
|
||||
const swap = await this.retrieve(swapId)
|
||||
|
||||
const newReturn = await this.returnService_.requestReturn(
|
||||
order,
|
||||
swap.return_items,
|
||||
swap.return_shipping
|
||||
)
|
||||
|
||||
return this.swapModel_.updateOne(
|
||||
{ _id: swapId },
|
||||
{ $set: { return: newReturn } }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the return associated with a swap as received. If the return
|
||||
* is received with mismatching return items the swap's status will be updated
|
||||
* to requires_action.
|
||||
* @param {Order} order - the order to receive the return based off
|
||||
* @param {string} swapId - the id of the swap to receive.
|
||||
* @param {Array<ReturnItem>} - the items that have been returned
|
||||
* @returns {Promise<Swap>} the resulting swap, with an updated return and
|
||||
* status.
|
||||
*/
|
||||
async receiveReturn(order, swapId, returnItems) {
|
||||
const swap = await this.retrieve(swapId)
|
||||
|
||||
const returnRequest = swap.return
|
||||
if (!returnRequest || !returnRequest._id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Swap has no return request"
|
||||
)
|
||||
}
|
||||
|
||||
if (!order._id.equals(swap.order_id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"The swap does not belong to the order"
|
||||
)
|
||||
}
|
||||
|
||||
const updatedReturn = await this.returnService_.receiveReturn(
|
||||
order,
|
||||
returnRequest,
|
||||
returnItems,
|
||||
returnRequest.refund_amount,
|
||||
false
|
||||
)
|
||||
|
||||
let status = "received"
|
||||
if (updatedReturn.status === "requires_action") {
|
||||
status = "requires_action"
|
||||
}
|
||||
|
||||
return this.swapModel_.updateOne(
|
||||
{
|
||||
_id: swapId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
status,
|
||||
return: updatedReturn,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fulfills the addtional items associated with the swap. Will call the
|
||||
* fulfillment providers associated with the shipping methods.
|
||||
* @param {string} swapId - the id of the swap to fulfill,
|
||||
* @param {object} metadata - optional metadata to attach to the fulfillment.
|
||||
* @returns {Promise<Swap>} the updated swap with new status and fulfillments.
|
||||
*/
|
||||
async createFulfillment(order, swapId, metadata = {}) {
|
||||
const swap = await this.retrieve(swapId)
|
||||
|
||||
const fulfillments = await this.fulfillmentService_.createFulfillment(
|
||||
{
|
||||
...swap,
|
||||
currency_code: order.currency_code,
|
||||
tax_rate: order.tax_rate,
|
||||
region_id: order.region_id,
|
||||
display_id: order.display_id,
|
||||
billing_address: order.billing_address,
|
||||
items: swap.additional_items,
|
||||
shipping_methods: swap.shipping_methods,
|
||||
is_swap: true,
|
||||
},
|
||||
swap.additional_items.map(i => ({
|
||||
item_id: i._id,
|
||||
quantity: i.quantity,
|
||||
})),
|
||||
metadata
|
||||
)
|
||||
|
||||
return this.swapModel_.updateOne(
|
||||
{
|
||||
_id: swapId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
fulfillment_status: "fulfilled",
|
||||
fulfillments,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a fulfillment as shipped and attaches tracking numbers.
|
||||
* @param {string} swapId - the id of the swap that has been shipped.
|
||||
* @param {string} fulfillmentId - the id of the specific fulfillment that
|
||||
* has been shipped
|
||||
* @param {Array<string>} trackingNumbers - the tracking numbers associated
|
||||
* with the shipment
|
||||
* @param {object} metadata - optional metadata to attach to the shipment.
|
||||
* @returns {Promise<Swap>} the updated swap with new fulfillments and status.
|
||||
*/
|
||||
async createShipment(swapId, fulfillmentId, trackingNumbers, metadata = {}) {
|
||||
const swap = await this.retrieve(swapId)
|
||||
|
||||
// Update the fulfillment to register
|
||||
const updatedFulfillments = await Promise.all(
|
||||
swap.fulfillments.map(f => {
|
||||
if (f._id.equals(fulfillmentId)) {
|
||||
return this.fulfillmentService_.createShipment(
|
||||
{
|
||||
items: swap.additional_items,
|
||||
shipping_methods: swap.shipping_methods,
|
||||
},
|
||||
f,
|
||||
trackingNumbers,
|
||||
metadata
|
||||
)
|
||||
}
|
||||
return f
|
||||
})
|
||||
)
|
||||
|
||||
// Go through all the additional items in the swap
|
||||
const updatedItems = swap.additional_items.map(i => {
|
||||
let shipmentItem
|
||||
for (const fulfillment of updatedFulfillments) {
|
||||
const item = fulfillment.items.find(fi => i._id.equals(fi._id))
|
||||
if (!!item) {
|
||||
shipmentItem = item
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (shipmentItem) {
|
||||
const shippedQuantity = i.shipped_quantity + shipmentItem.quantity
|
||||
return {
|
||||
...i,
|
||||
shipped: i.quantity === shippedQuantity,
|
||||
shipped_quantity: shippedQuantity,
|
||||
}
|
||||
}
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
const fulfillment_status = updatedItems.every(i => i.shipped)
|
||||
? "shipped"
|
||||
: "partially_shipped"
|
||||
|
||||
return this.swapModel_
|
||||
.updateOne(
|
||||
{ _id: swapId },
|
||||
{
|
||||
$set: {
|
||||
fulfillment_status,
|
||||
additional_items: updatedItems,
|
||||
fulfillments: updatedFulfillments,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
this.eventBus_.emit(SwapService.Events.SHIPMENT_CREATED, {
|
||||
swap_id: swapId,
|
||||
shipment: result.fulfillments.find(f => f._id.equals(fulfillmentId)),
|
||||
})
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated method to set metadata for a swap.
|
||||
* To ensure that plugins does not overwrite each
|
||||
* others metadata fields, setMetadata is provided.
|
||||
* @param {string} swapId - the swap to decorate.
|
||||
* @param {string} key - key for metadata field
|
||||
* @param {string} value - value for metadata field.
|
||||
* @return {Promise<Swap>} resolves to the updated result.
|
||||
*/
|
||||
async setMetadata(swapId, key, value) {
|
||||
const validatedId = this.validateId_(swapId)
|
||||
|
||||
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.swapModel_
|
||||
.updateOne({ _id: validatedId }, { $set: { [keyPath]: value } })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated method to delete metadata for a swap.
|
||||
* @param {string} swapId - the order to delete metadata from.
|
||||
* @param {string} key - key for metadata field
|
||||
* @return {Promise} resolves to the updated result.
|
||||
*/
|
||||
async deleteMetadata(swapId, key) {
|
||||
const validatedId = this.validateId_(swapId)
|
||||
|
||||
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.swapModel_
|
||||
.updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default SwapService
|
||||
Reference in new issue
Block a user