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
@@ -18,3 +18,14 @@ A different flow that is less common follows the steps:
|
|||||||
In Medusa Admin return shipping options are created in the same way that outgoing shipping options are created. Each return shipping option is associated with a region giving you the flexibility to price returns differently depending on the region the order has been placed in. Returns are not required to have shipping methods as it may be the case that return is arranged independently of a fulfillment provider.
|
In Medusa Admin return shipping options are created in the same way that outgoing shipping options are created. Each return shipping option is associated with a region giving you the flexibility to price returns differently depending on the region the order has been placed in. Returns are not required to have shipping methods as it may be the case that return is arranged independently of a fulfillment provider.
|
||||||
|
|
||||||
To create a return in Medusa Admin the store operator finds the original order and clicks "Create Return", the store operator then selects the items to be returned along with a shipping option, once the return is created the fulfillment provider takes care of providing the necessary documentation for the return; this can also be viewed in Medusa Admin.
|
To create a return in Medusa Admin the store operator finds the original order and clicks "Create Return", the store operator then selects the items to be returned along with a shipping option, once the return is created the fulfillment provider takes care of providing the necessary documentation for the return; this can also be viewed in Medusa Admin.
|
||||||
|
|
||||||
|
## Swaps
|
||||||
|
|
||||||
|
A swap can be used in cases where a customer wishes to exchange previously purchased items for different items. Usually this occurs if the customer wants to change the size or color of an item. In Medusa a swap can be initiated to handle the administrative tasks around swaps i.e. requesting a return, taking a payment from the customer for any shipping expenses and fulfilling the new items.
|
||||||
|
|
||||||
|
When a swap is created in Medusa Admin a return request will be initiated immediately generating return labels with the chosen fulfillment provider, furthermore a cart will be created and a payment link generated which can be sent to the customer to authorize a payment. When the return is received the swap can be marked as received and usually this will be where the new goods are to be sent out. The new goods are sent with the shipping method chosen by the customer in the payment process.
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
- At the moment only swaps that require a payment are supported i.e. swaps that would result in a refund to the customer are not possible.
|
||||||
|
- The customer must have paid for their swap before the items can be returned.
|
||||||
|
|
||||||
@@ -48,10 +48,13 @@ export default (rootDirectory) => {
|
|||||||
"/webshipper/shipments",
|
"/webshipper/shipments",
|
||||||
bodyParser.raw({ type: "application/vnd.api+json" }),
|
bodyParser.raw({ type: "application/vnd.api+json" }),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
|
const webshipperService = req.scope.resolve(
|
||||||
|
"webshipperFulfillmentService"
|
||||||
|
)
|
||||||
const eventBus = req.scope.resolve("eventBusService")
|
const eventBus = req.scope.resolve("eventBusService")
|
||||||
const logger = req.scope.resolve("logger")
|
const logger = req.scope.resolve("logger")
|
||||||
|
|
||||||
const secret = `da791d87513eb091640f9fb6c4b94384`
|
const secret = webshipperService.options_.webhook_secret
|
||||||
const hmac = crypto.createHmac("sha256", secret)
|
const hmac = crypto.createHmac("sha256", secret)
|
||||||
const digest = hmac.update(req.body).digest("base64")
|
const digest = hmac.update(req.body).digest("base64")
|
||||||
const hash = req.header("x-webshipper-hmac-sha256")
|
const hash = req.header("x-webshipper-hmac-sha256")
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import Webshipper from "../utils/webshipper"
|
|||||||
class WebshipperFulfillmentService extends FulfillmentService {
|
class WebshipperFulfillmentService extends FulfillmentService {
|
||||||
static identifier = "webshipper"
|
static identifier = "webshipper"
|
||||||
|
|
||||||
constructor({ logger, orderService }, options) {
|
constructor({ logger, swapService, orderService }, options) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.logger_ = logger
|
this.logger_ = logger
|
||||||
this.orderService_ = orderService
|
this.orderService_ = orderService
|
||||||
this.options_ = options
|
this.options_ = options
|
||||||
|
this.swapService_ = swapService
|
||||||
this.client_ = new Webshipper({
|
this.client_ = new Webshipper({
|
||||||
account: this.options_.account,
|
account: this.options_.account,
|
||||||
token: this.options_.api_token,
|
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 { shipping_address } = fromOrder
|
||||||
const newOrder = {
|
const newOrder = {
|
||||||
type: "orders",
|
type: "orders",
|
||||||
attributes: {
|
attributes: {
|
||||||
status: "pending",
|
status: "pending",
|
||||||
ext_ref: `${fromOrder._id}.${fromOrder.fulfillments.length}`,
|
ext_ref,
|
||||||
visible_ref: `${fromOrder.display_id}-${
|
visible_ref,
|
||||||
fromOrder.fulfillments.length + 1
|
|
||||||
}`,
|
|
||||||
order_lines: fulfillmentItems.map((item) => {
|
order_lines: fulfillmentItems.map((item) => {
|
||||||
return {
|
return {
|
||||||
ext_ref: item._id,
|
ext_ref: item._id,
|
||||||
@@ -325,14 +334,24 @@ class WebshipperFulfillmentService extends FulfillmentService {
|
|||||||
"."
|
"."
|
||||||
)
|
)
|
||||||
|
|
||||||
const order = await this.orderService_.retrieve(orderId)
|
if (orderId.charAt(0) === "S") {
|
||||||
const fulfillment = order.fulfillments[fulfillmentIndex]
|
const swap = await this.swapService_.retrieve(orderId.substring(1))
|
||||||
if (fulfillment) {
|
const fulfillment = swap.fulfillments[fulfillmentIndex]
|
||||||
await this.orderService_.createShipment(
|
await this.swapService_.createShipment(
|
||||||
order._id,
|
swap._id,
|
||||||
fulfillment._id,
|
fulfillment._id,
|
||||||
trackingNumbers
|
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.
|
* @param {object} obj - the object to decorate.
|
||||||
* @return {object} the decorated object.
|
* @return {object} the decorated object.
|
||||||
*/
|
*/
|
||||||
runDecorators_(obj) {
|
runDecorators_(obj, fields = [], expandFields = []) {
|
||||||
return this.decorators_.reduce(async (acc, next) => {
|
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))
|
}, Promise.resolve(obj))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@babel/preset-env": "^7.7.5",
|
"@babel/preset-env": "^7.7.5",
|
||||||
"@babel/register": "^7.7.4",
|
"@babel/register": "^7.7.4",
|
||||||
"@babel/runtime": "^7.9.6",
|
"@babel/runtime": "^7.9.6",
|
||||||
|
"axios-mock-adapter": "^1.19.0",
|
||||||
"client-sessions": "^0.8.0",
|
"client-sessions": "^0.8.0",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-classes": "^7.9.5",
|
"@babel/plugin-transform-classes": "^7.9.5",
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.21.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"medusa-core-utils": "^1.0.10",
|
"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(
|
const klarnaOrder = await klarnaProviderService.retrieveCompletedOrder(
|
||||||
klarna_order_id
|
klarna_order_id
|
||||||
).then(({ data }) => data)
|
)
|
||||||
|
|
||||||
const cartId = klarnaOrder.merchant_data
|
const cartId = klarnaOrder.merchant_data
|
||||||
try {
|
try {
|
||||||
const order = await orderService.retrieveByCartId(cartId)
|
const order = await orderService.retrieveByCartId(cartId)
|
||||||
await klarnaProviderService.acknowledgeOrder(klarnaOrder.order_id, order._id)
|
await klarnaProviderService.acknowledgeOrder(
|
||||||
|
klarnaOrder.order_id,
|
||||||
|
order._id
|
||||||
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.type === MedusaError.Types.NOT_FOUND) {
|
if (err.type === MedusaError.Types.NOT_FOUND) {
|
||||||
const cart = await cartService.retrieve(cartId)
|
const cart = await cartService.retrieve(cartId)
|
||||||
const order = await orderService.createFromCart(cart)
|
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 KlarnaProviderService from "../klarna-provider"
|
||||||
import mockAxios from "../../__mocks__/axios"
|
|
||||||
import { carts } from "../../__mocks__/cart"
|
import { carts } from "../../__mocks__/cart"
|
||||||
import { TotalsServiceMock } from "../../__mocks__/totals"
|
import { TotalsServiceMock } from "../../__mocks__/totals"
|
||||||
import { RegionServiceMock } from "../../__mocks__/region"
|
import { RegionServiceMock } from "../../__mocks__/region"
|
||||||
|
|
||||||
describe("KlarnaProviderService", () => {
|
describe("KlarnaProviderService", () => {
|
||||||
beforeEach(() => {
|
|
||||||
mockAxios.mockClear()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("createPayment", () => {
|
describe("createPayment", () => {
|
||||||
const klarnaProviderService = new KlarnaProviderService(
|
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(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("creates Klarna order", async () => {
|
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)
|
const result = await klarnaProviderService.createPayment(carts.frCart)
|
||||||
expect(mockAxios.post).toHaveBeenCalledTimes(1)
|
|
||||||
|
// expect(mockAxios.post).toHaveBeenCalledTimes(1)
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
order_id: "123456789",
|
order_id: "123456789",
|
||||||
order_amount: 100,
|
order_amount: 100,
|
||||||
@@ -67,21 +61,14 @@ describe("KlarnaProviderService", () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
it("returns Klarna order", async () => {
|
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||||
mockAxios.get.mockImplementation((data) => {
|
mockServer.onGet("/checkout/v3/orders/123456789").reply(() => {
|
||||||
return Promise.resolve({
|
return [200, { order_id: "123456789" }]
|
||||||
data: {
|
})
|
||||||
order_id: "123456789",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
it("returns Klarna order", async () => {
|
||||||
result = await klarnaProviderService.retrievePayment({
|
result = await klarnaProviderService.retrievePayment({
|
||||||
payment_method: {
|
order_id: "123456789",
|
||||||
data: {
|
|
||||||
id: "123456789",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
@@ -107,20 +94,13 @@ describe("KlarnaProviderService", () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
it("returns completed Klarna order", async () => {
|
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||||
mockAxios.get.mockImplementation((data) => {
|
mockServer.onGet("/ordermanagement/v1/orders/123456789").reply(() => {
|
||||||
return Promise.resolve({
|
return [200, { order_id: "123456789" }]
|
||||||
order_id: "123456789",
|
})
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
result = await klarnaProviderService.retrieveCompletedOrder({
|
it("returns completed Klarna order", async () => {
|
||||||
payment_method: {
|
result = await klarnaProviderService.retrieveCompletedOrder("123456789")
|
||||||
data: {
|
|
||||||
id: "123456789",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
order_id: "123456789",
|
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 () => {
|
it("returns updated Klarna order", async () => {
|
||||||
mockAxios.post.mockImplementation((data) => {
|
|
||||||
return Promise.resolve({
|
|
||||||
data: {
|
|
||||||
order_id: "123456789",
|
|
||||||
order_amount: 1000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
result = await klarnaProviderService.updatePayment(
|
result = await klarnaProviderService.updatePayment(
|
||||||
{
|
{
|
||||||
payment_method: {
|
order_id: "123456789",
|
||||||
data: {
|
|
||||||
id: "123456789",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
carts.frCart
|
carts.frCart
|
||||||
)
|
)
|
||||||
@@ -195,12 +172,14 @@ describe("KlarnaProviderService", () => {
|
|||||||
password: "123456789",
|
password: "123456789",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||||
it("returns order id", async () => {
|
mockServer
|
||||||
mockAxios.post.mockImplementation((data) => {
|
.onPost("/ordermanagement/v1/orders/123456789/cancel")
|
||||||
return Promise.resolve()
|
.reply(() => {
|
||||||
|
return [200]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("returns order id", async () => {
|
||||||
result = await klarnaProviderService.cancelPayment({
|
result = await klarnaProviderService.cancelPayment({
|
||||||
order_id: "123456789",
|
order_id: "123456789",
|
||||||
})
|
})
|
||||||
@@ -226,12 +205,20 @@ describe("KlarnaProviderService", () => {
|
|||||||
password: "123456789",
|
password: "123456789",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||||
it("returns order id", async () => {
|
mockServer
|
||||||
mockAxios.post.mockImplementation((data) => {
|
.onPost("/ordermanagement/v1/orders/123456789/acknowledge")
|
||||||
return Promise.resolve({})
|
.reply(() => {
|
||||||
|
return [200]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mockServer
|
||||||
|
.onPatch("/ordermanagement/v1/orders/123456789/merchant-references")
|
||||||
|
.reply(() => {
|
||||||
|
return [200]
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns order id", async () => {
|
||||||
result = await klarnaProviderService.acknowledgeOrder("123456789")
|
result = await klarnaProviderService.acknowledgeOrder("123456789")
|
||||||
|
|
||||||
expect(result).toEqual("123456789")
|
expect(result).toEqual("123456789")
|
||||||
@@ -255,12 +242,14 @@ describe("KlarnaProviderService", () => {
|
|||||||
password: "123456789",
|
password: "123456789",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||||
it("returns order id", async () => {
|
mockServer
|
||||||
mockAxios.post.mockImplementation((data) => {
|
.onPost("/ordermanagement/v1/orders/123456789/merchant-references")
|
||||||
return Promise.resolve()
|
.reply(() => {
|
||||||
|
return [200]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("returns order id", async () => {
|
||||||
result = await klarnaProviderService.addOrderToKlarnaOrder(
|
result = await klarnaProviderService.addOrderToKlarnaOrder(
|
||||||
"123456789",
|
"123456789",
|
||||||
"order123456789"
|
"order123456789"
|
||||||
@@ -287,20 +276,23 @@ describe("KlarnaProviderService", () => {
|
|||||||
password: "123456789",
|
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 () => {
|
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({
|
result = await klarnaProviderService.capturePayment({
|
||||||
order_id: "123456789",
|
order_id: "123456789",
|
||||||
})
|
})
|
||||||
@@ -326,12 +318,14 @@ describe("KlarnaProviderService", () => {
|
|||||||
password: "123456789",
|
password: "123456789",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
|
||||||
it("returns order id", async () => {
|
mockServer
|
||||||
mockAxios.post.mockImplementation((data) => {
|
.onPost("/ordermanagement/v1/orders/123456789/refunds")
|
||||||
return Promise.resolve()
|
.reply(() => {
|
||||||
|
return [200]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("returns order id", async () => {
|
||||||
result = await klarnaProviderService.refundPayment(
|
result = await klarnaProviderService.refundPayment(
|
||||||
{
|
{
|
||||||
order_id: "123456789",
|
order_id: "123456789",
|
||||||
|
|||||||
@@ -279,9 +279,9 @@ class KlarnaProviderService extends PaymentService {
|
|||||||
*/
|
*/
|
||||||
async retrieveCompletedOrder(klarnaOrderId) {
|
async retrieveCompletedOrder(klarnaOrderId) {
|
||||||
try {
|
try {
|
||||||
return this.klarna_.get(
|
return this.klarna_
|
||||||
`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}`
|
.get(`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}`)
|
||||||
)
|
.then(({ data }) => data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -964,43 +964,17 @@
|
|||||||
exec-sh "^0.3.2"
|
exec-sh "^0.3.2"
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
|
|
||||||
"@hapi/address@^2.1.2":
|
"@hapi/hoek@^9.0.0":
|
||||||
version "2.1.4"
|
version "9.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
|
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6"
|
||||||
integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==
|
integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw==
|
||||||
|
|
||||||
"@hapi/formula@^1.2.0":
|
"@hapi/topo@^5.0.0":
|
||||||
version "1.2.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-1.2.0.tgz#994649c7fea1a90b91a0a1e6d983523f680e10cd"
|
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7"
|
||||||
integrity sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA==
|
integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==
|
||||||
|
|
||||||
"@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==
|
|
||||||
dependencies:
|
dependencies:
|
||||||
"@hapi/address" "^2.1.2"
|
"@hapi/hoek" "^9.0.0"
|
||||||
"@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"
|
|
||||||
|
|
||||||
"@istanbuljs/load-nyc-config@^1.0.0":
|
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
@@ -1185,6 +1159,23 @@
|
|||||||
"@types/yargs" "^15.0.0"
|
"@types/yargs" "^15.0.0"
|
||||||
chalk "^3.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":
|
"@sinonjs/commons@^1.7.0":
|
||||||
version "1.7.2"
|
version "1.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
|
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"
|
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
|
||||||
integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
|
integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
|
||||||
|
|
||||||
axios@^0.19.2:
|
axios-mock-adapter@^1.19.0:
|
||||||
version "0.19.2"
|
version "1.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
|
resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.19.0.tgz#9d72e321a6c5418e1eff067aa99761a86c5188a4"
|
||||||
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
|
integrity sha512-D+0U4LNPr7WroiBDvWilzTMYPYTuZlbo6BI8YHZtj7wYQS8NkARlP9KBt8IWWHTQJ0q/8oZ0ClPBtKCCkx8cQg==
|
||||||
dependencies:
|
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:
|
babel-jest@^25.5.1:
|
||||||
version "25.5.1"
|
version "25.5.1"
|
||||||
@@ -2006,7 +2005,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "2.0.0"
|
ms "2.0.0"
|
||||||
|
|
||||||
debug@3.1.0, debug@=3.1.0:
|
debug@3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||||
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
|
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"
|
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
|
||||||
integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
|
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:
|
fast-json-stable-stringify@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
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"
|
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
|
||||||
integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
|
integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
|
||||||
|
|
||||||
follow-redirects@1.5.10:
|
follow-redirects@^1.10.0:
|
||||||
version "1.5.10"
|
version "1.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
|
||||||
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
|
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
|
||||||
dependencies:
|
|
||||||
debug "=3.1.0"
|
|
||||||
|
|
||||||
for-in@^1.0.2:
|
for-in@^1.0.2:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||||
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
|
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:
|
is-callable@^1.1.4, is-callable@^1.1.5:
|
||||||
version "1.1.5"
|
version "1.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
|
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"
|
resolved "https://registry.yarnpkg.com/joi-objectid/-/joi-objectid-3.0.1.tgz#63ace7860f8e1a993a28d40c40ffd8eff01a3668"
|
||||||
integrity sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ==
|
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:
|
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
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"
|
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||||
|
|
||||||
medusa-core-utils@^0.3.0:
|
medusa-core-utils@^1.0.10:
|
||||||
version "0.1.39"
|
version "1.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-0.1.39.tgz#d57816c9bd43f9a92883650c1e66add1665291df"
|
resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.0.10.tgz#6175f1e428318205742621f78d387254fc85b8a1"
|
||||||
integrity sha512-R8+U1ile7if+nR6Cjh5exunx0ETV0OfkWUUBUpz1KmHSDv0V0CcvQqU9lcZesPFDEbu3Y2iEjsCqidVA4nG2nQ==
|
integrity sha512-z12ITzPl5UpDJTILNXcMGD4yujSRkuvVtxkrvqCmA44IEyRj1hWZ0dGbVzuHj2wIahDHFFg66oq91IQH5QVsyg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@hapi/joi" "^16.1.8"
|
joi "^17.2.1"
|
||||||
joi-objectid "^3.0.1"
|
joi-objectid "^3.0.1"
|
||||||
|
|
||||||
medusa-interfaces@^0.3.0:
|
medusa-test-utils@^1.0.11:
|
||||||
version "0.1.39"
|
version "1.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-0.1.39.tgz#633db3c8c6afd7fec9ae24496737369d840105cf"
|
resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.0.11.tgz#bae901efa90426fb64818de700dc6e163820160f"
|
||||||
integrity sha512-byKIcK7o3L4shmGn+pgZAUyLrT991zCqK4jWXIleQJbGImQy6TmdXido+tEzFptVBJWMIQ8BWnP/b7r29D8EXA==
|
integrity sha512-CSNb70sXOfKTndzxWtPMYq+KeYaFkSLAPUyhuwDNVvkbne0faOYF5OMCV8r3aLoQ3D1Tvjrb/XB9Qt17ycjuPw==
|
||||||
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==
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@babel/plugin-transform-classes" "^7.9.5"
|
||||||
|
medusa-core-utils "^1.0.10"
|
||||||
mongoose "^5.8.0"
|
mongoose "^5.8.0"
|
||||||
|
|
||||||
memory-pager@^1.0.2:
|
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),
|
event_owner: [the id of the user who will own goods out events] (required),
|
||||||
warehouse: [the warehouse id to allocate orders from] (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)
|
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)
|
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)
|
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)
|
shipping_account_code: [nominal code to assign shipping line to] (optional: defaults to 4040)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class BrightpearlService extends BaseService {
|
|||||||
productVariantService,
|
productVariantService,
|
||||||
regionService,
|
regionService,
|
||||||
orderService,
|
orderService,
|
||||||
|
swapService,
|
||||||
discountService,
|
discountService,
|
||||||
},
|
},
|
||||||
options
|
options
|
||||||
@@ -23,6 +24,7 @@ class BrightpearlService extends BaseService {
|
|||||||
this.totalsService_ = totalsService
|
this.totalsService_ = totalsService
|
||||||
this.discountService_ = discountService
|
this.discountService_ = discountService
|
||||||
this.oauthService_ = oauthService
|
this.oauthService_ = oauthService
|
||||||
|
this.swapService_ = swapService
|
||||||
}
|
}
|
||||||
|
|
||||||
async getClient() {
|
async getClient() {
|
||||||
@@ -456,10 +458,9 @@ class BrightpearlService extends BaseService {
|
|||||||
.create(order)
|
.create(order)
|
||||||
.then(async (salesOrderId) => {
|
.then(async (salesOrderId) => {
|
||||||
const order = await client.orders.retrieve(salesOrderId)
|
const order = await client.orders.retrieve(salesOrderId)
|
||||||
const resResult = await client.warehouses.createReservation(
|
await client.warehouses
|
||||||
order,
|
.createReservation(order, this.options.warehouse)
|
||||||
this.options.warehouse
|
.catch(() => {})
|
||||||
)
|
|
||||||
return salesOrderId
|
return salesOrderId
|
||||||
})
|
})
|
||||||
.then((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) {
|
async createPayment(fromOrder) {
|
||||||
const client = await this.getClient()
|
const client = await this.getClient()
|
||||||
const soId =
|
const soId =
|
||||||
@@ -484,7 +669,7 @@ class BrightpearlService extends BaseService {
|
|||||||
const payment = {
|
const payment = {
|
||||||
transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref
|
transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref
|
||||||
transactionCode: fromOrder._id,
|
transactionCode: fromOrder._id,
|
||||||
paymentMethodCode: "1220",
|
paymentMethodCode: this.options.payment_method_code || "1220",
|
||||||
orderId: soId,
|
orderId: soId,
|
||||||
paymentDate: new Date(),
|
paymentDate: new Date(),
|
||||||
currencyIsoCode: fromOrder.currency_code,
|
currencyIsoCode: fromOrder.currency_code,
|
||||||
@@ -570,6 +755,7 @@ class BrightpearlService extends BaseService {
|
|||||||
nominalCode: this.options.shipping_account_code || "4040",
|
nominalCode: this.options.shipping_account_code || "4040",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,6 +816,16 @@ class BrightpearlService extends BaseService {
|
|||||||
})
|
})
|
||||||
.filter((i) => !!i)
|
.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, {
|
return this.orderService_.createFulfillment(order.externalRef, lines, {
|
||||||
goods_out_note: id,
|
goods_out_note: id,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
class OrderSubscriber {
|
class OrderSubscriber {
|
||||||
constructor({ eventBusService, orderService, brightpearlService }) {
|
constructor({
|
||||||
|
eventBusService,
|
||||||
|
orderService,
|
||||||
|
swapService,
|
||||||
|
brightpearlService,
|
||||||
|
}) {
|
||||||
this.orderService_ = orderService
|
this.orderService_ = orderService
|
||||||
this.brightpearlService_ = brightpearlService
|
this.brightpearlService_ = brightpearlService
|
||||||
|
this.swapService_ = swapService
|
||||||
|
|
||||||
eventBusService.subscribe("order.refund_created", this.registerRefund)
|
eventBusService.subscribe("order.refund_created", this.registerRefund)
|
||||||
|
|
||||||
@@ -15,6 +21,11 @@ class OrderSubscriber {
|
|||||||
)
|
)
|
||||||
|
|
||||||
eventBusService.subscribe("order.shipment_created", this.registerShipment)
|
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) => {
|
sendToBrightpearl = (order) => {
|
||||||
@@ -25,8 +36,38 @@ class OrderSubscriber {
|
|||||||
return this.brightpearlService_.createPayment(order)
|
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) => {
|
registerShipment = async (data) => {
|
||||||
const { order_id, shipment } = data
|
const { shipment } = data
|
||||||
const noteId = shipment.metadata.goods_out_note
|
const noteId = shipment.metadata.goods_out_note
|
||||||
if (noteId) {
|
if (noteId) {
|
||||||
await this.brightpearlService_.registerGoodsOutTrackingNumber(
|
await this.brightpearlService_.registerGoodsOutTrackingNumber(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
try {
|
try {
|
||||||
|
const orderService = req.scope.resolve("orderService")
|
||||||
const customerService = req.scope.resolve("customerService")
|
const customerService = req.scope.resolve("customerService")
|
||||||
let customer = await customerService.retrieve(id)
|
let customer = await customerService.retrieve(id)
|
||||||
customer = await customerService.decorate(
|
customer = await customerService.decorate(
|
||||||
@@ -12,9 +13,14 @@ export default async (req, res) => {
|
|||||||
"shipping_addresses",
|
"shipping_addresses",
|
||||||
"phone",
|
"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 })
|
res.json({ customer })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err
|
throw err
|
||||||
|
|||||||
@@ -16,11 +16,18 @@ export default async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const orderService = req.scope.resolve("orderService")
|
||||||
const customerService = req.scope.resolve("customerService")
|
const customerService = req.scope.resolve("customerService")
|
||||||
await customerService.update(id, value)
|
await customerService.update(id, value)
|
||||||
|
|
||||||
const customer = await customerService.retrieve(id)
|
const customer = await customerService.retrieve(id)
|
||||||
const data = await customerService.decorate(customer)
|
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 })
|
res.status(200).json({ customer: data })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err
|
throw err
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ export default async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const orderService = req.scope.resolve("orderService")
|
const orderService = req.scope.resolve("orderService")
|
||||||
let order = await orderService.archive(id)
|
let order = await orderService.archive(id)
|
||||||
order = await orderService.decorate(order, [], ["region"])
|
order = await orderService.decorate(
|
||||||
|
order,
|
||||||
|
[],
|
||||||
|
["region", "customer", "swaps"]
|
||||||
|
)
|
||||||
res.json({ order })
|
res.json({ order })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ export default async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const orderService = req.scope.resolve("orderService")
|
const orderService = req.scope.resolve("orderService")
|
||||||
let order = await orderService.cancel(id)
|
let order = await orderService.cancel(id)
|
||||||
order = await orderService.decorate(order, [], ["region"])
|
order = await orderService.decorate(
|
||||||
|
order,
|
||||||
|
[],
|
||||||
|
["region", "customer", "swaps"]
|
||||||
|
)
|
||||||
res.json({ order })
|
res.json({ order })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ export default async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const orderService = req.scope.resolve("orderService")
|
const orderService = req.scope.resolve("orderService")
|
||||||
let order = await orderService.capturePayment(id)
|
let order = await orderService.capturePayment(id)
|
||||||
order = await orderService.decorate(order, [], ["region"])
|
order = await orderService.decorate(
|
||||||
|
order,
|
||||||
|
[],
|
||||||
|
["region", "customer", "swaps"]
|
||||||
|
)
|
||||||
res.json({ order })
|
res.json({ order })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw 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 {
|
try {
|
||||||
const orderService = req.scope.resolve("orderService")
|
const orderService = req.scope.resolve("orderService")
|
||||||
let order = await orderService.completeOrder(id)
|
let order = await orderService.completeOrder(id)
|
||||||
order = await orderService.decorate(order, [], ["region"])
|
order = await orderService.decorate(order, [], ["region", "customer", "swaps"])
|
||||||
res.json({ order })
|
res.json({ order })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
|||||||
@@ -26,8 +26,14 @@ export default async (req, res) => {
|
|||||||
value.items,
|
value.items,
|
||||||
value.metadata
|
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) {
|
} catch (error) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ export default async (req, res) => {
|
|||||||
value.fulfillment_id,
|
value.fulfillment_id,
|
||||||
value.tracking_numbers
|
value.tracking_numbers
|
||||||
)
|
)
|
||||||
order = await orderService.decorate(order, [], ["region"])
|
order = await orderService.decorate(
|
||||||
|
order,
|
||||||
|
[],
|
||||||
|
["region", "customer", "swaps"]
|
||||||
|
)
|
||||||
res.json({ order })
|
res.json({ order })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw 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")
|
const customerService = req.scope.resolve("customerService")
|
||||||
|
|
||||||
let order = await orderService.retrieve(id)
|
let order = await orderService.retrieve(id)
|
||||||
order = await orderService.decorate(order, [], ["region", "customer"])
|
order = await orderService.decorate(
|
||||||
|
order,
|
||||||
if (order.customer_id) {
|
[],
|
||||||
order.customer = await customerService.retrieve(order.customer_id)
|
["region", "customer", "swaps"]
|
||||||
}
|
)
|
||||||
|
|
||||||
res.json({ order })
|
res.json({ order })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -95,6 +95,43 @@ export default app => {
|
|||||||
middlewares.wrap(require("./archive-order").default)
|
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.
|
* Set metadata key / value pair.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -25,14 +25,18 @@ export default async (req, res) => {
|
|||||||
if (typeof value.refund !== "undefined" && value.refund < 0) {
|
if (typeof value.refund !== "undefined" && value.refund < 0) {
|
||||||
refundAmount = 0
|
refundAmount = 0
|
||||||
}
|
}
|
||||||
let order = await orderService.return(
|
let order = await orderService.receiveReturn(
|
||||||
id,
|
id,
|
||||||
return_id,
|
return_id,
|
||||||
value.items,
|
value.items,
|
||||||
refundAmount,
|
refundAmount,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
order = await orderService.decorate(order, [], ["region"])
|
order = await orderService.decorate(
|
||||||
|
order,
|
||||||
|
[],
|
||||||
|
["region", "customer", "swaps"]
|
||||||
|
)
|
||||||
|
|
||||||
res.status(200).json({ order })
|
res.status(200).json({ order })
|
||||||
} catch (err) {
|
} 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.reason,
|
||||||
value.note
|
value.note
|
||||||
)
|
)
|
||||||
order = await orderService.decorate(order, [], ["region"])
|
order = await orderService.decorate(
|
||||||
|
order,
|
||||||
|
[],
|
||||||
|
["region", "customer", "swaps"]
|
||||||
|
)
|
||||||
|
|
||||||
res.status(200).json({ order })
|
res.status(200).json({ order })
|
||||||
} catch (err) {
|
} 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 })
|
res.status(200).json({ order })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ export default async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const orderService = req.scope.resolve("orderService")
|
const orderService = req.scope.resolve("orderService")
|
||||||
let order = await orderService.setMetadata(id, value.key, value.value)
|
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 })
|
res.status(200).json({ order })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ export default async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const orderService = req.scope.resolve("orderService")
|
const orderService = req.scope.resolve("orderService")
|
||||||
let order = await orderService.update(id, value)
|
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 })
|
res.status(200).json({ order })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import _ from "lodash"
|
|||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const variantService = req.scope.resolve("productVariantService")
|
||||||
const productService = req.scope.resolve("productService")
|
const productService = req.scope.resolve("productService")
|
||||||
const queryBuilderService = req.scope.resolve("queryBuilderService")
|
const queryBuilderService = req.scope.resolve("queryBuilderService")
|
||||||
|
|
||||||
@@ -14,6 +15,22 @@ export default async (req, res) => {
|
|||||||
query.is_giftcard = req.query.is_giftcard === "true"
|
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 limit = parseInt(req.query.limit) || 0
|
||||||
const offset = parseInt(req.query.offset) || 0
|
const offset = parseInt(req.query.offset) || 0
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { MedusaError, Validator } from "medusa-core-utils"
|
|||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const schema = Validator.object().keys({
|
const schema = Validator.object().keys({
|
||||||
name: Validator.string(),
|
name: Validator.string(),
|
||||||
|
swap_link_template: Validator.string(),
|
||||||
default_currency: Validator.string(),
|
default_currency: Validator.string(),
|
||||||
currencies: Validator.array().items(Validator.string()),
|
currencies: Validator.array().items(Validator.string()),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import orderRoutes from "./orders"
|
|||||||
import customerRoutes from "./customers"
|
import customerRoutes from "./customers"
|
||||||
import shippingOptionRoutes from "./shipping-options"
|
import shippingOptionRoutes from "./shipping-options"
|
||||||
import regionRoutes from "./regions"
|
import regionRoutes from "./regions"
|
||||||
|
import swapRoutes from "./swaps"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ export default (app, container, config) => {
|
|||||||
cartRoutes(route, container)
|
cartRoutes(route, container)
|
||||||
shippingOptionRoutes(route)
|
shippingOptionRoutes(route)
|
||||||
regionRoutes(route)
|
regionRoutes(route)
|
||||||
|
swapRoutes(route)
|
||||||
|
|
||||||
return app
|
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: {
|
processedOrder: {
|
||||||
_id: IdMap.getId("processed-order"),
|
_id: IdMap.getId("processed-order"),
|
||||||
email: "oliver@test.dk",
|
email: "oliver@test.dk",
|
||||||
|
tax_rate: 0,
|
||||||
billing_address: {
|
billing_address: {
|
||||||
first_name: "Oli",
|
first_name: "Oli",
|
||||||
last_name: "Medusa",
|
last_name: "Medusa",
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ class CartModel extends BaseModel {
|
|||||||
shipping_options: { type: [ShippingMethodSchema], default: [] },
|
shipping_options: { type: [ShippingMethodSchema], default: [] },
|
||||||
payment_method: { type: PaymentMethodSchema },
|
payment_method: { type: PaymentMethodSchema },
|
||||||
shipping_methods: { type: [ShippingMethodSchema], default: [] },
|
shipping_methods: { type: [ShippingMethodSchema], default: [] },
|
||||||
|
is_swap: { type: Boolean, default: false },
|
||||||
|
created: { type: String, default: Date.now },
|
||||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import PaymentMethodSchema from "./schemas/payment-method"
|
|||||||
import ShippingMethodSchema from "./schemas/shipping-method"
|
import ShippingMethodSchema from "./schemas/shipping-method"
|
||||||
import AddressSchema from "./schemas/address"
|
import AddressSchema from "./schemas/address"
|
||||||
import DiscountSchema from "./schemas/discount"
|
import DiscountSchema from "./schemas/discount"
|
||||||
import ShipmentSchema from "./schemas/shipment"
|
|
||||||
import ReturnSchema from "./schemas/return"
|
import ReturnSchema from "./schemas/return"
|
||||||
import RefundSchema from "./schemas/refund"
|
import RefundSchema from "./schemas/refund"
|
||||||
import FulfillmentSchema from "./schemas/fulfillment"
|
import FulfillmentSchema from "./schemas/fulfillment"
|
||||||
@@ -35,8 +34,9 @@ class OrderModel extends BaseModel {
|
|||||||
region_id: { type: String, required: true },
|
region_id: { type: String, required: true },
|
||||||
discounts: { type: [DiscountSchema], default: [] },
|
discounts: { type: [DiscountSchema], default: [] },
|
||||||
customer_id: { type: String },
|
customer_id: { type: String },
|
||||||
payment_method: { type: PaymentMethodSchema, default: {} },
|
payment_method: { type: PaymentMethodSchema, required: true },
|
||||||
shipping_methods: { type: [ShippingMethodSchema], default: [] },
|
shipping_methods: { type: [ShippingMethodSchema], required: true },
|
||||||
|
swaps: { type: [String], default: [] },
|
||||||
documents: { type: [String], default: [] },
|
documents: { type: [String], default: [] },
|
||||||
created: { type: String, default: Date.now },
|
created: { type: String, default: Date.now },
|
||||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import mongoose from "mongoose"
|
|||||||
* @property {Object} metadata
|
* @property {Object} metadata
|
||||||
*/
|
*/
|
||||||
export default new mongoose.Schema({
|
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 },
|
content: { type: mongoose.Schema.Types.Mixed, required: true },
|
||||||
quantity: { type: Number, required: true },
|
quantity: { type: Number, required: true },
|
||||||
is_requested: { type: Boolean, required: true },
|
is_requested: { type: Boolean, required: true },
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class StoreModel extends BaseModel {
|
|||||||
currencies: { type: [String], default: [] },
|
currencies: { type: [String], default: [] },
|
||||||
payment_providers: { type: [String], default: [] },
|
payment_providers: { type: [String], default: [] },
|
||||||
fulfillment_providers: { type: [String], default: [] },
|
fulfillment_providers: { type: [String], default: [] },
|
||||||
|
swap_link_template: { type: String },
|
||||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
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"),
|
profile_id: IdMap.getId("profile1"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
tax_rate: 0,
|
||||||
fulfillment_status: "fulfilled",
|
fulfillment_status: "fulfilled",
|
||||||
payment_status: "captured",
|
payment_status: "captured",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
|
|||||||
@@ -148,6 +148,58 @@ describe("CartService", () => {
|
|||||||
region_id: IdMap.getId("testRegion"),
|
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", () => {
|
describe("addLineItem", () => {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { IdMap } from "medusa-test-utils"
|
|||||||
import { OrderModelMock, orders } from "../../models/__mocks__/order"
|
import { OrderModelMock, orders } from "../../models/__mocks__/order"
|
||||||
import { carts } from "../../models/__mocks__/cart"
|
import { carts } from "../../models/__mocks__/cart"
|
||||||
import OrderService from "../order"
|
import OrderService from "../order"
|
||||||
|
import ReturnService from "../return"
|
||||||
|
import FulfillmentService from "../fulfillment"
|
||||||
import {
|
import {
|
||||||
PaymentProviderServiceMock,
|
PaymentProviderServiceMock,
|
||||||
DefaultProviderMock,
|
DefaultProviderMock,
|
||||||
@@ -434,11 +436,15 @@ describe("OrderService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("createFulfillment", () => {
|
describe("createFulfillment", () => {
|
||||||
|
const fulfillmentService = new FulfillmentService({
|
||||||
|
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||||
|
shippingProfileService: ShippingProfileServiceMock,
|
||||||
|
totalsService: TotalsServiceMock,
|
||||||
|
})
|
||||||
const orderService = new OrderService({
|
const orderService = new OrderService({
|
||||||
orderModel: OrderModelMock,
|
orderModel: OrderModelMock,
|
||||||
paymentProviderService: PaymentProviderServiceMock,
|
paymentProviderService: PaymentProviderServiceMock,
|
||||||
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
fulfillmentService,
|
||||||
shippingProfileService: ShippingProfileServiceMock,
|
|
||||||
eventBusService: EventBusServiceMock,
|
eventBusService: EventBusServiceMock,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -509,8 +515,7 @@ describe("OrderService", () => {
|
|||||||
},
|
},
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
fulfilled_quantity: 10,
|
fulfilled_quantity: 0,
|
||||||
fulfilled: true,
|
|
||||||
quantity: 10,
|
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({
|
const orderService = new OrderService({
|
||||||
orderModel: OrderModelMock,
|
orderModel: OrderModelMock,
|
||||||
|
returnService,
|
||||||
|
shippingOptionService: ShippingOptionServiceMock,
|
||||||
|
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||||
paymentProviderService: PaymentProviderServiceMock,
|
paymentProviderService: PaymentProviderServiceMock,
|
||||||
totalsService: TotalsServiceMock,
|
totalsService: TotalsServiceMock,
|
||||||
eventBusService: EventBusServiceMock,
|
eventBusService: EventBusServiceMock,
|
||||||
@@ -573,7 +586,7 @@ describe("OrderService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("calls order model functions", async () => {
|
it("calls order model functions", async () => {
|
||||||
await orderService.return(
|
await orderService.receiveReturn(
|
||||||
IdMap.getId("returned-order"),
|
IdMap.getId("returned-order"),
|
||||||
IdMap.getId("return"),
|
IdMap.getId("return"),
|
||||||
[
|
[
|
||||||
@@ -671,7 +684,7 @@ describe("OrderService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("return with custom refund", async () => {
|
it("return with custom refund", async () => {
|
||||||
await orderService.return(
|
await orderService.receiveReturn(
|
||||||
IdMap.getId("returned-order"),
|
IdMap.getId("returned-order"),
|
||||||
IdMap.getId("return"),
|
IdMap.getId("return"),
|
||||||
[
|
[
|
||||||
@@ -770,7 +783,7 @@ describe("OrderService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("calls order model functions and sets partially_returned", async () => {
|
it("calls order model functions and sets partially_returned", async () => {
|
||||||
await orderService.return(
|
await orderService.receiveReturn(
|
||||||
IdMap.getId("order-refund"),
|
IdMap.getId("order-refund"),
|
||||||
IdMap.getId("return"),
|
IdMap.getId("return"),
|
||||||
[
|
[
|
||||||
@@ -882,7 +895,7 @@ describe("OrderService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("sets requires_action on additional items", async () => {
|
it("sets requires_action on additional items", async () => {
|
||||||
await orderService.return(
|
await orderService.receiveReturn(
|
||||||
IdMap.getId("order-refund"),
|
IdMap.getId("order-refund"),
|
||||||
IdMap.getId("return"),
|
IdMap.getId("return"),
|
||||||
[
|
[
|
||||||
@@ -930,17 +943,17 @@ describe("OrderService", () => {
|
|||||||
|
|
||||||
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1)
|
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||||
{ _id: IdMap.getId("order-refund") },
|
{ _id: IdMap.getId("order-refund"), "returns._id": originalReturn._id },
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
returns: [toSet],
|
"returns.$": toSet,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("sets requires_action on unmatcing quantities", async () => {
|
it("sets requires_action on unmatcing quantities", async () => {
|
||||||
await orderService.return(
|
await orderService.receiveReturn(
|
||||||
IdMap.getId("order-refund"),
|
IdMap.getId("order-refund"),
|
||||||
IdMap.getId("return"),
|
IdMap.getId("return"),
|
||||||
[
|
[
|
||||||
@@ -966,10 +979,10 @@ describe("OrderService", () => {
|
|||||||
|
|
||||||
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1)
|
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||||
{ _id: IdMap.getId("order-refund") },
|
{ _id: IdMap.getId("order-refund"), "returns._id": originalReturn._id },
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
returns: [toSet],
|
"returns.$": toSet,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -977,8 +990,14 @@ describe("OrderService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("requestReturn", () => {
|
describe("requestReturn", () => {
|
||||||
|
const returnService = new ReturnService({
|
||||||
|
totalsService: TotalsServiceMock,
|
||||||
|
shippingOptionService: ShippingOptionServiceMock,
|
||||||
|
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||||
|
})
|
||||||
const orderService = new OrderService({
|
const orderService = new OrderService({
|
||||||
orderModel: OrderModelMock,
|
orderModel: OrderModelMock,
|
||||||
|
returnService,
|
||||||
shippingOptionService: ShippingOptionServiceMock,
|
shippingOptionService: ShippingOptionServiceMock,
|
||||||
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||||
paymentProviderService: PaymentProviderServiceMock,
|
paymentProviderService: PaymentProviderServiceMock,
|
||||||
@@ -1110,9 +1129,15 @@ describe("OrderService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("createShipment", () => {
|
describe("createShipment", () => {
|
||||||
|
const fulfillmentService = new FulfillmentService({
|
||||||
|
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||||
|
shippingProfileService: ShippingProfileServiceMock,
|
||||||
|
totalsService: TotalsServiceMock,
|
||||||
|
})
|
||||||
const orderService = new OrderService({
|
const orderService = new OrderService({
|
||||||
orderModel: OrderModelMock,
|
orderModel: OrderModelMock,
|
||||||
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
fulfillmentProviderService: FulfillmentProviderServiceMock,
|
||||||
|
fulfillmentService,
|
||||||
eventBusService: EventBusServiceMock,
|
eventBusService: EventBusServiceMock,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1136,37 +1161,34 @@ describe("OrderService", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
"fulfillments.$": [
|
"fulfillments.$": {
|
||||||
{
|
_id: IdMap.getId("fulfillment"),
|
||||||
_id: IdMap.getId("fulfillment"),
|
provider_id: "default_provider",
|
||||||
provider_id: "default_provider",
|
tracking_numbers: ["1234", "2345"],
|
||||||
tracking_numbers: ["1234", "2345"],
|
data: {},
|
||||||
data: {},
|
items: [
|
||||||
items: [
|
{
|
||||||
{
|
_id: IdMap.getId("existingLine"),
|
||||||
_id: IdMap.getId("existingLine"),
|
content: {
|
||||||
content: {
|
product: {
|
||||||
product: {
|
_id: IdMap.getId("validId"),
|
||||||
_id: IdMap.getId("validId"),
|
},
|
||||||
},
|
quantity: 1,
|
||||||
quantity: 1,
|
unit_price: 123,
|
||||||
unit_price: 123,
|
variant: {
|
||||||
variant: {
|
_id: IdMap.getId("can-cover"),
|
||||||
_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",
|
|
||||||
},
|
},
|
||||||
],
|
description: "This is a new line",
|
||||||
shipped_at: expect.anything(),
|
fulfilled_quantity: 10,
|
||||||
metadata: {},
|
quantity: 10,
|
||||||
},
|
thumbnail: "test-img-yeah.com/thumb",
|
||||||
],
|
title: "merge line",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
shipped_at: expect.anything(),
|
||||||
|
metadata: {},
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
_id: IdMap.getId("existingLine"),
|
_id: IdMap.getId("existingLine"),
|
||||||
@@ -1181,6 +1203,7 @@ describe("OrderService", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: "This is a new line",
|
description: "This is a new line",
|
||||||
|
shipped: true,
|
||||||
fulfilled_quantity: 10,
|
fulfilled_quantity: 10,
|
||||||
shipped_quantity: 10,
|
shipped_quantity: 10,
|
||||||
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", () => {
|
describe("archive", () => {
|
||||||
const orderService = new OrderService({
|
const orderService = new OrderService({
|
||||||
orderModel: OrderModelMock,
|
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)
|
const region = await this.regionService_.retrieve(region_id)
|
||||||
if (region.countries.length === 1) {
|
if (!data.shipping_address) {
|
||||||
// Preselect the country if the region only has 1
|
if (region.countries.length === 1) {
|
||||||
data.shipping_address = {
|
// Preselect the country if the region only has 1
|
||||||
country_code: region.countries[0],
|
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)) {
|
if (Array.isArray(item.content)) {
|
||||||
item.content.forEach(c => products.push(`${c.product._id}`))
|
item.content.forEach(c => products.push(`${c.product._id}`))
|
||||||
} else {
|
} else {
|
||||||
products.push(`${item.content.product._id}`)
|
if (item.content.product) {
|
||||||
|
products.push(`${item.content.product._id}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return products
|
return products
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class CustomerService extends BaseService {
|
|||||||
PASSWORD_RESET: "customer.password_reset",
|
PASSWORD_RESET: "customer.password_reset",
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor({ customerModel, orderService, eventBusService }) {
|
constructor({ customerModel, eventBusService }) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
/** @private @const {CustomerModel} */
|
/** @private @const {CustomerModel} */
|
||||||
@@ -21,9 +21,6 @@ class CustomerService extends BaseService {
|
|||||||
|
|
||||||
/** @private @const {EventBus} */
|
/** @private @const {EventBus} */
|
||||||
this.eventBus_ = eventBusService
|
this.eventBus_ = eventBusService
|
||||||
|
|
||||||
/** @private @const {EventBus} */
|
|
||||||
this.orderService_ = orderService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -372,15 +369,6 @@ class CustomerService extends BaseService {
|
|||||||
const requiredFields = ["_id", "metadata"]
|
const requiredFields = ["_id", "metadata"]
|
||||||
const decorated = _.pick(customer, fields.concat(requiredFields))
|
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)
|
const final = await this.runDecorators_(decorated)
|
||||||
return final
|
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",
|
ITEMS_RETURNED: "order.items_returned",
|
||||||
RETURN_ACTION_REQUIRED: "order.return_action_required",
|
RETURN_ACTION_REQUIRED: "order.return_action_required",
|
||||||
REFUND_CREATED: "order.refund_created",
|
REFUND_CREATED: "order.refund_created",
|
||||||
|
SWAP_CREATED: "order.swap_created",
|
||||||
|
SWAP_RECEIVED: "order.swap_received",
|
||||||
PLACED: "order.placed",
|
PLACED: "order.placed",
|
||||||
UPDATED: "order.updated",
|
UPDATED: "order.updated",
|
||||||
CANCELED: "order.canceled",
|
CANCELED: "order.canceled",
|
||||||
@@ -22,22 +24,29 @@ class OrderService extends BaseService {
|
|||||||
constructor({
|
constructor({
|
||||||
orderModel,
|
orderModel,
|
||||||
counterService,
|
counterService,
|
||||||
|
customerService,
|
||||||
paymentProviderService,
|
paymentProviderService,
|
||||||
shippingOptionService,
|
shippingOptionService,
|
||||||
shippingProfileService,
|
shippingProfileService,
|
||||||
discountService,
|
discountService,
|
||||||
fulfillmentProviderService,
|
fulfillmentProviderService,
|
||||||
|
fulfillmentService,
|
||||||
lineItemService,
|
lineItemService,
|
||||||
totalsService,
|
totalsService,
|
||||||
regionService,
|
regionService,
|
||||||
|
returnService,
|
||||||
|
swapService,
|
||||||
documentService,
|
documentService,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
}) {
|
}) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
/** @private @constantant {OrderModel} */
|
/** @private @constant {OrderModel} */
|
||||||
this.orderModel_ = orderModel
|
this.orderModel_ = orderModel
|
||||||
|
|
||||||
|
/** @private @constant {CustomerService} */
|
||||||
|
this.customerService_ = customerService
|
||||||
|
|
||||||
/** @private @constantant {PaymentProviderService} */
|
/** @private @constantant {PaymentProviderService} */
|
||||||
this.paymentProviderService_ = paymentProviderService
|
this.paymentProviderService_ = paymentProviderService
|
||||||
|
|
||||||
@@ -56,6 +65,12 @@ class OrderService extends BaseService {
|
|||||||
/** @private @constant {RegionService} */
|
/** @private @constant {RegionService} */
|
||||||
this.regionService_ = regionService
|
this.regionService_ = regionService
|
||||||
|
|
||||||
|
/** @private @constant {ReturnService} */
|
||||||
|
this.returnService_ = returnService
|
||||||
|
|
||||||
|
/** @private @constant {FulfillmentService} */
|
||||||
|
this.fulfillmentService_ = fulfillmentService
|
||||||
|
|
||||||
/** @private @constant {DiscountService} */
|
/** @private @constant {DiscountService} */
|
||||||
this.discountService_ = discountService
|
this.discountService_ = discountService
|
||||||
|
|
||||||
@@ -70,6 +85,9 @@ class OrderService extends BaseService {
|
|||||||
|
|
||||||
/** @private @constant {ShippingOptionService} */
|
/** @private @constant {ShippingOptionService} */
|
||||||
this.shippingOptionService_ = shippingOptionService
|
this.shippingOptionService_ = shippingOptionService
|
||||||
|
|
||||||
|
/** @private @constant {SwapService} */
|
||||||
|
this.swapService_ = swapService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -126,33 +144,6 @@ class OrderService extends BaseService {
|
|||||||
return value
|
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
|
* @param {Object} selector - the query object for find
|
||||||
* @return {Promise} the result of the find operation
|
* @return {Promise} the result of the find operation
|
||||||
@@ -419,41 +410,49 @@ class OrderService extends BaseService {
|
|||||||
async createShipment(orderId, fulfillmentId, trackingNumbers, metadata = {}) {
|
async createShipment(orderId, fulfillmentId, trackingNumbers, metadata = {}) {
|
||||||
const order = await this.retrieve(orderId)
|
const order = await this.retrieve(orderId)
|
||||||
|
|
||||||
let shipment
|
const shipment = order.fulfillments.find(f => f._id.equals(fulfillmentId))
|
||||||
const updated = order.fulfillments.map(f => {
|
if (!shipment) {
|
||||||
if (f._id.equals(fulfillmentId)) {
|
throw new MedusaError(
|
||||||
// For each item in the shipment, we set their status to shipped
|
MedusaError.Types.NOT_FOUND,
|
||||||
f.items.map(item => {
|
"Could not find fulfillment"
|
||||||
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) {
|
const updated = await this.fulfillmentService_.createShipment(
|
||||||
item.shipped_quantity = item.quantity
|
order,
|
||||||
order.items[itemIdx].shipped_quantity =
|
shipment,
|
||||||
(order.items[itemIdx].shipped_quantity || 0) + item.quantity
|
trackingNumbers,
|
||||||
}
|
metadata
|
||||||
})
|
)
|
||||||
shipment = {
|
|
||||||
...f,
|
|
||||||
tracking_numbers: trackingNumbers,
|
|
||||||
shipped_at: Date.now(),
|
|
||||||
metadata: {
|
|
||||||
...f.metadata,
|
|
||||||
...metadata,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return shipment
|
|
||||||
}
|
|
||||||
return f
|
|
||||||
})
|
|
||||||
|
|
||||||
let fulfillmentStatus = "shipped"
|
let fulfillmentStatus = "shipped"
|
||||||
for (const item of order.items) {
|
|
||||||
if (item.quantity !== item.shipped_quantity) {
|
const newItems = order.items.map(item => {
|
||||||
fulfillmentStatus = "partially_shipped"
|
const shipped = updated.items.find(fi => item._id.equals(fi._id))
|
||||||
break
|
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
|
// Add the shipment to the order
|
||||||
return this.orderModel_
|
return this.orderModel_
|
||||||
@@ -462,7 +461,7 @@ class OrderService extends BaseService {
|
|||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
"fulfillments.$": updated,
|
"fulfillments.$": updated,
|
||||||
items: order.items,
|
items: newItems,
|
||||||
fulfillment_status: fulfillmentStatus,
|
fulfillment_status: fulfillmentStatus,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -739,85 +738,62 @@ class OrderService extends BaseService {
|
|||||||
async createFulfillment(orderId, itemsToFulfill, metadata = {}) {
|
async createFulfillment(orderId, itemsToFulfill, metadata = {}) {
|
||||||
const order = await this.retrieve(orderId)
|
const order = await this.retrieve(orderId)
|
||||||
|
|
||||||
const lineItems = await this.getFulfillmentItems_(
|
const fulfillments = await this.fulfillmentService_.createFulfillment(
|
||||||
order,
|
order,
|
||||||
itemsToFulfill,
|
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 = []
|
let successfullyFulfilled = []
|
||||||
const results = await Promise.all(
|
for (const f of fulfillments) {
|
||||||
fulfillments.map(async method => {
|
successfullyFulfilled = [...successfullyFulfilled, ...f.items]
|
||||||
const provider = this.fulfillmentProviderService_.retrieveProvider(
|
}
|
||||||
method.provider_id
|
|
||||||
)
|
|
||||||
|
|
||||||
const data = await provider
|
const updateFields = {}
|
||||||
.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,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reflect the fulfillments in the items
|
// Reflect the fulfillments in the items
|
||||||
updateFields.items = order.items.map(i => {
|
updateFields.items = order.items.map(i => {
|
||||||
const ful = successfullyFulfilled.find(f => i._id.equals(f._id))
|
const ful = successfullyFulfilled.find(f => i._id.equals(f._id))
|
||||||
if (ful) {
|
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 {
|
return {
|
||||||
...i,
|
...i,
|
||||||
fulfilled: i.quantity === ful.quantity,
|
fulfilled: i.quantity === fulfilledQuantity,
|
||||||
fulfilled_quantity: i.fulfilled_quantity + ful.quantity,
|
fulfilled_quantity: fulfilledQuantity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!i.fulfilled) {
|
||||||
|
updateFields.fulfillment_status = "partially_fulfilled"
|
||||||
|
}
|
||||||
|
|
||||||
return i
|
return i
|
||||||
})
|
})
|
||||||
|
|
||||||
updateFields.fulfillment_status = "fulfilled"
|
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_
|
return this.orderModel_
|
||||||
.updateOne(
|
.updateOne(
|
||||||
{
|
{
|
||||||
_id: orderId,
|
_id: orderId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$addToSet: { fulfillments: { $each: results } },
|
$addToSet: { fulfillments: { $each: fulfillments } },
|
||||||
$set: updateFields,
|
$set: updateFields,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
for (const fulfillment of results) {
|
for (const fulfillment of fulfillments) {
|
||||||
this.eventBus_.emit(OrderService.Events.FULFILLMENT_CREATED, {
|
this.eventBus_.emit(OrderService.Events.FULFILLMENT_CREATED, {
|
||||||
order_id: orderId,
|
order_id: orderId,
|
||||||
fulfillment,
|
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.
|
* Generates documents.
|
||||||
* @param {Array<Document>} docs - documents to generate
|
* @param {Array<Document>} docs - documents to generate
|
||||||
@@ -941,73 +891,13 @@ class OrderService extends BaseService {
|
|||||||
async requestReturn(orderId, items, shippingMethod, refundAmount) {
|
async requestReturn(orderId, items, shippingMethod, refundAmount) {
|
||||||
const order = await this.retrieve(orderId)
|
const order = await this.retrieve(orderId)
|
||||||
|
|
||||||
// Throws if the order doesn't have the necessary status for return
|
const returnRequest = await this.returnService_.requestReturn(
|
||||||
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_(
|
|
||||||
order,
|
order,
|
||||||
items,
|
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_
|
return this.orderModel_
|
||||||
.updateOne(
|
.updateOne(
|
||||||
{
|
{
|
||||||
@@ -1015,14 +905,14 @@ class OrderService extends BaseService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
$push: {
|
$push: {
|
||||||
returns: newReturn,
|
returns: returnRequest,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
this.eventBus_.emit(OrderService.Events.RETURN_REQUESTED, {
|
this.eventBus_.emit(OrderService.Events.RETURN_REQUESTED, {
|
||||||
order: result,
|
order: result,
|
||||||
return: newReturn,
|
return: returnRequest,
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
@@ -1040,7 +930,13 @@ class OrderService extends BaseService {
|
|||||||
* @param {string[]} lineItems - the line items to return
|
* @param {string[]} lineItems - the line items to return
|
||||||
* @return {Promise} the result of the update operation
|
* @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 order = await this.retrieve(orderId)
|
||||||
const returnRequest = order.returns.find(r => r._id.equals(returnId))
|
const returnRequest = order.returns.find(r => r._id.equals(returnId))
|
||||||
if (!returnRequest) {
|
if (!returnRequest) {
|
||||||
@@ -1050,99 +946,24 @@ class OrderService extends BaseService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (returnRequest.status === "received") {
|
const updatedReturn = await this.returnService_.receiveReturn(
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
|
||||||
`Return with id ${returnId} has already been received`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const returnLines = await this.getFulfillmentItems_(
|
|
||||||
order,
|
order,
|
||||||
|
returnRequest,
|
||||||
items,
|
items,
|
||||||
this.validateReturnLineItem_
|
refundAmount,
|
||||||
|
allowMismatch
|
||||||
)
|
)
|
||||||
|
|
||||||
const newLines = returnLines.map(l => {
|
if (updatedReturn.status === "requires_action") {
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return this.orderModel_
|
return this.orderModel_
|
||||||
.updateOne(
|
.updateOne(
|
||||||
{
|
{
|
||||||
_id: orderId,
|
_id: orderId,
|
||||||
|
"returns._id": updatedReturn._id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
returns: newReturns,
|
"returns.$": updatedReturn,
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.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,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1157,7 +978,7 @@ class OrderService extends BaseService {
|
|||||||
|
|
||||||
let isFullReturn = true
|
let isFullReturn = true
|
||||||
const newItems = order.items.map(i => {
|
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) {
|
if (isReturn) {
|
||||||
const returnedQuantity = i.returned_quantity + isReturn.quantity
|
const returnedQuantity = i.returned_quantity + isReturn.quantity
|
||||||
let returned = i.quantity === returnedQuantity
|
let returned = i.quantity === returnedQuantity
|
||||||
@@ -1177,18 +998,9 @@ class OrderService extends BaseService {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const newReturns = order.returns.map(r => {
|
const newReturns = order.returns.map(r =>
|
||||||
if (r._id.equals(returnId)) {
|
r._id.equals(returnId) ? updatedReturn : r
|
||||||
return {
|
)
|
||||||
...r,
|
|
||||||
status: "received",
|
|
||||||
items: newLines,
|
|
||||||
refund_amount: toRefund,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const update = {
|
const update = {
|
||||||
$set: {
|
$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 { provider_id, data } = order.payment_method
|
||||||
const paymentProvider = this.paymentProviderService_.retrieveProvider(
|
const paymentProvider = this.paymentProviderService_.retrieveProvider(
|
||||||
provider_id
|
provider_id
|
||||||
)
|
)
|
||||||
await paymentProvider.refundPayment(data, toRefund)
|
await paymentProvider.refundPayment(data, updatedReturn.refund_amount)
|
||||||
update.$push = {
|
update.$push = {
|
||||||
refunds: {
|
refunds: {
|
||||||
amount: toRefund,
|
amount: updatedReturn.refund_amount,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1214,7 +1026,7 @@ class OrderService extends BaseService {
|
|||||||
return this.orderModel_.updateOne({ _id: orderId }, update).then(result => {
|
return this.orderModel_.updateOne({ _id: orderId }, update).then(result => {
|
||||||
this.eventBus_.emit(OrderService.Events.ITEMS_RETURNED, {
|
this.eventBus_.emit(OrderService.Events.ITEMS_RETURNED, {
|
||||||
order: result,
|
order: result,
|
||||||
return: result.returns.find(r => r._id.equals(returnId)),
|
return: updatedReturn,
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
@@ -1375,6 +1187,22 @@ class OrderService extends BaseService {
|
|||||||
|
|
||||||
o.created = order._id.getTimestamp()
|
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")) {
|
if (expandFields.includes("region")) {
|
||||||
o.region = await this.regionService_.retrieve(order.region_id)
|
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.
|
* Dedicated method to delete metadata for an order.
|
||||||
* @param {string} orderId - the order to delete metadata from.
|
* @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 {
|
} else {
|
||||||
if (!acc.includes(next.content.product._id)) {
|
// We may have line items that are not associated with a product
|
||||||
acc.push(next.content.product._id)
|
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