diff --git a/packages/medusa-fulfillment-manual/src/services/manual-fulfillment.js b/packages/medusa-fulfillment-manual/src/services/manual-fulfillment.js index bfb27c7bab..35af698406 100644 --- a/packages/medusa-fulfillment-manual/src/services/manual-fulfillment.js +++ b/packages/medusa-fulfillment-manual/src/services/manual-fulfillment.js @@ -33,7 +33,7 @@ class ManualFulfillmentService extends FulfillmentService { createOrder() { // No data is being sent anywhere - return + return Promise.resolve({}) } } diff --git a/packages/medusa-payment-stripe/src/services/stripe-provider.js b/packages/medusa-payment-stripe/src/services/stripe-provider.js index 2070a5c885..fcabde2ebb 100644 --- a/packages/medusa-payment-stripe/src/services/stripe-provider.js +++ b/packages/medusa-payment-stripe/src/services/stripe-provider.js @@ -96,6 +96,8 @@ class StripeProviderService extends PaymentService { const { customer_id, region_id } = cart const { currency_code } = await this.regionService_.retrieve(region_id) + console.log(customer_id) + let stripeCustomerId if (!customer_id) { const { id } = await this.stripe_.customers.create({ @@ -104,7 +106,8 @@ class StripeProviderService extends PaymentService { stripeCustomerId = id } else { const customer = await this.customerService_.retrieve(customer_id) - if (!customer.metadata.stripe_id) { + console.log(customer) + if (!(customer.metadata && customer.metadata.stripe_id)) { const { id } = await this.stripe_.customers.create({ email: customer.email, }) @@ -151,7 +154,7 @@ class StripeProviderService extends PaymentService { const { id } = data const amount = this.totalsService_.getTotal(cart) return this.stripe_.paymentIntents.update(id, { - amount, + amount: amount * 100, }) } catch (error) { throw error @@ -212,7 +215,7 @@ class StripeProviderService extends PaymentService { const { id } = paymentData try { return this.stripe_.refunds.create({ - amount, + amount: amount * 100, payment_intent: id, }) } catch (error) { diff --git a/packages/medusa-plugin-brightpearl/src/api/index.js b/packages/medusa-plugin-brightpearl/src/api/index.js index c220e73099..118785a3bc 100644 --- a/packages/medusa-plugin-brightpearl/src/api/index.js +++ b/packages/medusa-plugin-brightpearl/src/api/index.js @@ -4,12 +4,27 @@ import bodyParser from "body-parser" export default (container) => { const app = Router() - app.post("/brightpearl/inventory-update", bodyParser.json(), async (req, res) => { - const { id } = req.body + app.post("/brightpearl/goods-out", bodyParser.json(), async (req, res) => { + const { id, lifecycle_event } = req.body const brightpearlService = req.scope.resolve("brightpearlService") - await brightpearlService.updateInventory(id) + + if (lifecycle_event === "created") { + await brightpearlService.createFulfillmentFromGoodsOut(id) + } + res.sendStatus(200) }) + app.post( + "/brightpearl/inventory-update", + bodyParser.json(), + async (req, res) => { + const { id } = req.body + const brightpearlService = req.scope.resolve("brightpearlService") + await brightpearlService.updateInventory(id) + res.sendStatus(200) + } + ) + return app } diff --git a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js index d5bc031e43..1a3aea51cb 100644 --- a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js @@ -41,24 +41,52 @@ class BrightpearlService extends BaseService { } const client = new Brightpearl({ + account: this.options.account, url: data.api_domain, auth_type: data.token_type, access_token: data.access_token, }) + this.authData_ = data this.brightpearlClient_ = client return client } + async getAuthData() { + if (this.authData_) { + return this.authData_ + } + + const { data } = await this.oauthService_.retrieveByName("brightpearl") + if (!data || !data.access_token) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "You must authenticate the Brightpearl app in settings before continuing" + ) + } + + this.authData_ = data + return data + } + async verifyWebhooks() { const brightpearl = await this.getClient() const hooks = [ + { + subscribeTo: "goods-out-note.created", + httpMethod: "POST", + uriTemplate: `${this.options.backend_url}/brightpearl/goods-out`, + bodyTemplate: + '{"account": "${account-code}", "lifecycle_event": "${lifecycle-event}", "resource_type": "${resource-type}", "id": "${resource-id}" }', + contentType: "application/json", + idSetAccepted: false, + }, { subscribeTo: "product.modified.on-hand-modified", httpMethod: "POST", uriTemplate: `${this.options.backend_url}/brightpearl/inventory-update`, bodyTemplate: - '{"account": "${account-code}", "lifecycleEvent": "${lifecycle-event}", "resourceType": "${resource-type}", "id": "${resource-id}" }', + '{"account": "${account-code}", "lifecycle_event": "${lifecycle-event}", "resource_type": "${resource-type}", "id": "${resource-id}" }', contentType: "application/json", idSetAccepted: false, }, @@ -107,18 +135,22 @@ class BrightpearlService extends BaseService { async updateInventory(productId) { const client = await this.getClient() - const brightpearlProduct = await client.products.retrieve(productId) - const availability = await client.products.retrieveAvailability(productId) + const availability = await client.products + .retrieveAvailability(productId) + .catch(() => null) - const onHand = availability[productId].total.onHand + if (availability) { + const brightpearlProduct = await client.products.retrieve(productId) + const onHand = availability[productId].total.onHand - const sku = brightpearlProduct.identity.sku - const [variant] = await this.productVariantService_.list({ sku }) + const sku = brightpearlProduct.identity.sku + const [variant] = await this.productVariantService_.list({ sku }) - if (variant && variant.manage_inventory) { - await this.productVariantService_.update(variant._id, { - inventory_quantity: onHand, - }) + if (variant && variant.manage_inventory) { + await this.productVariantService_.update(variant._id, { + inventory_quantity: onHand, + }) + } } } @@ -174,11 +206,158 @@ class BrightpearlService extends BaseService { return client.warehouses.updateGoodsOutNote(noteId, { priority: false, shipping: { - reference: shipment.tracking_number, + reference: shipment.tracking_numbers.join(", "), }, }) } + async createRefundCredit(fromOrder, fromRefund) { + 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 + if (orderId) { + let accountingCode = "4000" + if ( + fromRefund.reason === "discount" && + this.options.discount_account_code + ) { + accountingCode = this.options.discount_account_code + } + + const parentSo = await client.orders.retrieve(orderId) + const order = { + currency: parentSo.currency, + ref: parentSo.ref, + externalRef: `${parentSo.externalRef}.${fromOrder.refunds.length}`, + channelId: this.options.channel_id || `1`, + installedIntegrationInstanceId: authData.installation_instance_id, + customer: parentSo.customer, + delivery: parentSo.delivery, + parentId: orderId, + rows: [ + { + name: `${fromRefund.reason}: ${fromRefund.note}`, + quantity: 1, + taxCode: region.tax_code, + net: fromRefund.amount / (1 + fromOrder.tax_rate), + tax: + fromRefund.amount - fromRefund.amount / (1 + fromOrder.tax_rate), + nominalCode: accountingCode, + }, + ], + } + + return client.orders + .createCredit(order) + .then(async (creditId) => { + const paymentMethod = fromOrder.payment_method + const paymentType = "PAYMENT" + const payment = { + transactionRef: `${paymentMethod._id}.${fromOrder.refunds.length}`, + transactionCode: fromOrder._id, + paymentMethodCode: this.options.payment_method_code || "1220", + orderId: creditId, + currencyIsoCode: fromOrder.currency_code, + amountPaid: fromRefund.amount, + paymentDate: new Date(), + paymentType, + } + + const existing = fromOrder.metadata.brightpearl_credit_ids || [] + const newIds = [...existing, creditId] + + await client.payments.create(payment) + + return this.orderService_.setMetadata( + fromOrder._id, + "brightpearl_credit_ids", + newIds + ) + }) + .catch((err) => console.log(err.response.data.errors)) + } + } + + async createSalesCredit(fromOrder, fromReturn) { + 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 + if (orderId) { + const parentSo = await client.orders.retrieve(orderId) + const order = { + currency: parentSo.currency, + ref: parentSo.ref, + externalRef: `${parentSo.externalRef}.${fromOrder.refunds.length}`, + channelId: this.options.channel_id || `1`, + installedIntegrationInstanceId: authData.installation_instance_id, + customer: parentSo.customer, + delivery: parentSo.delivery, + parentId: orderId, + rows: fromReturn.items.map((i) => { + const parentRow = parentSo.rows.find((row) => { + return row.externalRef === i.item_id + }) + return { + net: (parentRow.net / parentRow.quantity) * i.quantity, + tax: (parentRow.tax / parentRow.quantity) * i.quantity, + productId: parentRow.productId, + taxCode: parentRow.taxCode, + externalRef: parentRow.externalRef, + nominalCode: parentRow.nominalCode, + quantity: i.quantity, + } + }), + } + + const total = order.rows.reduce((acc, next) => { + return acc + next.net + next.tax + }, 0) + + const difference = fromReturn.refund_amount - total + if (difference) { + order.rows.push({ + name: "Difference", + quantity: 1, + taxCode: region.tax_code, + net: difference / (1 + fromOrder.tax_rate), + tax: difference - difference / (1 + fromOrder.tax_rate), + nominalCode: this.options.sales_account_code || "4000", + }) + } + + return client.orders + .createCredit(order) + .then(async (creditId) => { + const paymentMethod = fromOrder.payment_method + const paymentType = "PAYMENT" + const payment = { + transactionRef: `${paymentMethod._id}.${fromOrder.refunds.length}`, + transactionCode: fromOrder._id, + paymentMethodCode: this.options.payment_method_code || "1220", + orderId: creditId, + currencyIsoCode: fromOrder.currency_code, + amountPaid: fromReturn.refund_amount, + paymentDate: new Date(), + paymentType, + } + + const existing = fromOrder.metadata.brightpearl_credit_ids || [] + const newIds = [...existing, creditId] + + await client.payments.create(payment) + + return this.orderService_.setMetadata( + fromOrder._id, + "brightpearl_credit_ids", + newIds + ) + }) + .catch((err) => console.log(err.response.data.errors)) + } + } + async createSalesOrder(fromOrder) { const client = await this.getClient() let customer = await this.retrieveCustomerByEmail(fromOrder.email) @@ -188,12 +367,17 @@ class BrightpearlService extends BaseService { customer = await this.createCustomer(fromOrder) } + const authData = await this.getAuthData() + const { shipping_address } = fromOrder const order = { currency: { code: fromOrder.currency_code, }, + ref: fromOrder._id, externalRef: fromOrder._id, + channelId: this.options.channel_id || `1`, + installedIntegrationInstanceId: authData.installation_instance_id, customer: { id: customer.contactId, address: { @@ -237,7 +421,7 @@ class BrightpearlService extends BaseService { const payment = { transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref transactionCode: fromOrder._id, - paymentMethodCode: "1220", + paymentMethodCode: this.options.payment_method_code || "1220", orderId: salesOrderId, currencyIsoCode: fromOrder.currency_code, paymentDate: new Date(), @@ -368,6 +552,35 @@ class BrightpearlService extends BaseService { }) } + async createFulfillmentFromGoodsOut(id) { + const client = await this.getClient() + + // Get goods out and associated order + const goodsOut = await client.warehouses.retrieveGoodsOutNote(id) + const order = await client.orders.retrieve(goodsOut.orderId) + + console.log(order) + + // Combine the line items that we are going to create a fulfillment for + const lines = Object.keys(goodsOut.orderRows) + .map((key) => { + const row = order.rows.find((r) => r.id == key) + if (row) { + return { + item_id: row.externalRef, + quantity: goodsOut.orderRows[key][0].quantity, + } + } + + return null + }) + .filter((i) => !!i) + + return this.orderService_.createFulfillment(order.ref, lines, { + goods_out_note: id, + }) + } + async createCustomer(fromOrder) { const client = await this.getClient() const address = await client.addresses.create({ diff --git a/packages/medusa-plugin-brightpearl/src/subscribers/order.js b/packages/medusa-plugin-brightpearl/src/subscribers/order.js index 4b9bd7856b..8c0045f311 100644 --- a/packages/medusa-plugin-brightpearl/src/subscribers/order.js +++ b/packages/medusa-plugin-brightpearl/src/subscribers/order.js @@ -3,29 +3,53 @@ class OrderSubscriber { this.orderService_ = orderService this.brightpearlService_ = brightpearlService + eventBusService.subscribe("order.refund_created", this.registerRefund) + + eventBusService.subscribe("order.items_returned", this.registerReturn) + eventBusService.subscribe("order.placed", this.sendToBrightpearl) - eventBusService.subscribe("order.payment_captured", this.registerCapturedPayment) + + eventBusService.subscribe( + "order.payment_captured", + this.registerCapturedPayment + ) + eventBusService.subscribe("order.shipment_created", this.registerShipment) } - sendToBrightpearl = order => { + sendToBrightpearl = (order) => { return this.brightpearlService_.createSalesOrder(order) } - registerCapturedPayment = order => { + registerCapturedPayment = (order) => { return this.brightpearlService_.createCapturedPayment(order) } registerShipment = async (data) => { const { order_id, shipment } = data - const order = await this.orderService_.retrieve(order_id) - const notes = await this.brightpearlService_.createGoodsOutNote(order, shipment) - if (notes.length) { - const noteId = notes[0] - await this.brightpearlService_.registerGoodsOutTrackingNumber(noteId, shipment) + const noteId = shipment.metadata.goods_out_note + if (noteId) { + await this.brightpearlService_.registerGoodsOutTrackingNumber( + noteId, + shipment + ) await this.brightpearlService_.registerGoodsOutShipped(noteId, shipment) } } + + registerReturn = (data) => { + const { order, return: fromReturn } = data + return this.brightpearlService_ + .createSalesCredit(order, fromReturn) + .catch((err) => console.log(err)) + } + + registerRefund = (data) => { + const { order, refund } = data + return this.brightpearlService_ + .createRefundCredit(order, refund) + .catch((err) => console.log(err)) + } } export default OrderSubscriber diff --git a/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js b/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js index 1c6702b9d9..de1e3e0c1d 100644 --- a/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js @@ -23,7 +23,7 @@ class BrightpearlClient { constructor(options) { this.client_ = axios.create({ - baseURL: `${options.url}/public-api/${options.account}`, + baseURL: `https://${options.url}/public-api/${options.account}`, headers: { "brightpearl-app-ref": "medusa-dev", "brightpearl-dev-ref": "sebrindom", @@ -97,6 +97,14 @@ class BrightpearlClient { }) .then(({ data }) => data.response) }, + retrieveGoodsOutNote: (id) => { + return this.client_ + .request({ + url: `/warehouse-service/order/*/goods-note/goods-out/${id}`, + method: "GET", + }) + .then(({ data }) => data.response && data.response[id]) + }, createGoodsOutNote: (orderId, data) => { return this.client_ .request({ @@ -160,6 +168,15 @@ class BrightpearlClient { }) .then(({ data }) => data.response) }, + createCredit: (salesCredit) => { + return this.client_ + .request({ + url: `/order-service/sales-credit`, + method: "POST", + data: salesCredit, + }) + .then(({ data }) => data.response) + }, } } diff --git a/packages/medusa-plugin-brightpearl/utils/brightpearl.js b/packages/medusa-plugin-brightpearl/utils/brightpearl.js index d55a57bc07..0d5d805bbb 100644 --- a/packages/medusa-plugin-brightpearl/utils/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/utils/brightpearl.js @@ -98,13 +98,22 @@ var BrightpearlClient = /*#__PURE__*/function () { return data.response; }); }, + retrieveGoodsOutNote: function retrieveGoodsOutNote(id) { + return _this.client_.request({ + url: "/warehouse-service/order/*/goods-note/goods-out/".concat(id), + method: "GET" + }).then(function (_ref5) { + var data = _ref5.data; + return data.response && data.response[id]; + }); + }, createGoodsOutNote: function createGoodsOutNote(orderId, data) { return _this.client_.request({ url: "/warehouse-service/order/".concat(orderId, "/goods-note/goods-out"), method: "POST", data: data - }).then(function (_ref5) { - var data = _ref5.data; + }).then(function (_ref6) { + var data = _ref6.data; return data.response; }); }, @@ -137,8 +146,8 @@ var BrightpearlClient = /*#__PURE__*/function () { data: { products: data } - }).then(function (_ref6) { - var data = _ref6.data; + }).then(function (_ref7) { + var data = _ref7.data; return data.response; }); } @@ -151,8 +160,8 @@ var BrightpearlClient = /*#__PURE__*/function () { return _this.client_.request({ url: "/order-service/sales-order/".concat(orderId), method: "GET" - }).then(function (_ref7) { - var data = _ref7.data; + }).then(function (_ref8) { + var data = _ref8.data; return data.response.length && data.response[0]; })["catch"](function (err) { return console.log(err); @@ -163,8 +172,18 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/order-service/sales-order", method: "POST", data: order - }).then(function (_ref8) { - var data = _ref8.data; + }).then(function (_ref9) { + var data = _ref9.data; + return data.response; + }); + }, + createCredit: function createCredit(salesCredit) { + return _this.client_.request({ + url: "/order-service/sales-credit", + method: "POST", + data: salesCredit + }).then(function (_ref10) { + var data = _ref10.data; return data.response; }); } @@ -178,8 +197,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/contact-service/postal-address", method: "POST", data: address - }).then(function (_ref9) { - var data = _ref9.data; + }).then(function (_ref11) { + var data = _ref11.data; return data.response; }); } @@ -191,24 +210,24 @@ var BrightpearlClient = /*#__PURE__*/function () { retrieveAvailability: function retrieveAvailability(productId) { return _this.client_.request({ url: "/warehouse-service/product-availability/".concat(productId) - }).then(function (_ref10) { - var data = _ref10.data; + }).then(function (_ref12) { + var data = _ref12.data; return data.response && data.response; }); }, retrieve: function retrieve(productId) { return _this.client_.request({ url: "/product-service/product/".concat(productId) - }).then(function (_ref11) { - var data = _ref11.data; + }).then(function (_ref13) { + var data = _ref13.data; return data.response && data.response[0]; }); }, retrieveBySKU: function retrieveBySKU(sku) { return _this.client_.request({ url: "/product-service/product-search?SKU=".concat(sku) - }).then(function (_ref12) { - var data = _ref12.data; + }).then(function (_ref14) { + var data = _ref14.data; return _this.buildSearchResults_(data.response); }); } @@ -220,8 +239,8 @@ var BrightpearlClient = /*#__PURE__*/function () { retrieveByEmail: function retrieveByEmail(email) { return _this.client_.request({ url: "/contact-service/contact-search?primaryEmail=".concat(email) - }).then(function (_ref13) { - var data = _ref13.data; + }).then(function (_ref15) { + var data = _ref15.data; return _this.buildSearchResults_(data.response); }); }, @@ -230,8 +249,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/contact-service/contact", method: "POST", data: customerData - }).then(function (_ref14) { - var data = _ref14.data; + }).then(function (_ref16) { + var data = _ref16.data; return data.response; }); } @@ -239,7 +258,7 @@ var BrightpearlClient = /*#__PURE__*/function () { }); this.client_ = _axios["default"].create({ - baseURL: "".concat(options.url, "/public-api/").concat(options.account), + baseURL: "https://".concat(options.url, "/public-api/").concat(options.account), headers: { "brightpearl-app-ref": "medusa-dev", "brightpearl-dev-ref": "sebrindom", diff --git a/packages/medusa/src/api/index.js b/packages/medusa/src/api/index.js index b710f21d44..625c2626ca 100644 --- a/packages/medusa/src/api/index.js +++ b/packages/medusa/src/api/index.js @@ -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) diff --git a/packages/medusa/src/api/routes/admin/orders/capture-payment.js b/packages/medusa/src/api/routes/admin/orders/capture-payment.js index 6a5baf5ad0..c96d337335 100644 --- a/packages/medusa/src/api/routes/admin/orders/capture-payment.js +++ b/packages/medusa/src/api/routes/admin/orders/capture-payment.js @@ -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 diff --git a/packages/medusa/src/api/routes/admin/orders/complete-order.js b/packages/medusa/src/api/routes/admin/orders/complete-order.js index c2ca4282a2..e274fc10a6 100644 --- a/packages/medusa/src/api/routes/admin/orders/complete-order.js +++ b/packages/medusa/src/api/routes/admin/orders/complete-order.js @@ -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) diff --git a/packages/medusa/src/api/routes/admin/orders/create-fulfillment.js b/packages/medusa/src/api/routes/admin/orders/create-fulfillment.js index 7fc6952a61..0ff94bba8c 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-fulfillment.js +++ b/packages/medusa/src/api/routes/admin/orders/create-fulfillment.js @@ -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 diff --git a/packages/medusa/src/api/routes/admin/orders/create-order.js b/packages/medusa/src/api/routes/admin/orders/create-order.js index cc0aaf5236..e6ff1b36df 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-order.js +++ b/packages/medusa/src/api/routes/admin/orders/create-order.js @@ -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) { diff --git a/packages/medusa/src/api/routes/admin/orders/index.js b/packages/medusa/src/api/routes/admin/orders/index.js index 7f71c698ab..d8a3a3b983 100644 --- a/packages/medusa/src/api/routes/admin/orders/index.js +++ b/packages/medusa/src/api/routes/admin/orders/index.js @@ -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) diff --git a/packages/medusa/src/api/routes/admin/orders/refund-payment.js b/packages/medusa/src/api/routes/admin/orders/refund-payment.js new file mode 100644 index 0000000000..549ecf3db2 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/orders/refund-payment.js @@ -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 + } +} diff --git a/packages/medusa/src/api/routes/admin/orders/return-order.js b/packages/medusa/src/api/routes/admin/orders/return-order.js index 46d35ec3b9..651dfaf24a 100644 --- a/packages/medusa/src/api/routes/admin/orders/return-order.js +++ b/packages/medusa/src/api/routes/admin/orders/return-order.js @@ -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) { diff --git a/packages/medusa/src/api/routes/admin/orders/update-order.js b/packages/medusa/src/api/routes/admin/orders/update-order.js index c92becb96f..c65223a11f 100644 --- a/packages/medusa/src/api/routes/admin/orders/update-order.js +++ b/packages/medusa/src/api/routes/admin/orders/update-order.js @@ -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) { diff --git a/packages/medusa/src/api/routes/store/carts/create-payment-sessions.js b/packages/medusa/src/api/routes/store/carts/create-payment-sessions.js index 21bab083ea..bc95e5c206 100644 --- a/packages/medusa/src/api/routes/store/carts/create-payment-sessions.js +++ b/packages/medusa/src/api/routes/store/carts/create-payment-sessions.js @@ -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 } } diff --git a/packages/medusa/src/models/__mocks__/order.js b/packages/medusa/src/models/__mocks__/order.js index 99ff8f1ea6..2e98cb3e1a 100644 --- a/packages/medusa/src/models/__mocks__/order.js +++ b/packages/medusa/src/models/__mocks__/order.js @@ -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", diff --git a/packages/medusa/src/models/order.js b/packages/medusa/src/models/order.js index 41190fcd10..4749413b07 100644 --- a/packages/medusa/src/models/order.js +++ b/packages/medusa/src/models/order.js @@ -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 }, diff --git a/packages/medusa/src/models/schemas/fulfillment.js b/packages/medusa/src/models/schemas/fulfillment.js new file mode 100644 index 0000000000..76a4a9368a --- /dev/null +++ b/packages/medusa/src/models/schemas/fulfillment.js @@ -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: {} }, +}) diff --git a/packages/medusa/src/models/schemas/line-item.js b/packages/medusa/src/models/schemas/line-item.js index e434eb1c83..230e21325c 100644 --- a/packages/medusa/src/models/schemas/line-item.js +++ b/packages/medusa/src/models/schemas/line-item.js @@ -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: {} }, }) diff --git a/packages/medusa/src/models/schemas/refund.js b/packages/medusa/src/models/schemas/refund.js new file mode 100644 index 0000000000..d75d2c9297 --- /dev/null +++ b/packages/medusa/src/models/schemas/refund.js @@ -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: {} }, +}) diff --git a/packages/medusa/src/models/schemas/return.js b/packages/medusa/src/models/schemas/return.js new file mode 100644 index 0000000000..3447298a07 --- /dev/null +++ b/packages/medusa/src/models/schemas/return.js @@ -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: {} }, +}) diff --git a/packages/medusa/src/services/__mocks__/fulfillment-provider.js b/packages/medusa/src/services/__mocks__/fulfillment-provider.js index 1704b3ad2b..c8e9efee20 100644 --- a/packages/medusa/src/services/__mocks__/fulfillment-provider.js +++ b/packages/medusa/src/services/__mocks__/fulfillment-provider.js @@ -17,7 +17,7 @@ export const DefaultProviderMock = { return Promise.resolve() }), createOrder: jest.fn().mockImplementation(data => { - return Promise.resolve() + return Promise.resolve(data) }), } diff --git a/packages/medusa/src/services/__mocks__/order.js b/packages/medusa/src/services/__mocks__/order.js index 428fe94f9b..ea1f122ccb 100644 --- a/packages/medusa/src/services/__mocks__/order.js +++ b/packages/medusa/src/services/__mocks__/order.js @@ -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) diff --git a/packages/medusa/src/services/__tests__/order.js b/packages/medusa/src/services/__tests__/order.js index 20af230dae..20f8047bca 100644 --- a/packages/medusa/src/services/__tests__/order.js +++ b/packages/medusa/src/services/__tests__/order.js @@ -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: [ { diff --git a/packages/medusa/src/services/__tests__/totals.js b/packages/medusa/src/services/__tests__/totals.js index f43f9e57cc..217e559e30 100644 --- a/packages/medusa/src/services/__tests__/totals.js +++ b/packages/medusa/src/services/__tests__/totals.js @@ -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) }) }) }) diff --git a/packages/medusa/src/services/order.js b/packages/medusa/src/services/order.js index f1c9b02037..686f8e18d4 100644 --- a/packages/medusa/src/services/order.js +++ b/packages/medusa/src/services/order.js @@ -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, + }), } }) diff --git a/packages/medusa/src/services/totals.js b/packages/medusa/src/services/totals.js index 976de7f863..0f836c1a79 100644 --- a/packages/medusa/src/services/totals.js +++ b/packages/medusa/src/services/totals.js @@ -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