Brightpearl integration sales flow
This commit is contained in:
@@ -7,14 +7,44 @@ import errorHandler from "./middlewares/error-handler"
|
||||
export default (container, config) => {
|
||||
const app = Router()
|
||||
|
||||
app.post("/create-shipment/:order_id", async (req, res) => {
|
||||
app.post("/create-shipment/:order_id/:fulfillment_id", async (req, res) => {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const eventBus = req.scope.resolve("eventBusService")
|
||||
const order = await orderService.retrieve(req.params.order_id)
|
||||
|
||||
await orderService.createShipment(order._id, {
|
||||
item_ids: order.items.map(({ _id }) => `${_id}`),
|
||||
tracking_number: "1234",
|
||||
await orderService.createShipment(order._id, req.params.fulfillment_id, [
|
||||
"1234",
|
||||
])
|
||||
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
app.post("/run-hook/:order_id/refund", async (req, res) => {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const eventBus = req.scope.resolve("eventBusService")
|
||||
const order = await orderService.retrieve(req.params.order_id)
|
||||
const returnToSend = order.refunds[order.refunds.length - 1]
|
||||
|
||||
eventBus.emit("order.refund_created", {
|
||||
order,
|
||||
refund: returnToSend,
|
||||
})
|
||||
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
app.post("/run-hook/:order_id/return", async (req, res) => {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const eventBus = req.scope.resolve("eventBusService")
|
||||
const order = await orderService.retrieve(req.params.order_id)
|
||||
const returnToSend = order.returns[0]
|
||||
|
||||
eventBus.emit("order.items_returned", {
|
||||
order,
|
||||
return: {
|
||||
...returnToSend,
|
||||
refund_amount: 1000,
|
||||
},
|
||||
})
|
||||
|
||||
res.sendStatus(200)
|
||||
|
||||
@@ -3,7 +3,8 @@ export default async (req, res) => {
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const order = await orderService.capturePayment(id)
|
||||
let order = await orderService.capturePayment(id)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
res.json({ order })
|
||||
} catch (error) {
|
||||
throw error
|
||||
|
||||
@@ -3,7 +3,8 @@ export default async (req, res) => {
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const order = await orderService.completeOrder(id)
|
||||
let order = await orderService.completeOrder(id)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
res.json({ order })
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
|
||||
@@ -3,7 +3,8 @@ export default async (req, res) => {
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const order = await orderService.createFulfillment(id)
|
||||
let order = await orderService.createFulfillment(id)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
res.json({ order })
|
||||
} catch (error) {
|
||||
throw error
|
||||
|
||||
@@ -37,7 +37,8 @@ export default async (req, res) => {
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const order = await orderService.create(value)
|
||||
let order = await orderService.create(value)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
|
||||
res.status(200).json({ order })
|
||||
} catch (err) {
|
||||
|
||||
@@ -16,6 +16,10 @@ export default app => {
|
||||
middlewares.wrap(require("./complete-order").default)
|
||||
)
|
||||
|
||||
route.post(
|
||||
"/:id/refund",
|
||||
middlewares.wrap(require("./refund-payment").default)
|
||||
)
|
||||
route.post(
|
||||
"/:id/capture",
|
||||
middlewares.wrap(require("./capture-payment").default)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
const schema = Validator.object().keys({
|
||||
amount: Validator.number().required(),
|
||||
reason: Validator.string().required(),
|
||||
note: Validator.string()
|
||||
.allow("")
|
||||
.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")
|
||||
let order = await orderService.createRefund(
|
||||
id,
|
||||
value.amount,
|
||||
value.reason,
|
||||
value.note
|
||||
)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
|
||||
res.status(200).json({ order })
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,8 @@ export default async (req, res) => {
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const order = await orderService.return(id, value.items)
|
||||
let order = await orderService.return(id, value.items)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
|
||||
res.status(200).json({ order })
|
||||
} catch (err) {
|
||||
|
||||
@@ -31,7 +31,8 @@ export default async (req, res) => {
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const order = await orderService.update(id, value)
|
||||
let order = await orderService.update(id, value)
|
||||
order = await orderService.decorate(order, [], ["region"])
|
||||
|
||||
res.status(200).json({ order })
|
||||
} catch (err) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export default async (req, res) => {
|
||||
|
||||
res.status(200).json({ cart })
|
||||
} catch (err) {
|
||||
console.log(err.response.data)
|
||||
console.log(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,13 +51,9 @@ export const orders = {
|
||||
price: 100,
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("default"),
|
||||
},
|
||||
{
|
||||
_id: IdMap.getId("freeShipping"),
|
||||
name: "Free Shipping",
|
||||
price: 10,
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("profile1"),
|
||||
data: {
|
||||
extra: "hi",
|
||||
},
|
||||
},
|
||||
],
|
||||
fulfillment_status: "not_fulfilled",
|
||||
|
||||
@@ -7,6 +7,9 @@ import ShippingMethodSchema from "./schemas/shipping-method"
|
||||
import AddressSchema from "./schemas/address"
|
||||
import DiscountSchema from "./schemas/discount"
|
||||
import ShipmentSchema from "./schemas/shipment"
|
||||
import ReturnSchema from "./schemas/return"
|
||||
import RefundSchema from "./schemas/refund"
|
||||
import FulfillmentSchema from "./schemas/fulfillment"
|
||||
|
||||
class OrderModel extends BaseModel {
|
||||
static modelName = "Order"
|
||||
@@ -26,6 +29,9 @@ class OrderModel extends BaseModel {
|
||||
currency_code: { type: String, required: true },
|
||||
tax_rate: { type: Number, required: true },
|
||||
shipments: { type: [ShipmentSchema], default: [] },
|
||||
fulfillments: { type: [FulfillmentSchema], default: [] },
|
||||
returns: { type: [ReturnSchema], default: [] },
|
||||
refunds: { type: [RefundSchema], default: [] },
|
||||
region_id: { type: String, required: true },
|
||||
discounts: { type: [DiscountSchema], default: [] },
|
||||
customer_id: { type: String },
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import mongoose from "mongoose"
|
||||
|
||||
export default new mongoose.Schema({
|
||||
created: { type: String, default: Date.now },
|
||||
provider_id: { type: String, required: true },
|
||||
items: { type: [mongoose.Schema.Types.Mixed], required: true },
|
||||
data: { type: [mongoose.Schema.Types.Mixed], default: {} },
|
||||
tracking_numbers: { type: [String], default: [] },
|
||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
})
|
||||
@@ -36,6 +36,8 @@ export default new mongoose.Schema({
|
||||
content: { type: mongoose.Schema.Types.Mixed, required: true },
|
||||
quantity: { type: Number, required: true },
|
||||
returned: { type: Boolean, default: false },
|
||||
fulfilled: { type: Boolean, default: false },
|
||||
fulfilled_quantity: { type: Number, default: 0 },
|
||||
returned_quantity: { type: Number, default: 0 },
|
||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
})
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import mongoose from "mongoose"
|
||||
|
||||
export default new mongoose.Schema({
|
||||
created: { type: String, default: Date.now },
|
||||
note: { type: String, default: "" },
|
||||
reason: { type: String, default: "" },
|
||||
amount: { type: Number, required: true },
|
||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
import mongoose from "mongoose"
|
||||
|
||||
export default new mongoose.Schema({
|
||||
created: { type: String, default: Date.now },
|
||||
refund_amount: { type: Number, required: true },
|
||||
items: { type: [mongoose.Schema.Types.Mixed], required: true },
|
||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
})
|
||||
@@ -17,7 +17,7 @@ export const DefaultProviderMock = {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
createOrder: jest.fn().mockImplementation(data => {
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(data)
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,15 @@ export const OrderServiceMock = {
|
||||
createFromCart: jest.fn().mockImplementation(data => {
|
||||
return Promise.resolve(orders.testOrder)
|
||||
}),
|
||||
update: jest.fn().mockImplementation(data => Promise.resolve()),
|
||||
update: jest.fn().mockImplementation(data => {
|
||||
if (data === IdMap.getId("test-order")) {
|
||||
return Promise.resolve(orders.testOrder)
|
||||
}
|
||||
if (data === IdMap.getId("processed-order")) {
|
||||
return Promise.resolve(orders.processedOrder)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation(orderId => {
|
||||
if (orderId === IdMap.getId("test-order")) {
|
||||
return Promise.resolve(orders.testOrder)
|
||||
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
DefaultProviderMock,
|
||||
} from "../__mocks__/payment-provider"
|
||||
import { DiscountServiceMock } from "../__mocks__/discount"
|
||||
import { FulfillmentProviderServiceMock } from "../__mocks__/fulfillment-provider"
|
||||
import {
|
||||
FulfillmentProviderServiceMock,
|
||||
DefaultProviderMock as FulfillmentProviderMock,
|
||||
} from "../__mocks__/fulfillment-provider"
|
||||
import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile"
|
||||
import { TotalsServiceMock } from "../__mocks__/totals"
|
||||
import { RegionServiceMock } from "../__mocks__/region"
|
||||
@@ -402,32 +405,97 @@ describe("OrderService", () => {
|
||||
})
|
||||
|
||||
it("calls order model functions", async () => {
|
||||
await orderService.createFulfillment(IdMap.getId("test-order"))
|
||||
await orderService.createFulfillment(IdMap.getId("test-order"), [
|
||||
{
|
||||
item_id: IdMap.getId("existingLine"),
|
||||
quantity: 10,
|
||||
},
|
||||
])
|
||||
|
||||
expect(FulfillmentProviderMock.createOrder).toHaveBeenCalledTimes(1)
|
||||
expect(FulfillmentProviderMock.createOrder).toHaveBeenCalledWith(
|
||||
{
|
||||
extra: "hi",
|
||||
},
|
||||
[
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("validId"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("test-order") },
|
||||
{
|
||||
$push: {
|
||||
fulfillments: {
|
||||
$each: [
|
||||
{
|
||||
data: {
|
||||
extra: "hi",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("validId"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
provider_id: "default_provider",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
$set: {
|
||||
fulfillment_status: "fulfilled",
|
||||
shipping_methods: [
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("expensiveShipping"),
|
||||
items: [],
|
||||
name: "Expensive Shipping",
|
||||
price: 100,
|
||||
profile_id: IdMap.getId("default"),
|
||||
provider_id: "default_provider",
|
||||
},
|
||||
{
|
||||
_id: IdMap.getId("freeShipping"),
|
||||
items: [],
|
||||
name: "Free Shipping",
|
||||
price: 10,
|
||||
profile_id: IdMap.getId("profile1"),
|
||||
provider_id: "default_provider",
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 123,
|
||||
variant: {
|
||||
_id: IdMap.getId("can-cover"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("validId"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
fulfilled_quantity: 10,
|
||||
fulfilled: true,
|
||||
},
|
||||
],
|
||||
fulfillment_status: "fulfilled",
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -435,7 +503,12 @@ describe("OrderService", () => {
|
||||
|
||||
it("throws if payment is already processed", async () => {
|
||||
await expect(
|
||||
orderService.createFulfillment(IdMap.getId("fulfilled-order"))
|
||||
orderService.createFulfillment(IdMap.getId("fulfilled-order"), [
|
||||
{
|
||||
item_id: IdMap.getId("existingLine"),
|
||||
quantity: 10,
|
||||
},
|
||||
])
|
||||
).rejects.toThrow("Order is already fulfilled")
|
||||
})
|
||||
})
|
||||
@@ -464,6 +537,20 @@ describe("OrderService", () => {
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("processed-order") },
|
||||
{
|
||||
$push: {
|
||||
refunds: {
|
||||
amount: 1230,
|
||||
},
|
||||
returns: {
|
||||
items: [
|
||||
{
|
||||
item_id: IdMap.getId("existingLine"),
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
refund_amount: 1230,
|
||||
},
|
||||
},
|
||||
$set: {
|
||||
items: [
|
||||
{
|
||||
@@ -514,6 +601,20 @@ describe("OrderService", () => {
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("processed-order") },
|
||||
{
|
||||
$push: {
|
||||
refunds: {
|
||||
amount: 102,
|
||||
},
|
||||
returns: {
|
||||
items: [
|
||||
{
|
||||
item_id: IdMap.getId("existingLine"),
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
refund_amount: 102,
|
||||
},
|
||||
},
|
||||
$set: {
|
||||
items: [
|
||||
{
|
||||
@@ -560,6 +661,20 @@ describe("OrderService", () => {
|
||||
expect(OrderModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("order-refund") },
|
||||
{
|
||||
$push: {
|
||||
refunds: {
|
||||
amount: 0,
|
||||
},
|
||||
returns: {
|
||||
items: [
|
||||
{
|
||||
item_id: IdMap.getId("existingLine"),
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
refund_amount: 0,
|
||||
},
|
||||
},
|
||||
$set: {
|
||||
items: [
|
||||
{
|
||||
|
||||
@@ -323,7 +323,7 @@ describe("TotalsService", () => {
|
||||
it("calculates shipping", async () => {
|
||||
res = await totalsService.getShippingTotal(orders.testOrder)
|
||||
|
||||
expect(res).toEqual(110)
|
||||
expect(res).toEqual(100)
|
||||
})
|
||||
})
|
||||
describe("getTaxTotal", () => {
|
||||
@@ -341,7 +341,7 @@ describe("TotalsService", () => {
|
||||
it("calculates tax", async () => {
|
||||
res = await totalsService.getTaxTotal(orders.testOrder)
|
||||
|
||||
expect(res).toEqual(335)
|
||||
expect(res).toEqual(332.5)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -358,8 +358,7 @@ describe("TotalsService", () => {
|
||||
|
||||
it("calculates total", async () => {
|
||||
res = await totalsService.getTotal(orders.testOrder)
|
||||
|
||||
expect(res).toEqual(1230 + 335 + 110)
|
||||
expect(res).toEqual(1230 + 332.5 + 100)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ class OrderService extends BaseService {
|
||||
PAYMENT_CAPTURED: "order.payment_captured",
|
||||
SHIPMENT_CREATED: "order.shipment_created",
|
||||
ITEMS_RETURNED: "order.items_returned",
|
||||
REFUND_CREATED: "order.refund_created",
|
||||
PLACED: "order.placed",
|
||||
UPDATED: "order.updated",
|
||||
CANCELLED: "order.cancelled",
|
||||
@@ -373,53 +374,39 @@ class OrderService extends BaseService {
|
||||
/**
|
||||
* Adds a shipment to the order to indicate that an order has left the warehouse
|
||||
*/
|
||||
async createShipment(orderId, shipment) {
|
||||
async createShipment(orderId, fulfillmentId, trackingNumbers, metadata = {}) {
|
||||
const order = await this.retrieve(orderId)
|
||||
|
||||
console.log(order)
|
||||
if (order.fulfillment_status === "shipped") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Order has already been shipped"
|
||||
)
|
||||
}
|
||||
|
||||
const shipmentSchema = Validator.object({
|
||||
item_ids: Validator.array()
|
||||
.items(Validator.string())
|
||||
.required(),
|
||||
tracking_number: Validator.string().required(),
|
||||
let shipment
|
||||
const updated = order.fulfillments.map(f => {
|
||||
if (f._id.equals(fulfillmentId)) {
|
||||
shipment = {
|
||||
...f,
|
||||
tracking_numbers: trackingNumbers,
|
||||
metadata: {
|
||||
...f.metadata,
|
||||
...metadata,
|
||||
},
|
||||
}
|
||||
}
|
||||
return f
|
||||
})
|
||||
|
||||
const { value, error } = shipmentSchema.validate(shipment)
|
||||
if (error) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Shipment not valid: ${error}`
|
||||
)
|
||||
}
|
||||
|
||||
const existing = order.shipments || []
|
||||
const shipments = [...existing, value]
|
||||
const allCovered = order.items.every(
|
||||
i => !!shipments.find(s => s.item_ids.includes(`${i._id}`))
|
||||
)
|
||||
|
||||
const update = {
|
||||
$push: { shipments: value },
|
||||
$set: {
|
||||
fulfillment_status: allCovered ? "shipped" : "partially_shipped",
|
||||
},
|
||||
}
|
||||
|
||||
// Add the shipment to the order
|
||||
return this.orderModel_.updateOne({ _id: orderId }, update).then(result => {
|
||||
this.eventBus_.emit(OrderService.Events.SHIPMENT_CREATED, {
|
||||
order_id: orderId,
|
||||
shipment,
|
||||
return this.orderModel_
|
||||
.updateOne(
|
||||
{ _id: orderId },
|
||||
{
|
||||
$set: { fulfillments: updated },
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
this.eventBus_.emit(OrderService.Events.SHIPMENT_CREATED, {
|
||||
order_id: orderId,
|
||||
shipment,
|
||||
})
|
||||
return result
|
||||
})
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -614,9 +601,33 @@ class OrderService extends BaseService {
|
||||
* @param {string} orderId - id of order to cancel.
|
||||
* @return {Promise} result of the update operation.
|
||||
*/
|
||||
async createFulfillment(orderId) {
|
||||
async createFulfillment(orderId, itemsToFulfill, metadata = {}) {
|
||||
const order = await this.retrieve(orderId)
|
||||
|
||||
const lineItems = itemsToFulfill
|
||||
.map(({ item_id, quantity }) => {
|
||||
const item = order.items.find(i => i._id.equals(item_id))
|
||||
|
||||
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,
|
||||
}
|
||||
})
|
||||
.filter(i => !!i)
|
||||
|
||||
if (order.fulfillment_status !== "not_fulfilled") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
@@ -624,7 +635,7 @@ class OrderService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
const { shipping_methods, items } = order
|
||||
const { shipping_methods } = order
|
||||
|
||||
// prepare update object
|
||||
const updateFields = { fulfillment_status: "fulfilled" }
|
||||
@@ -634,26 +645,55 @@ class OrderService extends BaseService {
|
||||
}
|
||||
|
||||
// partition order items to their dedicated shipping method
|
||||
updateFields.shipping_methods = await this.partitionItems_(
|
||||
shipping_methods,
|
||||
items
|
||||
)
|
||||
const fulfillments = await this.partitionItems_(shipping_methods, lineItems)
|
||||
|
||||
await Promise.all(
|
||||
updateFields.shipping_methods.map(method => {
|
||||
let successfullyFulfilled = []
|
||||
const results = await Promise.all(
|
||||
fulfillments.map(async method => {
|
||||
const provider = this.fulfillmentProviderService_.retrieveProvider(
|
||||
method.provider_id
|
||||
)
|
||||
return provider.createOrder(method.data, method.items)
|
||||
|
||||
const data = await provider
|
||||
.createOrder(method.data, method.items)
|
||||
.then(res => {
|
||||
successfullyFulfilled = [...successfullyFulfilled, ...method.items]
|
||||
return res
|
||||
})
|
||||
|
||||
return {
|
||||
provider_id: method.provider_id,
|
||||
items: method.items,
|
||||
data,
|
||||
metadata,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Reflect the fulfillments in the items
|
||||
updateFields.items = order.items.map(i => {
|
||||
const ful = successfullyFulfilled.find(f => i._id.equals(f._id))
|
||||
if (ful) {
|
||||
if (i.quantity === ful.quantity) {
|
||||
i.fulfilled = true
|
||||
}
|
||||
|
||||
return {
|
||||
...i,
|
||||
fulfilled_quantity: ful.quantity,
|
||||
}
|
||||
}
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
return this.orderModel_
|
||||
.updateOne(
|
||||
{
|
||||
_id: orderId,
|
||||
},
|
||||
{
|
||||
$push: { fulfillments: { $each: results } },
|
||||
$set: updateFields,
|
||||
}
|
||||
)
|
||||
@@ -676,6 +716,16 @@ class OrderService extends BaseService {
|
||||
async return(orderId, lineItems, refundAmount) {
|
||||
const order = await this.retrieve(orderId)
|
||||
|
||||
const total = await this.totalsService_.getTotal(order)
|
||||
const refunded = await this.totalsService_.getRefundedTotal(order)
|
||||
|
||||
if (refundAmount > total - refunded) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Cannot refund more than the original payment"
|
||||
)
|
||||
}
|
||||
|
||||
// Find the lines to return
|
||||
const returnLines = lineItems.map(({ item_id, quantity }) => {
|
||||
const item = order.items.find(i => i._id.equals(item_id))
|
||||
@@ -686,6 +736,14 @@ class OrderService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -722,17 +780,22 @@ class OrderService extends BaseService {
|
||||
const newItems = order.items.map(i => {
|
||||
const isReturn = returnLines.find(r => r._id.equals(i._id))
|
||||
if (isReturn) {
|
||||
const returnedQuantity = i.returned_quantity + isReturn.quantity
|
||||
let returned = false
|
||||
if (i.quantity === isReturn.quantity) {
|
||||
if (i.quantity === returnedQuantity) {
|
||||
returned = true
|
||||
} else {
|
||||
isFullReturn = false
|
||||
}
|
||||
return {
|
||||
...i,
|
||||
returned_quantity: isReturn.quantity,
|
||||
returned,
|
||||
returned_quantity: returnedQuantity
|
||||
returned
|
||||
}
|
||||
} else {
|
||||
isFullReturn = false
|
||||
if (!i.returned) {
|
||||
isFullReturn = false
|
||||
}
|
||||
return i
|
||||
}
|
||||
})
|
||||
@@ -743,6 +806,15 @@ class OrderService extends BaseService {
|
||||
_id: orderId,
|
||||
},
|
||||
{
|
||||
$push: {
|
||||
refunds: {
|
||||
amount,
|
||||
},
|
||||
returns: {
|
||||
items: lineItems,
|
||||
refund_amount: amount,
|
||||
},
|
||||
},
|
||||
$set: {
|
||||
items: newItems,
|
||||
fulfillment_status: isFullReturn
|
||||
@@ -754,7 +826,10 @@ class OrderService extends BaseService {
|
||||
.then(result => {
|
||||
this.eventBus_.emit(OrderService.Events.ITEMS_RETURNED, {
|
||||
order: result,
|
||||
items: returnLines,
|
||||
return: {
|
||||
items: lineItems,
|
||||
refund_amount: amount,
|
||||
},
|
||||
})
|
||||
return result
|
||||
})
|
||||
@@ -786,6 +861,56 @@ class OrderService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Refunds a given amount back to the customer.
|
||||
*/
|
||||
async createRefund(orderId, refundAmount, reason, note) {
|
||||
const order = await this.retrieve(orderId)
|
||||
const total = await this.totalsService_.getTotal(order)
|
||||
const refunded = await this.totalsService_.getRefundedTotal(order)
|
||||
|
||||
if (refundAmount > total - refunded) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Cannot refund more than original order amount"
|
||||
)
|
||||
}
|
||||
|
||||
const { provider_id, data } = order.payment_method
|
||||
const paymentProvider = this.paymentProviderService_.retrieveProvider(
|
||||
provider_id
|
||||
)
|
||||
|
||||
await paymentProvider.refundPayment(data, refundAmount)
|
||||
|
||||
return this.orderModel_
|
||||
.updateOne(
|
||||
{
|
||||
_id: orderId,
|
||||
},
|
||||
{
|
||||
$push: {
|
||||
refunds: {
|
||||
amount: refundAmount,
|
||||
reason,
|
||||
note,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
this.eventBus_.emit(OrderService.Events.REFUND_CREATED, {
|
||||
order: result,
|
||||
refund: {
|
||||
amount: refundAmount,
|
||||
reason,
|
||||
note,
|
||||
},
|
||||
})
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorates an order.
|
||||
* @param {Order} order - the order to decorate.
|
||||
@@ -800,6 +925,8 @@ class OrderService extends BaseService {
|
||||
o.tax_total = await this.totalsService_.getTaxTotal(order)
|
||||
o.subtotal = await this.totalsService_.getSubtotal(order)
|
||||
o.total = await this.totalsService_.getTotal(order)
|
||||
o.refunded_total = await this.totalsService_.getRefundedTotal(order)
|
||||
o.refundable_amount = o.total - o.refunded_total
|
||||
o.created = order._id.getTimestamp()
|
||||
if (expandFields.includes("region")) {
|
||||
o.region = await this.regionService_.retrieve(order.region_id)
|
||||
@@ -808,7 +935,10 @@ class OrderService extends BaseService {
|
||||
o.items = o.items.map(i => {
|
||||
return {
|
||||
...i,
|
||||
refundable: this.totalsService_.getLineItemRefund(o, i),
|
||||
refundable: this.totalsService_.getLineItemRefund(o, {
|
||||
...i,
|
||||
quantity: i.quantity - i.returned_quantity,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -80,6 +80,11 @@ class TotalsService extends BaseService {
|
||||
return (subtotal - discountTotal + shippingTotal) * tax_rate
|
||||
}
|
||||
|
||||
getRefundedTotal(object) {
|
||||
const total = object.refunds.reduce((acc, next) => acc + next.amount, 0)
|
||||
return total
|
||||
}
|
||||
|
||||
getLineItemRefund(order, lineItem) {
|
||||
const { tax_rate, discounts } = order
|
||||
const taxRate = tax_rate || 0
|
||||
|
||||
Reference in New Issue
Block a user