From 630bf3abc666ed880056ec5550d6a06d0bec3af5 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Wed, 29 Jul 2020 11:51:59 +0200 Subject: [PATCH 1/5] Adds gift card support --- .../src/api/routes/hooks/shipping.js | 19 +- .../src/services/klarna-provider.js | 56 ++++-- .../src/services/sendgrid.js | 17 +- .../src/subscribers/order.js | 4 + packages/medusa/package.json | 3 +- .../routes/admin/products/create-product.js | 3 +- .../routes/admin/products/create-variant.js | 1 + .../api/routes/admin/products/get-product.js | 1 + .../routes/admin/products/list-products.js | 1 + .../routes/admin/products/update-product.js | 7 + .../routes/admin/products/update-variant.js | 1 + .../create-shipping-option.js | 13 +- packages/medusa/src/models/__mocks__/cart.js | 89 +++++++++ .../medusa/src/models/__mocks__/discount.js | 2 +- .../src/models/__mocks__/shipping-profile.js | 12 +- .../medusa/src/models/schemas/discount.js | 1 + .../medusa/src/models/schemas/line-item.js | 1 + .../medusa/src/services/__mocks__/discount.js | 5 + .../src/services/__mocks__/shipping-option.js | 1 + .../services/__mocks__/shipping-profile.js | 2 +- .../medusa/src/services/__tests__/cart.js | 52 ++++- .../medusa/src/services/__tests__/discount.js | 29 +++ .../medusa/src/services/__tests__/order.js | 119 ++++++++++- .../services/__tests__/shipping-profile.js | 31 +++ packages/medusa/src/services/cart.js | 188 +++++++++++++----- packages/medusa/src/services/discount.js | 41 +++- packages/medusa/src/services/line-item.js | 1 + packages/medusa/src/services/order.js | 45 ++++- packages/medusa/src/services/product.js | 9 +- .../medusa/src/services/shipping-profile.js | 16 +- packages/medusa/src/services/totals.js | 25 +-- packages/medusa/src/subscribers/order.js | 36 ++++ packages/medusa/yarn.lock | 19 ++ 33 files changed, 722 insertions(+), 128 deletions(-) diff --git a/packages/medusa-payment-klarna/src/api/routes/hooks/shipping.js b/packages/medusa-payment-klarna/src/api/routes/hooks/shipping.js index 6e91ab6422..633c8390d2 100644 --- a/packages/medusa-payment-klarna/src/api/routes/hooks/shipping.js +++ b/packages/medusa-payment-klarna/src/api/routes/hooks/shipping.js @@ -10,15 +10,18 @@ export default async (req, res) => { const cart = await cartService.retrieve(merchant_data) const shippingOptions = await shippingProfileService.fetchCartOptions(cart) - const option = shippingOptions.find(({ _id }) => _id.equals(selected_shipping_option.id)) + const ids = selected_shipping_option.id.split(".") + await Promise.all(ids.map(async id => { + const option = shippingOptions.find(({ _id }) => _id.equals(id)) - if (option) { - const newCart = await cartService.addShippingMethod(cart._id, option._id, option.data) - const order = await klarnaProviderService.cartToKlarnaOrder(newCart) - res.json(order) - } else { - res.sendStatus(400) - } + if (option) { + await cartService.addShippingMethod(cart._id, option._id, option.data) + } + })) + + const newCart = await cartService.retrieve(cart._id) + const order = await klarnaProviderService.cartToKlarnaOrder(newCart) + res.json(order) } catch (error) { throw error } diff --git a/packages/medusa-payment-klarna/src/services/klarna-provider.js b/packages/medusa-payment-klarna/src/services/klarna-provider.js index bb1aa4a44e..546da2a2e3 100644 --- a/packages/medusa-payment-klarna/src/services/klarna-provider.js +++ b/packages/medusa-payment-klarna/src/services/klarna-provider.js @@ -26,7 +26,7 @@ class KlarnaProviderService extends PaymentService { this.klarnaOrderManagementUrl_ = "/ordermanagement/v1/orders" this.backendUrl_ = - process.env.BACKEND_URL || "https://7e9a5bc2a2eb.ngrok.io" + process.env.BACKEND_URL || "https://c8e1abe7d8b3.ngrok.io" this.totalsService_ = totalsService @@ -73,10 +73,14 @@ class KlarnaProviderService extends PaymentService { }) if (cart.shipping_methods.length) { - const shippingMethod = cart.shipping_methods[0] - const price = shippingMethod.price + const { name, price } = cart.shipping_methods.reduce((acc, next) => { + acc.name = [...acc.name, next.name] + acc.price += next.price + return acc + }, { name: [], price: 0 }) + order_lines.push({ - name: `${shippingMethod.name}`, + name: name.join(" + "), quantity: 1, type: "shipping_fee", unit_price: price * (1 + taxRate) * 100, @@ -167,18 +171,42 @@ class KlarnaProviderService extends PaymentService { } } - // If the cart does have shipping methods, set the selected shipping method - order.shipping_options = shippingOptions.map((so) => ({ - id: so._id, - name: so.name, - price: so.price * (1 + tax_rate) * 100, - tax_amount: so.price * tax_rate * 100, - tax_rate: tax_rate * 10000, - preselected: shippingOptions.length === 1 - })) + const partitioned = shippingOptions.reduce((acc, next) => { + if (acc[next.profile_id]) { + acc[next.profile_id] = [...acc[next.profile_id], next] + } else { + acc[next.profile_id] = [next] + } + return acc + }, {}) + + let f = (a, b) => [].concat(...a.map(a => b.map(b => [].concat(a, b)))) + let cartesian = (a, b, ...c) => b ? cartesian(f(a, b), ...c) : a + + const methods = Object.keys(partitioned).map(k => partitioned[k]) + const combinations = cartesian(...methods) + + + order.shipping_options = combinations.map((combination) => { + combination = Array.isArray(combination) ? combination : [combination] + const details = combination.reduce((acc, next) => { + acc.id = [...acc.id, next._id] + acc.name = [...acc.name, next.name] + acc.price += next.price + return acc + }, { id: [], name: [], price: 0 }) + + return { + id: details.id.join("."), + name: details.name.join(" + "), + price: details.price * (1 + tax_rate) * 100, + tax_amount: details.price * tax_rate * 100, + tax_rate: tax_rate * 10000, + preselected: combinations.length === 1 + } + }) } - console.log(order) return order } diff --git a/packages/medusa-plugin-sendgrid/src/services/sendgrid.js b/packages/medusa-plugin-sendgrid/src/services/sendgrid.js index d50d4a4397..c58379798d 100644 --- a/packages/medusa-plugin-sendgrid/src/services/sendgrid.js +++ b/packages/medusa-plugin-sendgrid/src/services/sendgrid.js @@ -33,6 +33,9 @@ class SendGridService extends BaseService { async transactionalEmail(event, data) { let templateId switch (event) { + case "order.gift_card_created": + templateId = this.options_.gift_card_created_template + break case "order.placed": templateId = this.options_.order_placed_template break @@ -55,12 +58,14 @@ class SendGridService extends BaseService { return } try { - return SendGrid.send({ - template_id: templateId, - from: this.options_.from, - to: data.email, - dynamic_template_data: data, - }) + if (templateId) { + return SendGrid.send({ + template_id: templateId, + from: this.options_.from, + to: data.email, + dynamic_template_data: data, + }) + } } catch (error) { throw error } diff --git a/packages/medusa-plugin-sendgrid/src/subscribers/order.js b/packages/medusa-plugin-sendgrid/src/subscribers/order.js index af82482b43..f345b6e192 100644 --- a/packages/medusa-plugin-sendgrid/src/subscribers/order.js +++ b/packages/medusa-plugin-sendgrid/src/subscribers/order.js @@ -4,6 +4,10 @@ class OrderSubscriber { this.eventBus_ = eventBusService + this.eventBus_.subscribe("order.gift_card_created", async (order) => { + await this.sendgridService_.transactionalEmail("order.gift_card_created", order) + }) + this.eventBus_.subscribe("order.placed", async (order) => { await this.sendgridService_.transactionalEmail("order.placed", order) }) diff --git a/packages/medusa/package.json b/packages/medusa/package.json index 9a91c5a7d9..53872feb28 100644 --- a/packages/medusa/package.json +++ b/packages/medusa/package.json @@ -67,7 +67,8 @@ "passport-http-bearer": "^1.0.1", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", + "randomatic": "^3.1.1", "winston": "^3.2.1" }, "gitHead": "35e0930650d5f4aedf2610749cd131ae8b7e17cc" -} \ No newline at end of file +} diff --git a/packages/medusa/src/api/routes/admin/products/create-product.js b/packages/medusa/src/api/routes/admin/products/create-product.js index df1e31f1cb..dcb92c2720 100644 --- a/packages/medusa/src/api/routes/admin/products/create-product.js +++ b/packages/medusa/src/api/routes/admin/products/create-product.js @@ -3,7 +3,7 @@ import { MedusaError, Validator } from "medusa-core-utils" export default async (req, res) => { const schema = Validator.object().keys({ title: Validator.string().required(), - description: Validator.string(), + description: Validator.string().allow(""), tags: Validator.string(), is_giftcard: Validator.boolean().default(false), options: Validator.array().items({ @@ -15,6 +15,7 @@ export default async (req, res) => { title: Validator.string().required(), sku: Validator.string(), ean: Validator.string(), + barcode: Validator.string(), prices: Validator.array() .items({ currency_code: Validator.string().required(), diff --git a/packages/medusa/src/api/routes/admin/products/create-variant.js b/packages/medusa/src/api/routes/admin/products/create-variant.js index eb7b744206..2507a064b3 100644 --- a/packages/medusa/src/api/routes/admin/products/create-variant.js +++ b/packages/medusa/src/api/routes/admin/products/create-variant.js @@ -36,6 +36,7 @@ export default async (req, res) => { [ "title", "description", + "is_giftcard", "tags", "thumbnail", "handle", diff --git a/packages/medusa/src/api/routes/admin/products/get-product.js b/packages/medusa/src/api/routes/admin/products/get-product.js index 3bee285762..4356933146 100644 --- a/packages/medusa/src/api/routes/admin/products/get-product.js +++ b/packages/medusa/src/api/routes/admin/products/get-product.js @@ -9,6 +9,7 @@ export default async (req, res) => { [ "title", "description", + "is_giftcard", "tags", "thumbnail", "handle", diff --git a/packages/medusa/src/api/routes/admin/products/list-products.js b/packages/medusa/src/api/routes/admin/products/list-products.js index 3114333737..47f0f46395 100644 --- a/packages/medusa/src/api/routes/admin/products/list-products.js +++ b/packages/medusa/src/api/routes/admin/products/list-products.js @@ -10,6 +10,7 @@ export default async (req, res) => { [ "title", "description", + "is_giftcard", "tags", "thumbnail", "handle", diff --git a/packages/medusa/src/api/routes/admin/products/update-product.js b/packages/medusa/src/api/routes/admin/products/update-product.js index f4526c4afd..f3c79f2af3 100644 --- a/packages/medusa/src/api/routes/admin/products/update-product.js +++ b/packages/medusa/src/api/routes/admin/products/update-product.js @@ -18,6 +18,13 @@ export default async (req, res) => { title: Validator.string().optional(), sku: Validator.string().optional(), ean: Validator.string().optional(), + published: Validator.boolean(), + image: Validator.string() + .allow("") + .optional(), + barcode: Validator.string() + .allow("") + .optional(), prices: Validator.array().items( Validator.object() .keys({ diff --git a/packages/medusa/src/api/routes/admin/products/update-variant.js b/packages/medusa/src/api/routes/admin/products/update-variant.js index 92796fe84b..6c9aee0f85 100644 --- a/packages/medusa/src/api/routes/admin/products/update-variant.js +++ b/packages/medusa/src/api/routes/admin/products/update-variant.js @@ -87,6 +87,7 @@ export default async (req, res) => { "options", "thumbnail", "variants", + "is_giftcard", "published", ], ["variants"] diff --git a/packages/medusa/src/api/routes/admin/shipping-options/create-shipping-option.js b/packages/medusa/src/api/routes/admin/shipping-options/create-shipping-option.js index 1704ddb1d8..1261c6afbb 100644 --- a/packages/medusa/src/api/routes/admin/shipping-options/create-shipping-option.js +++ b/packages/medusa/src/api/routes/admin/shipping-options/create-shipping-option.js @@ -33,13 +33,14 @@ export default async (req, res) => { const shippingProfileService = req.scope.resolve("shippingProfileService") // Add to default shipping profile - const { _id } = await shippingProfileService.retrieveDefault() + if (!value.profile_id) { + const { _id } = await shippingProfileService.retrieveDefault() + value.profile_id = _id + } - const data = await optionService.create({ - ...value, - profile_id: _id, - }) - await shippingProfileService.addShippingOption(_id, data._id) + const data = await optionService.create(value) + + await shippingProfileService.addShippingOption(value.profile_id, data._id) res.status(200).json({ shipping_option: data }) } catch (err) { diff --git a/packages/medusa/src/models/__mocks__/cart.js b/packages/medusa/src/models/__mocks__/cart.js index 238953e88d..30e2e9c75e 100644 --- a/packages/medusa/src/models/__mocks__/cart.js +++ b/packages/medusa/src/models/__mocks__/cart.js @@ -41,6 +41,7 @@ export const carts = { cartWithPaySessionsDifRegion: { _id: IdMap.getId("cartWithPaySessionsDifRegion"), region_id: IdMap.getId("region-france"), + total: 1, items: [ { _id: IdMap.getId("existingLine"), @@ -81,6 +82,7 @@ export const carts = { }, cartWithPaySessions: { _id: IdMap.getId("cartWithPaySessions"), + total: 1, region_id: IdMap.getId("testRegion"), shipping_methods: [], items: [ @@ -123,6 +125,7 @@ export const carts = { }, cartWithLine: { _id: IdMap.getId("cartWithLine"), + total: 1, title: "test", region_id: IdMap.getId("testRegion"), items: [ @@ -166,6 +169,92 @@ export const carts = { discounts: [], customer_id: "", }, + withGiftCard: { + _id: IdMap.getId("withGiftCard"), + region_id: IdMap.getId("region-france"), + items: [ + { + _id: IdMap.getId("existingLine"), + title: "merge line", + description: "This is a new line", + is_giftcard: false, + thumbnail: "test-img-yeah.com/thumb", + content: { + unit_price: 123, + variant: { + _id: IdMap.getId("can-cover"), + }, + product: { + _id: IdMap.getId("product"), + }, + quantity: 1, + }, + quantity: 10, + }, + { + _id: IdMap.getId("giftline"), + title: "GiftCard", + description: "Gift card line", + thumbnail: "test-img-yeah.com/thumb", + metadata: { + name: "Test Name", + }, + is_giftcard: true, + content: { + unit_price: 100, + variant: { + _id: IdMap.getId("giftCardVar"), + }, + product: { + _id: IdMap.getId("giftCardProd"), + }, + quantity: 1, + }, + quantity: 1, + }, + ], + email: "test", + payment_sessions: [ + { + provider_id: "default_provider", + data: { + money_id: "success", + }, + }, + ], + payment_method: { + provider_id: "default_provider", + data: { + money_id: "success", + }, + }, + shipping_methods: [ + { + provider_id: "gls", + data: { + yes: "sir", + }, + }, + ], + shipping_address: { + first_name: "hi", + last_name: "you", + country_code: "DK", + city: "of lights", + address_1: "You bet street", + postal_code: "4242", + }, + billing_address: { + first_name: "hi", + last_name: "you", + country_code: "DK", + city: "of lights", + address_1: "You bet street", + postal_code: "4242", + }, + discounts: [], + customer_id: "", + }, completeCart: { _id: IdMap.getId("complete-cart"), region_id: IdMap.getId("region-france"), diff --git a/packages/medusa/src/models/__mocks__/discount.js b/packages/medusa/src/models/__mocks__/discount.js index 37f2aef200..23594351d4 100644 --- a/packages/medusa/src/models/__mocks__/discount.js +++ b/packages/medusa/src/models/__mocks__/discount.js @@ -122,7 +122,7 @@ export const discounts = { } export const DiscountModelMock = { - create: jest.fn().mockReturnValue(Promise.resolve()), + create: jest.fn().mockImplementation(data => Promise.resolve(data)), updateOne: jest.fn().mockImplementation((query, update) => { return Promise.resolve() }), diff --git a/packages/medusa/src/models/__mocks__/shipping-profile.js b/packages/medusa/src/models/__mocks__/shipping-profile.js index 42b1b2fa80..1f7837a5fd 100644 --- a/packages/medusa/src/models/__mocks__/shipping-profile.js +++ b/packages/medusa/src/models/__mocks__/shipping-profile.js @@ -11,7 +11,13 @@ export const profiles = { _id: IdMap.getId("profile1"), name: "Profile One", products: [IdMap.getId("product1")], - shipping_options: [IdMap.getId("shipping1")], + shipping_options: [IdMap.getId("shipping_1")], + }, + profile2: { + _id: IdMap.getId("profile2"), + name: "Profile two", + products: [IdMap.getId("product2")], + shipping_options: [IdMap.getId("shipping_2")], }, } @@ -21,6 +27,10 @@ export const ShippingProfileModelMock = { return Promise.resolve() }), find: jest.fn().mockImplementation(query => { + if (query.products && query.products.$in) { + return Promise.resolve([profiles.profile1, profiles.profile2]) + } + return Promise.resolve([]) }), deleteOne: jest.fn().mockReturnValue(Promise.resolve()), diff --git a/packages/medusa/src/models/schemas/discount.js b/packages/medusa/src/models/schemas/discount.js index 3ccf038215..c0fc8b96e2 100644 --- a/packages/medusa/src/models/schemas/discount.js +++ b/packages/medusa/src/models/schemas/discount.js @@ -3,6 +3,7 @@ import mongoose from "mongoose" import DiscountRule from "./discount-rule" export default new mongoose.Schema({ + is_giftcard: { type: Boolean }, code: { type: String }, discount_rule: { type: DiscountRule }, usage_count: { type: Number }, diff --git a/packages/medusa/src/models/schemas/line-item.js b/packages/medusa/src/models/schemas/line-item.js index 1f3d1009e9..aafcc942d8 100644 --- a/packages/medusa/src/models/schemas/line-item.js +++ b/packages/medusa/src/models/schemas/line-item.js @@ -8,6 +8,7 @@ export default new mongoose.Schema({ description: { type: String }, thumbnail: { type: String }, is_giftcard: { type: Boolean, default: false }, + has_shipping: { type: Boolean, default: false }, // mongoose doesn't allow multi-type validation but this field allows both // an object containing: diff --git a/packages/medusa/src/services/__mocks__/discount.js b/packages/medusa/src/services/__mocks__/discount.js index e287a99393..0c351ff4b8 100644 --- a/packages/medusa/src/services/__mocks__/discount.js +++ b/packages/medusa/src/services/__mocks__/discount.js @@ -46,6 +46,11 @@ export const DiscountServiceMock = { removeRegion: jest.fn().mockReturnValue(Promise.resolve()), addValidVariant: jest.fn().mockReturnValue(Promise.resolve()), removeValidVariant: jest.fn().mockReturnValue(Promise.resolve()), + generateGiftCard: jest.fn().mockReturnValue( + Promise.resolve({ + _id: IdMap.getId("gift_card_id"), + }) + ), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/__mocks__/shipping-option.js b/packages/medusa/src/services/__mocks__/shipping-option.js index 9ee657d057..97b4e32d90 100644 --- a/packages/medusa/src/services/__mocks__/shipping-option.js +++ b/packages/medusa/src/services/__mocks__/shipping-option.js @@ -110,6 +110,7 @@ export const ShippingOptionServiceMock = { _id: IdMap.getId("fail"), }) } + return Promise.resolve({ _id: methodId }) }), delete: jest.fn().mockReturnValue(Promise.resolve()), } diff --git a/packages/medusa/src/services/__mocks__/shipping-profile.js b/packages/medusa/src/services/__mocks__/shipping-profile.js index 6e49c1dd81..97028bf88c 100644 --- a/packages/medusa/src/services/__mocks__/shipping-profile.js +++ b/packages/medusa/src/services/__mocks__/shipping-profile.js @@ -29,7 +29,7 @@ export const ShippingProfileServiceMock = { if (data === IdMap.getId("profile1")) { return Promise.resolve(profiles.other) } - return Promise.resolve() + return Promise.resolve(profiles.default) }), retrieveGiftCardDefault: jest.fn().mockImplementation(data => { return Promise.resolve({ _id: IdMap.getId("giftCardProfile") }) diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index 2d538ae06f..a1d79e7f63 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -10,6 +10,7 @@ import { RegionServiceMock } from "../__mocks__/region" import { EventBusServiceMock } from "../__mocks__/event-bus" import { CustomerServiceMock } from "../__mocks__/customer" import { ShippingOptionServiceMock } from "../__mocks__/shipping-option" +import { TotalsServiceMock } from "../__mocks__/totals" import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile" import { CartModelMock, carts } from "../../models/__mocks__/cart" import { LineItemServiceMock } from "../__mocks__/line-item" @@ -193,7 +194,12 @@ describe("CartService", () => { _id: IdMap.getId("emptyCart"), }, { - $push: { items: lineItem }, + $push: { + items: { + ...lineItem, + has_shipping: false, + }, + }, } ) }) @@ -225,7 +231,10 @@ describe("CartService", () => { "items._id": IdMap.getId("existingLine"), }, { - $set: { "items.$.quantity": 20 }, + $set: { + "items.$.quantity": 20, + "items.$.has_shipping": false, + }, } ) }) @@ -267,7 +276,12 @@ describe("CartService", () => { _id: IdMap.getId("cartWithLine"), }, { - $push: { items: lineItem }, + $push: { + items: { + ...lineItem, + has_shipping: false, + }, + }, } ) }) @@ -390,10 +404,9 @@ describe("CartService", () => { expect(CartModelMock.updateOne).toHaveBeenCalledWith( { _id: IdMap.getId("cartWithLine"), - "items._id": IdMap.getId("existingLine"), }, { - $set: { "items.$.quantity": 9 }, + $pull: { items: { _id: IdMap.getId("existingLine") } }, } ) }) @@ -905,6 +918,7 @@ describe("CartService", () => { cartModel: CartModelMock, regionService: RegionServiceMock, paymentProviderService: PaymentProviderServiceMock, + totalsService: TotalsServiceMock, eventBusService: EventBusServiceMock, }) @@ -1183,6 +1197,26 @@ describe("CartService", () => { }, { $set: { + items: [ + { + _id: IdMap.getId("existingLine"), + title: "merge line", + description: "This is a new line", + has_shipping: true, + thumbnail: "test-img-yeah.com/thumb", + content: { + unit_price: 123, + variant: { + _id: IdMap.getId("can-cover"), + }, + product: { + _id: IdMap.getId("product"), + }, + quantity: 1, + }, + quantity: 10, + }, + ], shipping_methods: [ { _id: IdMap.getId("freeShipping"), @@ -1221,6 +1255,10 @@ describe("CartService", () => { }, { $set: { + items: carts.frCart.items.map(i => ({ + ...i, + has_shipping: false, + })), shipping_methods: [ { _id: IdMap.getId("freeShipping"), @@ -1261,6 +1299,10 @@ describe("CartService", () => { }, { $set: { + items: carts.frCart.items.map(i => ({ + ...i, + has_shipping: false, + })), shipping_methods: [ { _id: IdMap.getId("freeShipping"), diff --git a/packages/medusa/src/services/__tests__/discount.js b/packages/medusa/src/services/__tests__/discount.js index b3f43fdff5..1de10e3cf0 100644 --- a/packages/medusa/src/services/__tests__/discount.js +++ b/packages/medusa/src/services/__tests__/discount.js @@ -6,6 +6,7 @@ import { } from "../../models/__mocks__/dynamic-discount-code" import { IdMap } from "medusa-test-utils" import { ProductVariantServiceMock } from "../__mocks__/product-variant" +import { EventBusServiceMock } from "../__mocks__/event-bus" import { RegionServiceMock } from "../__mocks__/region" describe("DiscountService", () => { @@ -274,4 +275,32 @@ describe("DiscountService", () => { ) }) }) + + describe("generateGiftCard", () => { + const discountService = new DiscountService({ + discountModel: DiscountModelMock, + regionService: RegionServiceMock, + eventBusService: EventBusServiceMock, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("calls model layer create", async () => { + await discountService.generateGiftCard(100, IdMap.getId("testRegion")) + + expect(DiscountModelMock.create).toHaveBeenCalledTimes(1) + expect(DiscountModelMock.create).toHaveBeenCalledWith({ + code: expect.stringMatching(/(([A-Z0-9]){4}(-?)){4}/), + is_giftcard: true, + discount_rule: { + type: "fixed", + allocation: "total", + value: 100, + }, + regions: [IdMap.getId("testRegion")], + }) + }) + }) }) diff --git a/packages/medusa/src/services/__tests__/order.js b/packages/medusa/src/services/__tests__/order.js index 8ee3ac0fcd..feb9f78b7c 100644 --- a/packages/medusa/src/services/__tests__/order.js +++ b/packages/medusa/src/services/__tests__/order.js @@ -3,6 +3,7 @@ import { OrderModelMock, orders } from "../../models/__mocks__/order" import { carts } from "../../models/__mocks__/cart" import OrderService from "../order" import { PaymentProviderServiceMock } from "../__mocks__/payment-provider" +import { DiscountServiceMock } from "../__mocks__/discount" import { FulfillmentProviderServiceMock } from "../__mocks__/fulfillment-provider" import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile" import { TotalsServiceMock } from "../__mocks__/totals" @@ -36,6 +37,7 @@ describe("OrderService", () => { const orderService = new OrderService({ orderModel: OrderModelMock, paymentProviderService: PaymentProviderServiceMock, + discountService: DiscountServiceMock, regionService: RegionServiceMock, eventBusService: EventBusServiceMock, }) @@ -60,6 +62,83 @@ describe("OrderService", () => { session: expect.anything(), }) }) + + it("creates cart with gift card", async () => { + await orderService.createFromCart(carts.withGiftCard) + + const order = { + ...carts.withGiftCard, + items: [ + { + _id: IdMap.getId("existingLine"), + title: "merge line", + description: "This is a new line", + is_giftcard: false, + thumbnail: "test-img-yeah.com/thumb", + content: { + unit_price: 123, + variant: { + _id: IdMap.getId("can-cover"), + }, + product: { + _id: IdMap.getId("product"), + }, + quantity: 1, + }, + quantity: 10, + }, + { + _id: IdMap.getId("giftline"), + title: "GiftCard", + description: "Gift card line", + thumbnail: "test-img-yeah.com/thumb", + metadata: { + giftcard: IdMap.getId("gift_card_id"), + name: "Test Name", + }, + is_giftcard: true, + content: { + unit_price: 100, + variant: { + _id: IdMap.getId("giftCardVar"), + }, + product: { + _id: IdMap.getId("giftCardProd"), + }, + quantity: 1, + }, + quantity: 1, + }, + ], + currency_code: "eur", + cart_id: carts.withGiftCard._id, + } + + delete order._id + delete order.payment_sessions + + expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(2) + expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + "order.gift_card_created", + { + currency_code: "eur", + tax_rate: 0.25, + email: "test", + giftcard: expect.any(Object), + } + ) + + expect(DiscountServiceMock.generateGiftCard).toHaveBeenCalledTimes(1) + expect(DiscountServiceMock.generateGiftCard).toHaveBeenCalledWith( + 100, + IdMap.getId("region-france") + ) + + expect(OrderModelMock.create).toHaveBeenCalledTimes(1) + expect(OrderModelMock.create).toHaveBeenCalledWith([order], { + session: expect.anything(), + }) + }) }) describe("retrieve", () => { @@ -297,11 +376,9 @@ describe("OrderService", () => { }) it("throws if payment is already processed", async () => { - try { - await orderService.capturePayment(IdMap.getId("payed-order")) - } catch (error) { - expect(error.message).toEqual("Payment already captured") - } + await expect( + orderService.capturePayment(IdMap.getId("payed-order")) + ).rejects.toThrow("Payment already captured") }) }) @@ -324,16 +401,36 @@ describe("OrderService", () => { expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) expect(OrderModelMock.updateOne).toHaveBeenCalledWith( { _id: IdMap.getId("test-order") }, - { $set: { fulfillment_status: "fulfilled" } } + { + $set: { + fulfillment_status: "fulfilled", + shipping_methods: [ + { + _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", + }, + ], + }, + } ) }) it("throws if payment is already processed", async () => { - try { - await orderService.createFulfillment(IdMap.getId("fulfilled-order")) - } catch (error) { - expect(error.message).toEqual("Order is already fulfilled") - } + await expect( + orderService.createFulfillment(IdMap.getId("fulfilled-order")) + ).rejects.toThrow("Order is already fulfilled") }) }) diff --git a/packages/medusa/src/services/__tests__/shipping-profile.js b/packages/medusa/src/services/__tests__/shipping-profile.js index 9a49f349bd..fc7a986db4 100644 --- a/packages/medusa/src/services/__tests__/shipping-profile.js +++ b/packages/medusa/src/services/__tests__/shipping-profile.js @@ -353,6 +353,37 @@ describe("ShippingProfileService", () => { }) }) + describe("fetchCartOptions", () => { + const profileService = new ShippingProfileService({ + shippingProfileModel: ShippingProfileModelMock, + shippingOptionService: ShippingOptionServiceMock, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("fetches correct options", async () => { + await profileService.fetchCartOptions({ + items: [ + { + content: { product: { _id: IdMap.getId("product_1") } }, + }, + { + content: { product: { _id: IdMap.getId("product_2") } }, + }, + ], + }) + + expect(ShippingProfileModelMock.find).toBeCalledTimes(1) + expect(ShippingProfileModelMock.find).toBeCalledWith({ + products: { $in: [IdMap.getId("product_1"), IdMap.getId("product_2")] }, + }) + + expect(ShippingOptionServiceMock.validateCartOption).toBeCalledTimes(2) + }) + }) + describe("addShippingOption", () => { const profileService = new ShippingProfileService({ shippingProfileModel: ShippingProfileModelMock, diff --git a/packages/medusa/src/services/cart.js b/packages/medusa/src/services/cart.js index dcc33b3de2..dbefa68c33 100644 --- a/packages/medusa/src/services/cart.js +++ b/packages/medusa/src/services/cart.js @@ -249,6 +249,23 @@ class CartService extends BaseService { return c } + /** + * Returns an array of product ids in a line item. + * @param {LineItem} item - the line item to fetch products from + * @return {[string]} an array of product ids + */ + getItemProducts_(item) { + // Find all the products in the line item + const products = [] + if (Array.isArray(item.content)) { + item.content.forEach(c => products.push(`${c.product._id}`)) + } else { + products.push(`${item.content.product._id}`) + } + + return products + } + /** * Removes a line item from the cart. * @param {string} cartId - the id of the cart that we will remove from @@ -258,33 +275,42 @@ class CartService extends BaseService { async removeLineItem(cartId, lineItemId) { const cart = await this.retrieve(cartId) const itemToRemove = cart.items.find(line => line._id.equals(lineItemId)) - if (!itemToRemove) { return Promise.resolve(cart) } - // If cart has more than one of those line items, we update the quantity - // instead of removing it - if (itemToRemove.quantity > 1) { - const newQuantity = itemToRemove.quantity - 1 + const update = { + $pull: { items: { _id: itemToRemove._id } }, + } - return this.cartModel_ - .updateOne( - { - _id: cartId, - "items._id": itemToRemove._id, - }, - { - $set: { - "items.$.quantity": newQuantity, - }, + // Remove shipping methods if they are not needed + if (cart.shipping_methods && cart.shipping_methods.length) { + const filteredItems = cart.items.filter(i => !i._id.equals(lineItemId)) + + let newShippingMethods = await Promise.all( + cart.shipping_methods.map(async m => { + const profile = await this.shippingProfileService_.retrieve( + m.profile_id + ) + const hasItem = filteredItems.find(item => { + const products = this.getItemProducts_(item) + return products.some(p => profile.products.includes(p)) + }) + + if (hasItem) { + return m } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result + + return null }) + ) + newShippingMethods = newShippingMethods.filter(n => !!n) + + if (newShippingMethods.length !== cart.shipping_methods.length) { + update.$set = { + shipping_methods: newShippingMethods, + } + } } return this.cartModel_ @@ -292,11 +318,7 @@ class CartService extends BaseService { { _id: cartId, }, - { - $pull: { - items: { _id: itemToRemove._id }, - }, - } + update ) .then(result => { // Notify subscribers @@ -305,6 +327,34 @@ class CartService extends BaseService { }) } + /** + * Checks if a given line item has a shipping method that can fulfill it. + * Returns true if all products in the cart can be fulfilled with the current + * shipping methods. + * @param {Cart} cart - the cart + * @param {LineItem} lineItem - the line item + * @return {boolean} + */ + async validateLineItemShipping_(shippingMethods, lineItem) { + if (shippingMethods && shippingMethods.length) { + const profiles = await Promise.all( + shippingMethods.map(m => + this.shippingProfileService_.retrieve(m.profile_id) + ) + ) + + const products = this.getItemProducts_(lineItem) + + // Check if there is a shipping method for each product + const hasShipping = products.map( + p => !!profiles.find(profile => profile.products.includes(p)) + ) + return hasShipping.every(b => b) + } + + return false + } + /** * Adds a line item to the cart. * @param {string} cartId - the id of the cart that we will add to @@ -318,6 +368,11 @@ class CartService extends BaseService { this.lineItemService_.isEqual(line, validatedLineItem) ) + const hasShipping = await this.validateLineItemShipping_( + cart.shipping_methods, + validatedLineItem + ) + // If content matches one of the line items currently in the cart we can // simply update the quantity of the existing line item if (currentItem) { @@ -345,6 +400,7 @@ class CartService extends BaseService { { $set: { "items.$.quantity": newQuantity, + "items.$.has_shipping": hasShipping, }, } ) @@ -375,7 +431,12 @@ class CartService extends BaseService { _id: cartId, }, { - $push: { items: validatedLineItem }, + $push: { + items: { + ...validatedLineItem, + has_shipping: hasShipping, + }, + }, } ) .then(result => { @@ -808,6 +869,25 @@ class CartService extends BaseService { const cart = await this.retrieve(cartId) const region = await this.regionService_.retrieve(cart.region_id) + const total = await this.totalsService_.getTotal(cart) + + if (total === 0) { + return this.cartModel_ + .updateOne( + { + _id: cart._id, + }, + { + $set: { payment_sessions: [] }, + } + ) + .then(result => { + // Notify subscribers + this.eventBus_.emit(CartService.Events.UPDATED, result) + return result + }) + } + // If there are existing payment sessions ensure that these are up to date let sessions = [] if (cart.payment_sessions && cart.payment_sessions.length) { @@ -817,10 +897,19 @@ class CartService extends BaseService { return null } - const data = await this.paymentProviderService_.updateSession( - pSession, - cart - ) + let data + try { + data = await this.paymentProviderService_.updateSession( + pSession, + cart + ) + } catch (err) { + data = await this.paymentProviderService_.createSession( + pSession.provider_id, + cart + ) + } + return { provider_id: pSession.provider_id, data, @@ -981,13 +1070,27 @@ class CartService extends BaseService { newMethods.push(option) } + const newItems = await Promise.all( + cart.items.map(async item => { + const hasShipping = await this.validateLineItemShipping_( + newMethods, + item + ) + + return { + ...item, + has_shipping: hasShipping, + } + }) + ) + return this.cartModel_ .updateOne( { _id: cart._id, }, { - $set: { shipping_methods: newMethods }, + $set: { shipping_methods: newMethods, items: newItems }, } ) .then(result => { @@ -1045,20 +1148,15 @@ class CartService extends BaseService { update.shipping_methods = [] } - //if (cart.items.length && cart.payment_sessions.length) { - // update.payment_sessions = await Promise.all( - // region.payment_providers.map(async pId => { - // const data = await this.paymentProviderService_.createSession(pId, { - // ...cart, - // ...update, - // }) - // return { - // provider_id: pId, - // data, - // } - // }) - // ) - //} + if (cart.discounts && cart.discounts.length) { + const newDiscounts = cart.discounts.map(d => { + if (d.regions.includes(regionId)) { + return d + } + }) + + update.discounts = newDiscounts.filter(d => !!d) + } // Payment methods are region specific so the user needs to find a // new payment method diff --git a/packages/medusa/src/services/discount.js b/packages/medusa/src/services/discount.js index 5f1b98f6d1..c0241d53e7 100644 --- a/packages/medusa/src/services/discount.js +++ b/packages/medusa/src/services/discount.js @@ -1,6 +1,7 @@ +import _ from "lodash" +import randomize from "randomatic" import { BaseService } from "medusa-interfaces" import { Validator, MedusaError } from "medusa-core-utils" -import _ from "lodash" /** * Provides layer to manipulate discounts. @@ -13,6 +14,7 @@ class DiscountService extends BaseService { totalsService, productVariantService, regionService, + eventBusService, }) { super() @@ -30,6 +32,9 @@ class DiscountService extends BaseService { /** @private @const {RegionService} */ this.regionService_ = regionService + + /** @private @const {EventBus} */ + this.eventBus_ = eventBusService } /** @@ -60,7 +65,7 @@ class DiscountService extends BaseService { description: Validator.string(), type: Validator.string().required(), value: Validator.number() - .positive() + .min(0) .required(), allocation: Validator.string().required(), valid_for: Validator.array().items(Validator.string()), @@ -210,6 +215,38 @@ class DiscountService extends BaseService { ) } + /** + * Generates a gift card with the specified value which is valid in the + * specified region. + * @param {number} value - the value that the gift card represents + * @param {string} regionId - the id of the region in which the gift card can + * be used + * @return {Discount} the newly created gift card + */ + async generateGiftCard(value, regionId) { + const region = await this.regionService_.retrieve(regionId) + + const code = [ + randomize("A0", 4), + randomize("A0", 4), + randomize("A0", 4), + randomize("A0", 4), + ].join("-") + + const discountRule = this.validateDiscountRule_({ + type: "fixed", + allocation: "total", + value, + }) + + return this.discountModel_.create({ + code, + discount_rule: discountRule, + is_giftcard: true, + regions: [region._id], + }) + } + /** * Creates a dynamic code for a discount id. * @param {string} discountId - the id of the discount to create a code for diff --git a/packages/medusa/src/services/line-item.js b/packages/medusa/src/services/line-item.js index d9f6317a9e..823f9b59cb 100644 --- a/packages/medusa/src/services/line-item.js +++ b/packages/medusa/src/services/line-item.js @@ -37,6 +37,7 @@ class LineItemService extends BaseService { const lineItemSchema = Validator.object({ title: Validator.string().required(), + is_giftcard: Validator.bool().optional(), description: Validator.string() .allow("") .optional(), diff --git a/packages/medusa/src/services/order.js b/packages/medusa/src/services/order.js index b065ba8b6c..d1f385e8e7 100644 --- a/packages/medusa/src/services/order.js +++ b/packages/medusa/src/services/order.js @@ -4,6 +4,7 @@ import { BaseService } from "medusa-interfaces" class OrderService extends BaseService { static Events = { + GIFT_CARD_CREATED: "order.gift_card_created", PLACED: "order.placed", UPDATED: "order.updated", CANCELLED: "order.cancelled", @@ -14,6 +15,7 @@ class OrderService extends BaseService { orderModel, paymentProviderService, shippingProfileService, + discountService, fulfillmentProviderService, lineItemService, totalsService, @@ -43,6 +45,9 @@ class OrderService extends BaseService { /** @private @const {RegionService} */ this.regionService_ = regionService + /** @private @const {DiscountService} */ + this.discountService_ = discountService + /** @private @const {EventBus} */ this.eventBus_ = eventBusService } @@ -306,6 +311,33 @@ class OrderService extends BaseService { paymentSession.data ) + // Generate gift cards if in cart + const items = await Promise.all( + cart.items.map(async i => { + if (i.is_giftcard) { + const giftcard = await this.discountService_ + .generateGiftCard(i.content.unit_price, region._id) + .then(result => { + this.eventBus_.emit(OrderService.Events.GIFT_CARD_CREATED, { + currency_code: region.currency_code, + tax_rate: region.tax_rate, + giftcard: result, + email: cart.email, + }) + return result + }) + return { + ...i, + metadata: { + ...i.metadata, + giftcard: giftcard._id, + }, + } + } + return i + }) + ) + const o = { payment_method: { provider_id: paymentSession.provider_id, @@ -313,7 +345,7 @@ class OrderService extends BaseService { }, discounts: cart.discounts, shipping_methods: cart.shipping_methods, - items: cart.items, + items, shipping_address: cart.shipping_address, billing_address: cart.shipping_address, region_id: cart.region_id, @@ -329,7 +361,7 @@ class OrderService extends BaseService { // Emit and return this.eventBus_.emit(OrderService.Events.PLACED, orderDocument[0]) - return orderDocument[0].toObject() + return orderDocument[0] }) .then(() => this.orderModel_.findOne({ cart_id: cart._id })) } @@ -541,14 +573,17 @@ class OrderService extends BaseService { } // partition order items to their dedicated shipping method - order.shipping_methods = await this.partitionItems_(shipping_methods, items) + updateFields.shipping_methods = await this.partitionItems_( + shipping_methods, + items + ) await Promise.all( - order.shipping_methods.map(method => { + updateFields.shipping_methods.map(method => { const provider = this.fulfillmentProviderService_.retrieveProvider( method.provider_id ) - provider.createOrder(method.data, method.items) + return provider.createOrder(method.data, method.items) }) ) diff --git a/packages/medusa/src/services/product.js b/packages/medusa/src/services/product.js index 1bd5729fb6..a91ed67639 100644 --- a/packages/medusa/src/services/product.js +++ b/packages/medusa/src/services/product.js @@ -143,7 +143,14 @@ class ProductService extends BaseService { } if (update.variants) { - update.variants = await Promise.all( + const existingVariants = await this.retrieveVariants(validatedId) + for (const existing of existingVariants) { + if (!update.variants.find(v => v._id && existing._id.equals(v._id))) { + await this.deleteVariant(productId, existing._id) + } + } + + await Promise.all( update.variants.map(async variant => { if (variant._id) { if (variant.prices && variant.prices.length) { diff --git a/packages/medusa/src/services/shipping-profile.js b/packages/medusa/src/services/shipping-profile.js index 52cc5ba5e8..ec63af4698 100644 --- a/packages/medusa/src/services/shipping-profile.js +++ b/packages/medusa/src/services/shipping-profile.js @@ -101,7 +101,7 @@ class ShippingProfileService extends BaseService { * Retrieves the default gift card profile * @return the shipping profile for gift cards */ - async retrieveGiftCardProfile() { + async retrieveGiftCardDefault() { return await this.profileModel_ .findOne({ name: "default_gift_card_profile" }) .catch(err => { @@ -115,7 +115,7 @@ class ShippingProfileService extends BaseService { * @return {Promise} the shipping profile */ async createGiftCardDefault() { - const profile = await this.retrieveGiftCardProfile() + const profile = await this.retrieveGiftCardDefault() if (!profile) { return this.profileModel_.create({ name: "default_gift_card_profile" }) } @@ -388,13 +388,21 @@ class ShippingProfileService extends BaseService { ) const options = await Promise.all( - optionIds.map(oId => { - return this.shippingOptionService_ + optionIds.map(async oId => { + const option = await this.shippingOptionService_ .validateCartOption(oId, cart) .catch(err => { // If validation failed we skip the option return null }) + + if (option) { + return { + ...option, + profile: profiles.find(p => p._id.equals(option.profile_id)), + } + } + return null }) ) diff --git a/packages/medusa/src/services/totals.js b/packages/medusa/src/services/totals.js index 483333a807..09289df137 100644 --- a/packages/medusa/src/services/totals.js +++ b/packages/medusa/src/services/totals.js @@ -292,36 +292,29 @@ class TotalsService extends BaseService { } const { type, allocation, value } = discount.discount_rule + let toReturn = 0 if (type === "percentage" && allocation === "total") { - return (subtotal / 100) * value - } - - if (type === "percentage" && allocation === "item") { + toReturn = (subtotal / 100) * value + } else if (type === "percentage" && allocation === "item") { const itemPercentageDiscounts = await this.getAllocationItemDiscounts( discount, cart, "percentage" ) - const totalDiscount = _.sumBy(itemPercentageDiscounts, d => d.amount) - return totalDiscount - } - - if (type === "fixed" && allocation === "total") { - return value - } - - if (type === "fixed" && allocation === "item") { + toReturn = _.sumBy(itemPercentageDiscounts, d => d.amount) + } else if (type === "fixed" && allocation === "total") { + toReturn = value + } else if (type === "fixed" && allocation === "item") { const itemFixedDiscounts = await this.getAllocationItemDiscounts( discount, cart, "fixed" ) - const totalDiscount = _.sumBy(itemFixedDiscounts, d => d.amount) - return totalDiscount + toReturn = _.sumBy(itemFixedDiscounts, d => d.amount) } - return 0 + return Math.min(subtotal, toReturn) } } diff --git a/packages/medusa/src/subscribers/order.js b/packages/medusa/src/subscribers/order.js index f9689029b3..81f8f708c6 100644 --- a/packages/medusa/src/subscribers/order.js +++ b/packages/medusa/src/subscribers/order.js @@ -4,9 +4,17 @@ class OrderSubscriber { cartService, customerService, eventBusService, + discountService, + totalsService, }) { + this.totalsService_ = totalsService + this.paymentProviderService_ = paymentProviderService + this.customerService_ = customerService + + this.discountService_ = discountService + this.cartService_ = cartService this.eventBus_ = eventBusService @@ -33,6 +41,34 @@ class OrderSubscriber { this.eventBus_.subscribe("order.placed", async order => { await this.cartService_.delete(order.cart_id) }) + + this.eventBus_.subscribe("order.placed", this.handleDiscounts) + } + + handleDiscounts = async order => { + await Promise.all( + order.discounts.map(async d => { + const subtotal = await this.totalsService_.getSubtotal(order) + if (d.is_giftcard) { + const discountRule = { + ...d.discount_rule, + value: Math.max(0, d.discount_rule.value - subtotal), + } + + delete discountRule._id + + return this.discountService_.update(d._id, { + discount_rule: discountRule, + usage_count: d.usage_count + 1, + disabled: discountRule.value === 0, + }) + } else { + return this.discountService_.update(d._id, { + usage_count: d.usage_count + 1, + }) + } + }) + ) } } diff --git a/packages/medusa/yarn.lock b/packages/medusa/yarn.lock index 20f2789ac0..d8c0b200f7 100644 --- a/packages/medusa/yarn.lock +++ b/packages/medusa/yarn.lock @@ -3665,6 +3665,11 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" +is-number@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" + integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -4553,6 +4558,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +math-random@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" + integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -5501,6 +5511,15 @@ random-bytes@~1.0.0: resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= +randomatic@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" + integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw== + dependencies: + is-number "^4.0.0" + kind-of "^6.0.0" + math-random "^1.0.1" + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" From c18677d5f2b2605635613cab0e03bf1870485a11 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Mon, 3 Aug 2020 10:23:45 +0200 Subject: [PATCH 2/5] Adds integration to send sales orders to brightpearl --- .../src/services/stripe-provider.js | 6 + packages/medusa-plugin-brightpearl/.babelrc | 13 + packages/medusa-plugin-brightpearl/.eslintrc | 9 + packages/medusa-plugin-brightpearl/.gitignore | 15 + packages/medusa-plugin-brightpearl/.npmignore | 9 + .../medusa-plugin-brightpearl/.prettierrc | 7 + packages/medusa-plugin-brightpearl/index.js | 1 + .../medusa-plugin-brightpearl/package.json | 42 +++ .../src/services/brightpearl.js | 260 ++++++++++++++++++ .../src/subscribers/order.js | 31 +++ .../src/utils/brightpearl.js | 166 +++++++++++ .../utils/brightpearl.js | 216 +++++++++++++++ .../src/services/segment.js | 1 - packages/medusa/src/api/index.js | 33 +++ packages/medusa/src/models/order.js | 3 + packages/medusa/src/models/region.js | 1 + .../medusa/src/models/schemas/shipment.js | 7 + packages/medusa/src/services/order.js | 76 ++++- packages/medusa/src/services/totals.js | 39 +++ packages/medusa/src/subscribers/order.js | 6 +- 20 files changed, 931 insertions(+), 10 deletions(-) create mode 100644 packages/medusa-plugin-brightpearl/.babelrc create mode 100644 packages/medusa-plugin-brightpearl/.eslintrc create mode 100644 packages/medusa-plugin-brightpearl/.gitignore create mode 100644 packages/medusa-plugin-brightpearl/.npmignore create mode 100644 packages/medusa-plugin-brightpearl/.prettierrc create mode 100644 packages/medusa-plugin-brightpearl/index.js create mode 100644 packages/medusa-plugin-brightpearl/package.json create mode 100644 packages/medusa-plugin-brightpearl/src/services/brightpearl.js create mode 100644 packages/medusa-plugin-brightpearl/src/subscribers/order.js create mode 100644 packages/medusa-plugin-brightpearl/src/utils/brightpearl.js create mode 100644 packages/medusa-plugin-brightpearl/utils/brightpearl.js create mode 100644 packages/medusa/src/models/schemas/shipment.js diff --git a/packages/medusa-payment-stripe/src/services/stripe-provider.js b/packages/medusa-payment-stripe/src/services/stripe-provider.js index 183b34c8af..2070a5c885 100644 --- a/packages/medusa-payment-stripe/src/services/stripe-provider.js +++ b/packages/medusa-payment-stripe/src/services/stripe-provider.js @@ -162,6 +162,12 @@ class StripeProviderService extends PaymentService { try { const { id } = data return this.stripe_.paymentIntents.cancel(id) + .catch(err => { + if (err.statusCode === 400) { + return + } + throw err + }) } catch (error) { throw error } diff --git a/packages/medusa-plugin-brightpearl/.babelrc b/packages/medusa-plugin-brightpearl/.babelrc new file mode 100644 index 0000000000..4d2dfe8f09 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/.babelrc @@ -0,0 +1,13 @@ +{ + "plugins": [ + "@babel/plugin-proposal-class-properties", + "@babel/plugin-transform-instanceof", + "@babel/plugin-transform-classes" + ], + "presets": ["@babel/preset-env"], + "env": { + "test": { + "plugins": ["@babel/plugin-transform-runtime"] + } + } +} diff --git a/packages/medusa-plugin-brightpearl/.eslintrc b/packages/medusa-plugin-brightpearl/.eslintrc new file mode 100644 index 0000000000..2a889697f0 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/.eslintrc @@ -0,0 +1,9 @@ +{ + "plugins": ["prettier"], + "extends": ["prettier"], + "rules": { + "prettier/prettier": "error", + "semi": "error", + "no-unused-expressions": "true" + } +} diff --git a/packages/medusa-plugin-brightpearl/.gitignore b/packages/medusa-plugin-brightpearl/.gitignore new file mode 100644 index 0000000000..a28122e219 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/.gitignore @@ -0,0 +1,15 @@ +/lib +node_modules +.DS_store +.env* +/*.js +!index.js +yarn.lock + +/dist + +/api +/services +/models +/subscribers + diff --git a/packages/medusa-plugin-brightpearl/.npmignore b/packages/medusa-plugin-brightpearl/.npmignore new file mode 100644 index 0000000000..486581be18 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/.npmignore @@ -0,0 +1,9 @@ +/lib +node_modules +.DS_store +.env* +/*.js +!index.js +yarn.lock + + diff --git a/packages/medusa-plugin-brightpearl/.prettierrc b/packages/medusa-plugin-brightpearl/.prettierrc new file mode 100644 index 0000000000..70175ce150 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/.prettierrc @@ -0,0 +1,7 @@ +{ + "endOfLine": "lf", + "semi": false, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5" +} \ No newline at end of file diff --git a/packages/medusa-plugin-brightpearl/index.js b/packages/medusa-plugin-brightpearl/index.js new file mode 100644 index 0000000000..172f1ae6a4 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/index.js @@ -0,0 +1 @@ +// noop diff --git a/packages/medusa-plugin-brightpearl/package.json b/packages/medusa-plugin-brightpearl/package.json new file mode 100644 index 0000000000..3add66aa9a --- /dev/null +++ b/packages/medusa-plugin-brightpearl/package.json @@ -0,0 +1,42 @@ +{ + "name": "medusa-plugin-brightpearl", + "version": "1.0.0", + "description": "Brightpearl plugin for Medusa Commerce", + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/medusa-plugin-brightpearl" + }, + "author": "Sebastian Rindom", + "license": "AGPL-3.0-or-later", + "devDependencies": { + "@babel/cli": "^7.7.5", + "@babel/core": "^7.7.5", + "@babel/node": "^7.7.4", + "@babel/plugin-proposal-class-properties": "^7.7.4", + "@babel/plugin-transform-classes": "^7.9.5", + "@babel/plugin-transform-instanceof": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.7.6", + "@babel/preset-env": "^7.7.5", + "@babel/register": "^7.7.4", + "@babel/runtime": "^7.9.6", + "client-sessions": "^0.8.0", + "cross-env": "^5.2.1", + "eslint": "^6.8.0", + "jest": "^25.5.2", + "medusa-test-utils": "^0.3.0" + }, + "scripts": { + "build": "babel src -d dist", + "prepare": "cross-env NODE_ENV=production npm run build", + "watch": "babel -w src --out-dir . --ignore **/__tests__", + "test": "jest" + }, + "dependencies": { + "axios": "^0.19.2", + "express": "^4.17.1", + "medusa-core-utils": "^0.3.0", + "medusa-interfaces": "^0.3.0" + } +} diff --git a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js new file mode 100644 index 0000000000..ade9a2b93a --- /dev/null +++ b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js @@ -0,0 +1,260 @@ +import { BaseService } from "medusa-interfaces" +import Brightpearl from "../utils/brightpearl" + +class BrightpearlService extends BaseService { + constructor({ totalsService, regionService, orderService, discountService }, options) { + super() + + this.options = options + this.regionService_ = regionService + this.orderService_ = orderService + this.totalsService_ = totalsService + this.discountService_ = discountService + + this.brightpearl_ = new Brightpearl({ + account: options.account, + datacenter: options.datacenter, + app_ref: options.app_ref, + token: options.token + }) + } + + async createGoodsOutNote(fromOrder, shipment) { + const id = fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id + + if (!id) { + return + } + + const order = await this.brightpearl_.orders.retrieve(id) + const productRows = shipment.item_ids.map(id => { + const row = order.rows.find(({ externalRef }) => externalRef === id) + return { + productId: row.productId, + salesOrderRowId: row.id, + quantity: row.quantity + } + }) + + const goodsOut = { + warehouses: [ + { + releaseDate: new Date(), + warehouseId: this.options.warehouse, + transfer: false, + products: productRows, + } + ], + priority: false, + } + + return this.brightpearl_.warehouses.createGoodsOutNote(id, goodsOut) + } + + registerGoodsOutShipped(noteId, shipment) { + return this.brightpearl_.warehouses.registerGoodsOutEvent(noteId, { + events: [ + { + eventCode: "SHW", + occured: new Date(), + eventOwnerId: this.options.event_owner, + } + ] + }) + } + + registerGoodsOutTrackingNumber(noteId, shipment) { + return this.brightpearl_.warehouses.updateGoodsOutNote(noteId, { + priority: false, + shipping: { + reference: shipment.tracking_number, + } + }) + } + + async createSalesOrder(fromOrder) { + let customer = await this.retrieveCustomerByEmail(fromOrder.email) + + // All sales orders must have a customer + if (!customer) { + customer = await this.createCustomer(fromOrder) + } + + const { shipping_address } = fromOrder + const order = { + currency: { + code: fromOrder.currency_code + }, + externalRef: fromOrder._id, + 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(fromOrder) + } + + return this.brightpearl_.orders.create(order) + .then(async salesOrderId => { + const order = await this.brightpearl_.orders.retrieve(salesOrderId) + const resResult = await this.brightpearl_.warehouses.createReservation(order, this.options.warehouse) + return salesOrderId + }) + .then(async salesOrderId => { + const paymentMethod = fromOrder.payment_method + const paymentType = "AUTH" + const payment = { + transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref + transactionCode: fromOrder._id, + paymentMethodCode: "1220", + orderId: salesOrderId, + currencyIsoCode: fromOrder.currency_code, + paymentDate: new Date(), + paymentType, + } + + // Only if authorization type + if (paymentType === "AUTH") { + const today = new Date() + const authExpire = today.setDate(today.getDate() + 7) + payment.amountAuthorized = await this.totalsService_.getTotal(fromOrder) + payment.authorizationExpiry = new Date(authExpire) + } else { + // For captured + } + + await this.brightpearl_.payments.create(payment) + + return salesOrderId + }) + .then((salesOrderId) => { + return this.orderService_.setMetadata(fromOrder._id, "brightpearl_sales_order_id", salesOrderId) + }) + } + + async createCapturedPayment(fromOrder) { + const soId = fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id + if (!soId) { + return + } + + const paymentType = "CAPTURE" + const paymentMethod = fromOrder.payment_method + const payment = { + transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref + transactionCode: fromOrder._id, + paymentMethodCode: "1220", + orderId: soId, + paymentDate: new Date(), + currencyIsoCode: fromOrder.currency_code, + amountPaid: await this.totalsService_.getTotal(fromOrder), + paymentType, + } + + await this.brightpearl_.payments.create(payment) + } + + async getBrightpearlRows(fromOrder) { + const region = await this.regionService_.retrieve(fromOrder.region_id) + const discount = fromOrder.discounts.find(({ discount_rule }) => discount_rule.type !== "free_shipping") + let lineDiscounts = [] + if (discount) { + lineDiscounts = this.discountService_.getLineDiscounts(fromOrder, discount) + } + + const lines = await Promise.all(fromOrder.items.map(async item => { + const bpProduct = await this.retrieveProductBySKU(item.content.variant.sku) + + const discount = lineDiscounts.find(l => l.item._id.equals(item._id)) || { amount: 0 } + + const row = {} + if (bpProduct) { + row.productId = bpProduct.productId + } else { + row.name = item.title + } + row.net = item.content.unit_price * item.quantity - discount.amount + row.tax = row.net * fromOrder.tax_rate + row.quantity = item.quantity + row.taxCode = region.tax_code + row.externalRef = item._id + row.nominalCode = this.options.sales_account_code || "4000" + + return row + })) + + const shippingTotal = this.totalsService_.getShippingTotal(fromOrder) + const shippingMethods = fromOrder.shipping_methods + if (shippingMethods.length > 0) { + lines.push({ + name: `Shipping: ${shippingMethods.map(m => m.name).join(" + ")}`, + quantity: 1, + net: shippingTotal, + tax: shippingTotal * fromOrder.tax_rate, + taxCode: region.tax_code, + nominalCode: this.options.shipping_account_code || "4040", + }) + } + return lines + } + + retrieveCustomerByEmail(email) { + return this.brightpearl_.customers.retrieveByEmail(email).then(customers => { + if (!customers.length) { + return null + } + return customers.find(c => c.primaryEmail === email) + }) + } + + retrieveProductBySKU(sku) { + return this.brightpearl_.products.retrieveBySKU(sku).then(products => { + if (!products.length) { + return null + } + return products[0] + }) + } + + async createCustomer(fromOrder) { + const address = await this.brightpearl_.addresses.create({ + addressLine1: fromOrder.shipping_address.address_1, + addressLine2: fromOrder.shipping_address.address_2, + postalCode: fromOrder.shipping_address.postal_code, + countryIsoCode: fromOrder.shipping_address.country_code, + }) + + const customer = await this.brightpearl_.customers.create({ + firstName: fromOrder.shipping_address.first_name, + lastName: fromOrder.shipping_address.last_name, + postAddressIds: { + DEF: address, + BIL: address, + DEL: address, + } + }) + + return { contactId: customer } + } +} + +export default BrightpearlService diff --git a/packages/medusa-plugin-brightpearl/src/subscribers/order.js b/packages/medusa-plugin-brightpearl/src/subscribers/order.js new file mode 100644 index 0000000000..4b9bd7856b --- /dev/null +++ b/packages/medusa-plugin-brightpearl/src/subscribers/order.js @@ -0,0 +1,31 @@ +class OrderSubscriber { + constructor({ eventBusService, orderService, brightpearlService }) { + this.orderService_ = orderService + this.brightpearlService_ = brightpearlService + + eventBusService.subscribe("order.placed", this.sendToBrightpearl) + eventBusService.subscribe("order.payment_captured", this.registerCapturedPayment) + eventBusService.subscribe("order.shipment_created", this.registerShipment) + } + + sendToBrightpearl = order => { + return this.brightpearlService_.createSalesOrder(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) + await this.brightpearlService_.registerGoodsOutShipped(noteId, shipment) + } + } +} + +export default OrderSubscriber diff --git a/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js b/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js new file mode 100644 index 0000000000..effdcbe728 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js @@ -0,0 +1,166 @@ +import axios from "axios" + +class BrightpearlClient { + constructor(options) { + this.client_ = axios.create({ + baseURL: `https://${options.datacenter}.brightpearl.com/public-api/${options.account}`, + headers: { + 'brightpearl-app-ref': options.app_ref, + 'brightpearl-account-token': options.token + }, + }) + + this.payments = this.buildPaymentEndpoints() + this.warehouses = this.buildWarehouseEndpoints() + this.orders = this.buildOrderEndpoints() + this.addresses = this.buildAddressEndpoints() + this.customers = this.buildCustomerEndpoints() + this.products = this.buildProductEndpoints() + } + + buildSearchResults_(response) { + const { results, metaData } = response + // Map the column names to the columns + return results.map(resColumns => { + const object = {} + for (let i = 0; i < resColumns.length; i++) { + const fieldName = metaData.columns[i].name + object[fieldName] = resColumns[i] + } + return object + }) + } + + buildPaymentEndpoints = () => { + return { + create: (payment) => { + return this.client_.request({ + url: `/accounting-service/customer-payment`, + method: "POST", + data: payment + }) + .then(({ data }) => data.response) + } + } + } + + buildWarehouseEndpoints = () => { + return { + retrieveReservation: (orderId) => { + return this.client_.request({ + url: `/warehouse-service/order/${orderId}/reservation`, + method: "GET", + }) + .then(({ data }) => data.response) + }, + createGoodsOutNote: (orderId, data) => { + return this.client_.request({ + url: `/warehouse-service/order/${orderId}/goods-note/goods-out`, + method: "POST", + data, + }).then(({ data }) => data.response) + }, + updateGoodsOutNote: (noteId, update) => { + return this.client_.request({ + url: `/warehouse-service/goods-note/goods-out/${noteId}`, + method: "PUT", + data: update + }) + }, + registerGoodsOutEvent: (noteId, data) => { + return this.client_.request({ + url: `/warehouse-service/goods-note/goods-out/${noteId}/event`, + method: "POST", + data + }) + }, + createReservation: (order, warehouse) => { + const id = order.id + const data = order.rows.map(r => ({ + productId: r.productId, + salesOrderRowId: r.id, + quantity: r.quantity + })) + return this.client_.request({ + url: `/warehouse-service/order/${id}/reservation/warehouse/${warehouse}`, + method: "POST", + data: { + products: data + } + }).then(({ data }) => data.response) + } + } + } + + buildOrderEndpoints = () => { + return { + retrieve: (orderId) => { + return this.client_.request({ + url: `/order-service/sales-order/${orderId}`, + method: "GET", + }) + .then(({ data }) => data.response.length && data.response[0]) + .catch(err => console.log(err)) + }, + create: (order) => { + return this.client_.request({ + url: `/order-service/sales-order`, + method: "POST", + data: order + }) + .then(({ data }) => data.response) + }, + } + } + + buildAddressEndpoints = () => { + return { + create: (address) => { + return this.client_.request({ + url: `/contact-service/postal-address`, + method: "POST", + data: address + }) + .then(({ data }) => data.response) + } + } + } + + buildProductEndpoints = () => { + return { + retrieveBySKU: (sku) => { + return this.client_.request({ + url: `/product-service/product-search?SKU=${sku}`, + }) + .then(({ data }) => { + return this.buildSearchResults_(data.response) + }) + } + } + } + + + buildCustomerEndpoints = () => { + return { + retrieveByEmail: (email) => { + return this.client_.request({ + url: `/contact-service/contact-search?primaryEmail=${email}`, + }) + .then(({data }) => { + return this.buildSearchResults_(data.response) + }) + }, + + create: (customerData) => { + return this.client_.request({ + url: `/contact-service/contact`, + method: "POST", + data: customerData + }) + .then(({ data }) => data.response) + } + } + } +} + +export default BrightpearlClient diff --git a/packages/medusa-plugin-brightpearl/utils/brightpearl.js b/packages/medusa-plugin-brightpearl/utils/brightpearl.js new file mode 100644 index 0000000000..279209cf18 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/utils/brightpearl.js @@ -0,0 +1,216 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +var _axios = _interopRequireDefault(require("axios")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } + +function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } } + +function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +var BrightpearlClient = /*#__PURE__*/function () { + function BrightpearlClient(options) { + var _this = this; + + _classCallCheck(this, BrightpearlClient); + + _defineProperty(this, "buildPaymentEndpoints", function () { + return { + create: function create(payment) { + return _this.client_.request({ + url: "/accounting-service/customer-payment", + method: "POST", + data: payment + }).then(function (_ref) { + var data = _ref.data; + return data.response; + }); + } + }; + }); + + _defineProperty(this, "buildWarehouseEndpoints", function () { + return { + retrieveReservation: function retrieveReservation(orderId) { + return _this.client_.request({ + url: "/warehouse-service/order/".concat(orderId, "/reservation"), + method: "GET" + }).then(function (_ref2) { + var data = _ref2.data; + return data.response; + }); + }, + 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 (_ref3) { + var data = _ref3.data; + return data.response; + }); + }, + updateGoodsOutNote: function updateGoodsOutNote(noteId, update) { + return _this.client_.request({ + url: "/warehouse-service/goods-note/goods-out/".concat(noteId), + method: "PUT", + data: update + }); + }, + registerGoodsOutEvent: function registerGoodsOutEvent(noteId, data) { + return _this.client_.request({ + url: "/warehouse-service/goods-note/goods-out/".concat(noteId, "/event"), + method: "POST", + data: data + }); + }, + createReservation: function createReservation(order, warehouse) { + var id = order.id; + var data = order.rows.map(function (r) { + return { + productId: r.productId, + salesOrderRowId: r.id, + quantity: r.quantity + }; + }); + return _this.client_.request({ + url: "/warehouse-service/order/".concat(id, "/reservation/warehouse/").concat(warehouse), + method: "POST", + data: { + products: data + } + }).then(function (_ref4) { + var data = _ref4.data; + return data.response; + }); + } + }; + }); + + _defineProperty(this, "buildOrderEndpoints", function () { + return { + retrieve: function retrieve(orderId) { + return _this.client_.request({ + url: "/order-service/sales-order/".concat(orderId), + method: "GET" + }).then(function (_ref5) { + var data = _ref5.data; + return data.response.length && data.response[0]; + })["catch"](function (err) { + return console.log(err); + }); + }, + create: function create(order) { + return _this.client_.request({ + url: "/order-service/sales-order", + method: "POST", + data: order + }).then(function (_ref6) { + var data = _ref6.data; + return data.response; + }); + } + }; + }); + + _defineProperty(this, "buildAddressEndpoints", function () { + return { + create: function create(address) { + return _this.client_.request({ + url: "/contact-service/postal-address", + method: "POST", + data: address + }).then(function (_ref7) { + var data = _ref7.data; + return data.response; + }); + } + }; + }); + + _defineProperty(this, "buildProductEndpoints", function () { + return { + retrieveBySKU: function retrieveBySKU(sku) { + return _this.client_.request({ + url: "/product-service/product-search?SKU=".concat(sku) + }).then(function (_ref8) { + var data = _ref8.data; + return _this.buildSearchResults_(data.response); + }); + } + }; + }); + + _defineProperty(this, "buildCustomerEndpoints", function () { + return { + retrieveByEmail: function retrieveByEmail(email) { + return _this.client_.request({ + url: "/contact-service/contact-search?primaryEmail=".concat(email) + }).then(function (_ref9) { + var data = _ref9.data; + return _this.buildSearchResults_(data.response); + }); + }, + create: function create(customerData) { + return _this.client_.request({ + url: "/contact-service/contact", + method: "POST", + data: customerData + }).then(function (_ref10) { + var data = _ref10.data; + return data.response; + }); + } + }; + }); + + this.client_ = _axios["default"].create({ + baseURL: "https://".concat(options.datacenter, ".brightpearl.com/public-api/").concat(options.account), + headers: { + 'brightpearl-app-ref': options.app_ref, + 'brightpearl-account-token': options.token + } + }); + this.payments = this.buildPaymentEndpoints(); + this.warehouses = this.buildWarehouseEndpoints(); + this.orders = this.buildOrderEndpoints(); + this.addresses = this.buildAddressEndpoints(); + this.customers = this.buildCustomerEndpoints(); + this.products = this.buildProductEndpoints(); + } + + _createClass(BrightpearlClient, [{ + key: "buildSearchResults_", + value: function buildSearchResults_(response) { + var results = response.results, + metaData = response.metaData; // Map the column names to the columns + + return results.map(function (resColumns) { + var object = {}; + + for (var i = 0; i < resColumns.length; i++) { + var fieldName = metaData.columns[i].name; + object[fieldName] = resColumns[i]; + } + + return object; + }); + } + }]); + + return BrightpearlClient; +}(); + +var _default = BrightpearlClient; +exports["default"] = _default; \ No newline at end of file diff --git a/packages/medusa-plugin-segment/src/services/segment.js b/packages/medusa-plugin-segment/src/services/segment.js index 1cbb9bbd16..82994fb06b 100644 --- a/packages/medusa-plugin-segment/src/services/segment.js +++ b/packages/medusa-plugin-segment/src/services/segment.js @@ -50,7 +50,6 @@ class SegmentService extends BaseService { async buildOrder(order) { - console.log("build", order) const subtotal = await this.totalsService_.getSubtotal(order) const total = await this.totalsService_.getTotal(order) const tax = await this.totalsService_.getTaxTotal(order) diff --git a/packages/medusa/src/api/index.js b/packages/medusa/src/api/index.js index 4d0b4b9002..b710f21d44 100644 --- a/packages/medusa/src/api/index.js +++ b/packages/medusa/src/api/index.js @@ -7,6 +7,39 @@ import errorHandler from "./middlewares/error-handler" export default (container, config) => { const app = Router() + app.post("/create-shipment/:order_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", + }) + + res.sendStatus(200) + }) + + app.post("/run-hook/:order_id/capture", async (req, res) => { + const orderService = req.scope.resolve("orderService") + const eventBus = req.scope.resolve("eventBusService") + const order = await orderService.retrieve(req.params.order_id) + + eventBus.emit("order.payment_captured", order) + + res.sendStatus(200) + }) + + app.post("/run-hook/:order_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) + + eventBus.emit("order.placed", order) + + res.sendStatus(200) + }) + admin(app, container, config) store(app, container, config) diff --git a/packages/medusa/src/models/order.js b/packages/medusa/src/models/order.js index f0db2b003c..41190fcd10 100644 --- a/packages/medusa/src/models/order.js +++ b/packages/medusa/src/models/order.js @@ -6,6 +6,7 @@ import PaymentMethodSchema from "./schemas/payment-method" import ShippingMethodSchema from "./schemas/shipping-method" import AddressSchema from "./schemas/address" import DiscountSchema from "./schemas/discount" +import ShipmentSchema from "./schemas/shipment" class OrderModel extends BaseModel { static modelName = "Order" @@ -23,6 +24,8 @@ class OrderModel extends BaseModel { shipping_address: { type: AddressSchema, required: true }, items: { type: [LineItemSchema], required: true }, currency_code: { type: String, required: true }, + tax_rate: { type: Number, required: true }, + shipments: { type: [ShipmentSchema], default: [] }, region_id: { type: String, required: true }, discounts: { type: [DiscountSchema], default: [] }, customer_id: { type: String }, diff --git a/packages/medusa/src/models/region.js b/packages/medusa/src/models/region.js index aa13126369..97ee74a03e 100644 --- a/packages/medusa/src/models/region.js +++ b/packages/medusa/src/models/region.js @@ -7,6 +7,7 @@ class RegionModel extends BaseModel { name: { type: String, required: true }, currency_code: { type: String, required: true }, tax_rate: { type: Number, required: true, default: 0 }, + tax_code: { type: String }, countries: { type: [String], default: [] }, payment_providers: { type: [String], default: [] }, fulfillment_providers: { type: [String], default: [] }, diff --git a/packages/medusa/src/models/schemas/shipment.js b/packages/medusa/src/models/schemas/shipment.js new file mode 100644 index 0000000000..e21b5dc924 --- /dev/null +++ b/packages/medusa/src/models/schemas/shipment.js @@ -0,0 +1,7 @@ +import mongoose from "mongoose" + +export default new mongoose.Schema({ + item_ids: { type: [String], required: true }, + tracking_number: { type: String, default: "" }, + metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, +}) diff --git a/packages/medusa/src/services/order.js b/packages/medusa/src/services/order.js index d1f385e8e7..c261b0cb44 100644 --- a/packages/medusa/src/services/order.js +++ b/packages/medusa/src/services/order.js @@ -5,6 +5,8 @@ import { BaseService } from "medusa-interfaces" class OrderService extends BaseService { static Events = { GIFT_CARD_CREATED: "order.gift_card_created", + PAYMENT_CAPTURED: "order.payment_captured", + SHIPMENT_CREATED: "order.shipment_created", PLACED: "order.placed", UPDATED: "order.updated", CANCELLED: "order.cancelled", @@ -352,6 +354,7 @@ class OrderService extends BaseService { email: cart.email, customer_id: cart.customer_id, cart_id: cart._id, + tax_rate: region.tax_rate, currency_code: region.currency_code, } @@ -366,6 +369,58 @@ class OrderService extends BaseService { .then(() => this.orderModel_.findOne({ cart_id: cart._id })) } + /** + * Adds a shipment to the order to indicate that an order has left the warehouse + */ + async createShipment(orderId, shipment) { + 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(), + }) + + 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 result + }) + } + /** * Creates an order * @param {object} order - the order to create @@ -535,14 +590,19 @@ class OrderService extends BaseService { await paymentProvider.capturePayment(data) - return this.orderModel_.updateOne( - { - _id: orderId, - }, - { - $set: updateFields, - } - ) + return this.orderModel_ + .updateOne( + { + _id: orderId, + }, + { + $set: updateFields, + } + ) + .then(result => { + this.eventBus_.emit(OrderService.Events.PAYMENT_CAPTURED, result) + return result + }) } /** diff --git a/packages/medusa/src/services/totals.js b/packages/medusa/src/services/totals.js index 09289df137..40ee9d98ce 100644 --- a/packages/medusa/src/services/totals.js +++ b/packages/medusa/src/services/totals.js @@ -252,6 +252,45 @@ class TotalsService extends BaseService { return discounts } + async getLineDiscounts(cart, discount) { + const subtotal = this.getSubtotal(cart) + const { type, allocation, value } = discount.discount_rule + if (allocation === "total") { + let percentage = 0 + if (type === "percentage") { + percentage = value / 100 + } else if (type === "fixed") { + percentage = value / subtotal + } + + return cart.items.map(item => { + const lineTotal = item.content.unit_price * item.quantity + + return { + item, + amount: lineTotal * percentage, + } + }) + } else if (allocation === "item") { + const allocationDiscounts = this.getAllocationItemDiscounts( + discount, + cart, + type + ) + return cart.items.map(item => { + const discounted = allocationDiscounts.find(a => + a.lineItem._id.equals(item._id) + ) + return { + item, + amount: !!discounted ? discounted.amount : 0, + } + }) + } + + return cart.items.map(i => ({ item: i, amount: 0 })) + } + /** * Calculates the total discount amount for each of the different supported * discount types. If discounts aren't present or invalid returns 0. diff --git a/packages/medusa/src/subscribers/order.js b/packages/medusa/src/subscribers/order.js index 81f8f708c6..3f63381c65 100644 --- a/packages/medusa/src/subscribers/order.js +++ b/packages/medusa/src/subscribers/order.js @@ -39,7 +39,11 @@ class OrderSubscriber { }) this.eventBus_.subscribe("order.placed", async order => { - await this.cartService_.delete(order.cart_id) + await this.cartService_.delete(order.cart_id).catch(err => { + if (err.type !== "not_found") { + throw err + } + }) }) this.eventBus_.subscribe("order.placed", this.handleDiscounts) From e69c3aba01fc5498c50d7476b854ec6170157ff0 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Mon, 3 Aug 2020 14:05:56 +0200 Subject: [PATCH 3/5] creates webhook and cron job to update inventory --- .../loaders/inventory.js | 17 ++++ .../loaders/webhooks.js | 37 +++++++++ .../src/api/index.js | 15 ++++ .../src/loaders/inventory.js | 8 ++ .../src/loaders/webhooks.js | 6 ++ .../src/services/brightpearl.js | 66 +++++++++++++++- .../src/utils/brightpearl.js | 32 ++++++++ .../utils/brightpearl.js | 78 ++++++++++++++----- packages/medusa/src/loaders/plugins.js | 25 +++++- packages/medusa/src/services/event-bus.js | 55 +++++++++++++ 10 files changed, 317 insertions(+), 22 deletions(-) create mode 100644 packages/medusa-plugin-brightpearl/loaders/inventory.js create mode 100644 packages/medusa-plugin-brightpearl/loaders/webhooks.js create mode 100644 packages/medusa-plugin-brightpearl/src/api/index.js create mode 100644 packages/medusa-plugin-brightpearl/src/loaders/inventory.js create mode 100644 packages/medusa-plugin-brightpearl/src/loaders/webhooks.js diff --git a/packages/medusa-plugin-brightpearl/loaders/inventory.js b/packages/medusa-plugin-brightpearl/loaders/inventory.js new file mode 100644 index 0000000000..c534686a7d --- /dev/null +++ b/packages/medusa-plugin-brightpearl/loaders/inventory.js @@ -0,0 +1,17 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +var inventorySync = function inventorySync(container) { + var brightpearlService = container.resolve("brightpearlService"); + var eventBus = container.resolve("eventBusService"); + var pattern = "43 4,10,14,20 * * *"; // nice for tests "*/10 * * * * *" + + eventBus.createCronJob("inventory-sync", {}, pattern, brightpearlService.syncInventory()); +}; + +var _default = inventorySync; +exports["default"] = _default; \ No newline at end of file diff --git a/packages/medusa-plugin-brightpearl/loaders/webhooks.js b/packages/medusa-plugin-brightpearl/loaders/webhooks.js new file mode 100644 index 0000000000..2373540143 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/loaders/webhooks.js @@ -0,0 +1,37 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } + +function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } + +var webhookLoader = /*#__PURE__*/function () { + var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(container) { + var brightpearlService; + return regeneratorRuntime.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + brightpearlService = container.resolve("brightpearlService"); + _context.next = 3; + return brightpearlService.verifyWebhooks(); + + case 3: + case "end": + return _context.stop(); + } + } + }, _callee); + })); + + return function webhookLoader(_x) { + return _ref.apply(this, arguments); + }; +}(); + +var _default = webhookLoader; +exports["default"] = _default; \ No newline at end of file diff --git a/packages/medusa-plugin-brightpearl/src/api/index.js b/packages/medusa-plugin-brightpearl/src/api/index.js new file mode 100644 index 0000000000..c220e73099 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/src/api/index.js @@ -0,0 +1,15 @@ +import { Router } from "express" +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 + const brightpearlService = req.scope.resolve("brightpearlService") + await brightpearlService.updateInventory(id) + res.sendStatus(200) + }) + + return app +} diff --git a/packages/medusa-plugin-brightpearl/src/loaders/inventory.js b/packages/medusa-plugin-brightpearl/src/loaders/inventory.js new file mode 100644 index 0000000000..00f3751e02 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/src/loaders/inventory.js @@ -0,0 +1,8 @@ +const inventorySync = container => { + const brightpearlService = container.resolve("brightpearlService") + const eventBus = container.resolve("eventBusService") + const pattern = "43 4,10,14,20 * * *" // nice for tests "*/10 * * * * *" + eventBus.createCronJob("inventory-sync", {}, pattern, brightpearlService.syncInventory()) +} + +export default inventorySync diff --git a/packages/medusa-plugin-brightpearl/src/loaders/webhooks.js b/packages/medusa-plugin-brightpearl/src/loaders/webhooks.js new file mode 100644 index 0000000000..ea78833176 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/src/loaders/webhooks.js @@ -0,0 +1,6 @@ +const webhookLoader = async (container) => { + const brightpearlService = container.resolve("brightpearlService") + await brightpearlService.verifyWebhooks() +} + +export default webhookLoader diff --git a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js index ade9a2b93a..41ef40b996 100644 --- a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js @@ -2,10 +2,11 @@ import { BaseService } from "medusa-interfaces" import Brightpearl from "../utils/brightpearl" class BrightpearlService extends BaseService { - constructor({ totalsService, regionService, orderService, discountService }, options) { + constructor({ totalsService, productVariantService, regionService, orderService, discountService }, options) { super() this.options = options + this.productVariantService_ = productVariantService this.regionService_ = regionService this.orderService_ = orderService this.totalsService_ = totalsService @@ -19,6 +20,69 @@ class BrightpearlService extends BaseService { }) } + async verifyWebhooks() { + const hooks = [ + { + 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}" }', + contentType: "application/json", + idSetAccepted: false, + } + ] + + const installedHooks = await this.brightpearl_.webhooks.list().catch(() => []) + for (const hook of hooks) { + const isInstalled = installedHooks.find(i => + i.subscribeTo === hook.subscribeTo && + i.httpMethod === hook.httpMethod && + i.uriTemplate === hook.uriTemplate && + i.bodyTemplate === hook.bodyTemplate && + i.contentType === hook.contentType && + i.idSetAccepted === hook.idSetAccepted + ) + + if (!isInstalled) { + await this.brightpearl_.webhooks.create(hook) + } + } + } + + async syncInventory() { + const variants = await this.productVariantService_.list() + return Promise.all(variants.map(async v => { + const brightpearlProduct = await this.retrieveProductBySKU(v.sku) + if (!brightpearlProduct) { + return + } + + const { productId } = brightpearlProduct + const availability = await this.brightpearl_.products.retrieveAvailability(productId) + const onHand = availability[productId].total.onHand + + return this.productVariantService_.update(v._id, { + inventory_quantity: onHand + }) + })) + } + + async updateInventory(productId) { + const brightpearlProduct = await this.brightpearl_.products.retrieve(productId) + const availability = await this.brightpearl_.products.retrieveAvailability(productId) + + const onHand = availability[productId].total.onHand + + 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 + }) + } + } + async createGoodsOutNote(fromOrder, shipment) { const id = fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id diff --git a/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js b/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js index effdcbe728..2a0c87ae61 100644 --- a/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js @@ -10,6 +10,7 @@ class BrightpearlClient { }, }) + this.webhooks = this.buildWebhookEndpoints() this.payments = this.buildPaymentEndpoints() this.warehouses = this.buildWarehouseEndpoints() this.orders = this.buildOrderEndpoints() @@ -31,6 +32,25 @@ class BrightpearlClient { }) } + buildWebhookEndpoints = () => { + return { + list: () => { + return this.client_.request({ + url: `/integration-service/webhook`, + method: "GET", + }) + .then(({ data }) => data.response) + }, + create: (data) => { + return this.client_.request({ + url: `/integration-service/webhook`, + method: "POST", + data + }) + } + } + } + buildPaymentEndpoints = () => { return { create: (payment) => { @@ -128,6 +148,18 @@ class BrightpearlClient { buildProductEndpoints = () => { return { + retrieveAvailability: productId => { + return this.client_.request({ + url: `/warehouse-service/product-availability/${productId}`, + }) + .then(({ data }) => data.response && data.response) + }, + retrieve: (productId) => { + return this.client_.request({ + url: `/product-service/product/${productId}`, + }) + .then(({ data }) => data.response && data.response[0]) + }, retrieveBySKU: (sku) => { return this.client_.request({ url: `/product-service/product-search?SKU=${sku}`, diff --git a/packages/medusa-plugin-brightpearl/utils/brightpearl.js b/packages/medusa-plugin-brightpearl/utils/brightpearl.js index 279209cf18..8e988a96c0 100644 --- a/packages/medusa-plugin-brightpearl/utils/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/utils/brightpearl.js @@ -25,6 +25,27 @@ var BrightpearlClient = /*#__PURE__*/function () { _classCallCheck(this, BrightpearlClient); + _defineProperty(this, "buildWebhookEndpoints", function () { + return { + list: function list() { + return _this.client_.request({ + url: "/integration-service/webhook", + method: "GET" + }).then(function (_ref) { + var data = _ref.data; + return data.response; + }); + }, + create: function create(data) { + return _this.client_.request({ + url: "/integration-service/webhook", + method: "POST", + data: data + }); + } + }; + }); + _defineProperty(this, "buildPaymentEndpoints", function () { return { create: function create(payment) { @@ -32,8 +53,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/accounting-service/customer-payment", method: "POST", data: payment - }).then(function (_ref) { - var data = _ref.data; + }).then(function (_ref2) { + var data = _ref2.data; return data.response; }); } @@ -46,8 +67,8 @@ var BrightpearlClient = /*#__PURE__*/function () { return _this.client_.request({ url: "/warehouse-service/order/".concat(orderId, "/reservation"), method: "GET" - }).then(function (_ref2) { - var data = _ref2.data; + }).then(function (_ref3) { + var data = _ref3.data; return data.response; }); }, @@ -56,8 +77,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/warehouse-service/order/".concat(orderId, "/goods-note/goods-out"), method: "POST", data: data - }).then(function (_ref3) { - var data = _ref3.data; + }).then(function (_ref4) { + var data = _ref4.data; return data.response; }); }, @@ -90,8 +111,8 @@ var BrightpearlClient = /*#__PURE__*/function () { data: { products: data } - }).then(function (_ref4) { - var data = _ref4.data; + }).then(function (_ref5) { + var data = _ref5.data; return data.response; }); } @@ -104,8 +125,8 @@ var BrightpearlClient = /*#__PURE__*/function () { return _this.client_.request({ url: "/order-service/sales-order/".concat(orderId), method: "GET" - }).then(function (_ref5) { - var data = _ref5.data; + }).then(function (_ref6) { + var data = _ref6.data; return data.response.length && data.response[0]; })["catch"](function (err) { return console.log(err); @@ -116,8 +137,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/order-service/sales-order", method: "POST", data: order - }).then(function (_ref6) { - var data = _ref6.data; + }).then(function (_ref7) { + var data = _ref7.data; return data.response; }); } @@ -131,8 +152,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/contact-service/postal-address", method: "POST", data: address - }).then(function (_ref7) { - var data = _ref7.data; + }).then(function (_ref8) { + var data = _ref8.data; return data.response; }); } @@ -141,11 +162,27 @@ var BrightpearlClient = /*#__PURE__*/function () { _defineProperty(this, "buildProductEndpoints", function () { return { + retrieveAvailability: function retrieveAvailability(productId) { + return _this.client_.request({ + url: "/warehouse-service/product-availability/".concat(productId) + }).then(function (_ref9) { + var data = _ref9.data; + return data.response && data.response; + }); + }, + retrieve: function retrieve(productId) { + return _this.client_.request({ + url: "/product-service/product/".concat(productId) + }).then(function (_ref10) { + var data = _ref10.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 (_ref8) { - var data = _ref8.data; + }).then(function (_ref11) { + var data = _ref11.data; return _this.buildSearchResults_(data.response); }); } @@ -157,8 +194,8 @@ var BrightpearlClient = /*#__PURE__*/function () { retrieveByEmail: function retrieveByEmail(email) { return _this.client_.request({ url: "/contact-service/contact-search?primaryEmail=".concat(email) - }).then(function (_ref9) { - var data = _ref9.data; + }).then(function (_ref12) { + var data = _ref12.data; return _this.buildSearchResults_(data.response); }); }, @@ -167,8 +204,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/contact-service/contact", method: "POST", data: customerData - }).then(function (_ref10) { - var data = _ref10.data; + }).then(function (_ref13) { + var data = _ref13.data; return data.response; }); } @@ -182,6 +219,7 @@ var BrightpearlClient = /*#__PURE__*/function () { 'brightpearl-account-token': options.token } }); + this.webhooks = this.buildWebhookEndpoints(); this.payments = this.buildPaymentEndpoints(); this.warehouses = this.buildWarehouseEndpoints(); this.orders = this.buildOrderEndpoints(); diff --git a/packages/medusa/src/loaders/plugins.js b/packages/medusa/src/loaders/plugins.js index cf6c9a6604..bcc2e92d4e 100644 --- a/packages/medusa/src/loaders/plugins.js +++ b/packages/medusa/src/loaders/plugins.js @@ -16,7 +16,7 @@ import { sync as existsSync } from "fs-exists-cached" /** * Registers all services in the services directory */ -export default ({ rootDirectory, container, app }) => { +export default async ({ rootDirectory, container, app }) => { const { configModule, configFilePath } = getConfigFile( rootDirectory, `medusa-config` @@ -55,6 +55,29 @@ export default ({ rootDirectory, container, app }) => { registerCoreRouters(pluginDetails, container) registerSubscribers(pluginDetails, container) }) + + await Promise.all( + resolved.map(async pluginDetails => runLoaders(pluginDetails, container)) + ) +} + +async function runLoaders(pluginDetails, container) { + const loaderFiles = glob.sync( + `${pluginDetails.resolve}/loaders/[!__]*.js`, + {} + ) + await Promise.all( + loaderFiles.map(async loader => { + try { + const module = require(loader).default + if (typeof module === "function") { + await module(container) + } + } catch (err) { + return Promise.resolve() + } + }) + ) } function registerMedusaApi(pluginDetails, container) { diff --git a/packages/medusa/src/services/event-bus.js b/packages/medusa/src/services/event-bus.js index 29f430746a..52fb3ede4d 100644 --- a/packages/medusa/src/services/event-bus.js +++ b/packages/medusa/src/services/event-bus.js @@ -13,11 +13,20 @@ class EventBusService { /** @private {object} */ this.observers_ = {} + /** @private {object} to handle cron jobs */ + this.cronHandlers_ = {} + + /** @private {BullQueue} used for cron jobs */ + this.cronQueue_ = new Bull(`cron-jobs:queue`, config.redisURI) + /** @private {BullQueue} */ this.queue_ = new Bull(`${this.constructor.name}:queue`, config.redisURI) // Register our worker to handle emit calls this.queue_.process(this.worker_) + + // Register cron worker + this.cronQueue_.process(this.cronWorker_) } /** @@ -38,6 +47,21 @@ class EventBusService { } } + /** + * + */ + registerCronHandler_(event, subscriber) { + if (typeof subscriber !== "function") { + throw new Error("Handler must be a function") + } + + if (this.observers_[event]) { + this.cronHandlers_[event].push(subscriber) + } else { + this.cronHandlers_[event] = [subscriber] + } + } + /** * Calls all subscribers when an event occurs. * @param {string} eventName - the name of the event to be process. @@ -77,6 +101,37 @@ class EventBusService { }) ) } + + cronWorker_ = job => { + const { eventName, data } = job.data + const observers = this.cronHandlers_[eventName] || [] + this.logger_.info(`Processing cron job: ${eventName}`) + + return Promise.all( + observers.map(subscriber => { + return subscriber(data).catch(err => { + this.logger_.warn( + `An error occured while processing ${eventName}: ${err}` + ) + return err + }) + }) + ) + } + + /** + * Registers a cron job. + */ + createCronJob(eventName, data, cron, handler) { + this.registerCronHandler(eventName, handler) + return this.cronQueue_.add( + { + eventName, + data, + }, + { repeat: { cron } } + ) + } } export default EventBusService From 21bc096b2e0e8f429374eafc19f1085c66e4325f Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Tue, 4 Aug 2020 17:13:47 +0200 Subject: [PATCH 4/5] Adds Oauth support to plugins --- .../dist/services/manual-fulfillment.js | 18 +- .../medusa-fulfillment-manual/package.json | 4 +- packages/medusa-interfaces/src/index.js | 1 + .../medusa-interfaces/src/oauth-service.js | 25 ++ .../medusa-plugin-brightpearl/package.json | 6 +- .../src/services/brightpearl.js | 247 +++++++++++------- .../src/services/oauth.js | 40 +++ .../src/utils/brightpearl.js | 211 ++++++++------- .../utils/brightpearl.js | 85 ++++-- .../package.json | 1 + .../yarn.lock | 2 +- packages/medusa-plugin-economic/yarn.lock | 201 +------------- .../yarn.lock | 201 +------------- .../api/routes/admin/apps/authorize-app.js | 25 ++ .../medusa/src/api/routes/admin/apps/index.js | 16 ++ .../medusa/src/api/routes/admin/apps/list.js | 12 + packages/medusa/src/api/routes/admin/index.js | 2 + packages/medusa/src/loaders/plugins.js | 135 ++++++---- packages/medusa/src/models/oauth.js | 16 ++ packages/medusa/src/services/oauth.js | 110 ++++++++ packages/medusa/yarn.lock | 15 -- 21 files changed, 688 insertions(+), 685 deletions(-) create mode 100644 packages/medusa-interfaces/src/oauth-service.js create mode 100644 packages/medusa-plugin-brightpearl/src/services/oauth.js create mode 100644 packages/medusa/src/api/routes/admin/apps/authorize-app.js create mode 100644 packages/medusa/src/api/routes/admin/apps/index.js create mode 100644 packages/medusa/src/api/routes/admin/apps/list.js create mode 100644 packages/medusa/src/models/oauth.js create mode 100644 packages/medusa/src/services/oauth.js diff --git a/packages/medusa-fulfillment-manual/dist/services/manual-fulfillment.js b/packages/medusa-fulfillment-manual/dist/services/manual-fulfillment.js index 6ffcf58127..5efa9f900a 100644 --- a/packages/medusa-fulfillment-manual/dist/services/manual-fulfillment.js +++ b/packages/medusa-fulfillment-manual/dist/services/manual-fulfillment.js @@ -5,9 +5,7 @@ Object.defineProperty(exports, "__esModule", { }); exports["default"] = void 0; -var _medusaInterfaces = _interopRequireDefault(require("medusa-interfaces")); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } +var _medusaInterfaces = require("medusa-interfaces"); function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } } @@ -23,7 +21,7 @@ function _inherits(subClass, superClass) { if (typeof superClass !== "function" function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } -function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function () { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } +function _createSuper(Derived) { return function () { var Super = _getPrototypeOf(Derived), result; if (_isNativeReflectConstruct()) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } @@ -35,8 +33,8 @@ function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.g function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -var ManualFulfillmentService = /*#__PURE__*/function (_BaseFulfillmentServi) { - _inherits(ManualFulfillmentService, _BaseFulfillmentServi); +var ManualFulfillmentService = /*#__PURE__*/function (_FulfillmentService) { + _inherits(ManualFulfillmentService, _FulfillmentService); var _super = _createSuper(ManualFulfillmentService); @@ -61,11 +59,7 @@ var ManualFulfillmentService = /*#__PURE__*/function (_BaseFulfillmentServi) { }, { key: "validateOption", value: function validateOption(data) { - if (data.id === "manual-fulfillment") { - return true; - } - - return false; + return true; } }, { key: "canCalculate", @@ -86,7 +80,7 @@ var ManualFulfillmentService = /*#__PURE__*/function (_BaseFulfillmentServi) { }]); return ManualFulfillmentService; -}(_medusaInterfaces["default"]); +}(_medusaInterfaces.FulfillmentService); _defineProperty(ManualFulfillmentService, "identifier", "manual"); diff --git a/packages/medusa-fulfillment-manual/package.json b/packages/medusa-fulfillment-manual/package.json index a6fdd9c7a6..6b30f4e652 100644 --- a/packages/medusa-fulfillment-manual/package.json +++ b/packages/medusa-fulfillment-manual/package.json @@ -1,6 +1,6 @@ { "name": "medusa-fulfillment-manual", - "version": "1.0", + "version": "1.0.0", "description": "A manual fulfillment provider for Medusa", "main": "index.js", "repository": { @@ -34,4 +34,4 @@ "express": "^4.17.1", "medusa-core-utils": "^0.3.0" } -} \ No newline at end of file +} diff --git a/packages/medusa-interfaces/src/index.js b/packages/medusa-interfaces/src/index.js index a5711e18ed..b8e739ddda 100644 --- a/packages/medusa-interfaces/src/index.js +++ b/packages/medusa-interfaces/src/index.js @@ -3,3 +3,4 @@ export { default as BaseModel } from "./base-model" export { default as PaymentService } from "./payment-service" export { default as FulfillmentService } from "./fulfillment-service" export { default as FileService } from "./file-service" +export { default as OauthService } from "./oauth-service" diff --git a/packages/medusa-interfaces/src/oauth-service.js b/packages/medusa-interfaces/src/oauth-service.js new file mode 100644 index 0000000000..6b4947b57a --- /dev/null +++ b/packages/medusa-interfaces/src/oauth-service.js @@ -0,0 +1,25 @@ +import BaseService from "./base-service" + +/** + * Interface for file connectors + * @interface + */ +class BaseOauthService extends BaseService { + constructor() { + super() + } + + generateToken() { + throw Error("generateToken must be overridden by the child class") + } + + refreshToken() { + throw Error("refreshToken must be overridden by the child class") + } + + destroyToken() { + throw Error("destroyToken must be overridden by the child class") + } +} + +export default BaseOauthService diff --git a/packages/medusa-plugin-brightpearl/package.json b/packages/medusa-plugin-brightpearl/package.json index 3add66aa9a..99c44f15ed 100644 --- a/packages/medusa-plugin-brightpearl/package.json +++ b/packages/medusa-plugin-brightpearl/package.json @@ -25,7 +25,8 @@ "cross-env": "^5.2.1", "eslint": "^6.8.0", "jest": "^25.5.2", - "medusa-test-utils": "^0.3.0" + "medusa-test-utils": "^0.3.0", + "prettier": "^2.0.5" }, "scripts": { "build": "babel src -d dist", @@ -37,6 +38,7 @@ "axios": "^0.19.2", "express": "^4.17.1", "medusa-core-utils": "^0.3.0", - "medusa-interfaces": "^0.3.0" + "medusa-interfaces": "^0.3.0", + "randomatic": "^3.1.1" } } diff --git a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js index 41ef40b996..50594f5e77 100644 --- a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js @@ -2,7 +2,17 @@ import { BaseService } from "medusa-interfaces" import Brightpearl from "../utils/brightpearl" class BrightpearlService extends BaseService { - constructor({ totalsService, productVariantService, regionService, orderService, discountService }, options) { + constructor( + { + oauthService, + totalsService, + productVariantService, + regionService, + orderService, + discountService, + }, + options + ) { super() this.options = options @@ -11,92 +21,122 @@ class BrightpearlService extends BaseService { this.orderService_ = orderService this.totalsService_ = totalsService this.discountService_ = discountService + this.oauthService_ = oauthService + } - this.brightpearl_ = new Brightpearl({ - account: options.account, - datacenter: options.datacenter, - app_ref: options.app_ref, - token: options.token + async getClient() { + if (this.brightpearlClient_) { + return this.brightpearlClient_ + } + + const authData = await this.oauthService_.retrieveByName("brightpearl") + const { data } = authData + + if (!data || !data.access_token) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "You must authenticate the Brightpearl app in settings before continuing" + ) + } + + const client = new Brightpearl({ + url: data.api_domain, + auth_type: data.token_type, + access_token: data.access_token, }) + + this.brightpearlClient_ = client + return client } async verifyWebhooks() { + const brightpearl = await this.getClient() const hooks = [ { 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}" }', + bodyTemplate: + '{"account": "${account-code}", "lifecycleEvent": "${lifecycle-event}", "resourceType": "${resource-type}", "id": "${resource-id}" }', contentType: "application/json", idSetAccepted: false, - } + }, ] - const installedHooks = await this.brightpearl_.webhooks.list().catch(() => []) + const installedHooks = await brightpearl.webhooks.list().catch(() => []) for (const hook of hooks) { - const isInstalled = installedHooks.find(i => - i.subscribeTo === hook.subscribeTo && - i.httpMethod === hook.httpMethod && - i.uriTemplate === hook.uriTemplate && - i.bodyTemplate === hook.bodyTemplate && - i.contentType === hook.contentType && - i.idSetAccepted === hook.idSetAccepted + const isInstalled = installedHooks.find( + (i) => + i.subscribeTo === hook.subscribeTo && + i.httpMethod === hook.httpMethod && + i.uriTemplate === hook.uriTemplate && + i.bodyTemplate === hook.bodyTemplate && + i.contentType === hook.contentType && + i.idSetAccepted === hook.idSetAccepted ) if (!isInstalled) { - await this.brightpearl_.webhooks.create(hook) + await brightpearl.webhooks.create(hook) } } } async syncInventory() { + const client = await this.getClient() const variants = await this.productVariantService_.list() - return Promise.all(variants.map(async v => { - const brightpearlProduct = await this.retrieveProductBySKU(v.sku) - if (!brightpearlProduct) { - return - } + return Promise.all( + variants.map(async (v) => { + const brightpearlProduct = await this.retrieveProductBySKU(v.sku) + if (!brightpearlProduct) { + return + } - const { productId } = brightpearlProduct - const availability = await this.brightpearl_.products.retrieveAvailability(productId) - const onHand = availability[productId].total.onHand - - return this.productVariantService_.update(v._id, { - inventory_quantity: onHand + const { productId } = brightpearlProduct + const availability = await client.products.retrieveAvailability( + productId + ) + const onHand = availability[productId].total.onHand + + return this.productVariantService_.update(v._id, { + inventory_quantity: onHand, + }) }) - })) + ) } async updateInventory(productId) { - const brightpearlProduct = await this.brightpearl_.products.retrieve(productId) - const availability = await this.brightpearl_.products.retrieveAvailability(productId) + const client = await this.getClient() + const brightpearlProduct = await client.products.retrieve(productId) + const availability = await client.products.retrieveAvailability(productId) const onHand = availability[productId].total.onHand const sku = brightpearlProduct.identity.sku - const [ variant ] = await this.productVariantService_.list({ sku }) + const [variant] = await this.productVariantService_.list({ sku }) if (variant && variant.manage_inventory) { await this.productVariantService_.update(variant._id, { - inventory_quantity: onHand + inventory_quantity: onHand, }) } } async createGoodsOutNote(fromOrder, shipment) { - const id = fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id + const client = await this.getClient() + const id = + fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id if (!id) { return } - const order = await this.brightpearl_.orders.retrieve(id) - const productRows = shipment.item_ids.map(id => { + const order = await client.orders.retrieve(id) + const productRows = shipment.item_ids.map((id) => { const row = order.rows.find(({ externalRef }) => externalRef === id) return { productId: row.productId, salesOrderRowId: row.id, - quantity: row.quantity + quantity: row.quantity, } }) @@ -107,36 +147,39 @@ class BrightpearlService extends BaseService { warehouseId: this.options.warehouse, transfer: false, products: productRows, - } + }, ], priority: false, } - return this.brightpearl_.warehouses.createGoodsOutNote(id, goodsOut) + return client.warehouses.createGoodsOutNote(id, goodsOut) } - registerGoodsOutShipped(noteId, shipment) { - return this.brightpearl_.warehouses.registerGoodsOutEvent(noteId, { + async registerGoodsOutShipped(noteId, shipment) { + const client = await this.getClient() + return client.warehouses.registerGoodsOutEvent(noteId, { events: [ { eventCode: "SHW", occured: new Date(), eventOwnerId: this.options.event_owner, - } - ] + }, + ], }) } - registerGoodsOutTrackingNumber(noteId, shipment) { - return this.brightpearl_.warehouses.updateGoodsOutNote(noteId, { + async registerGoodsOutTrackingNumber(noteId, shipment) { + const client = await this.getClient() + return client.warehouses.updateGoodsOutNote(noteId, { priority: false, shipping: { reference: shipment.tracking_number, - } + }, }) } async createSalesOrder(fromOrder) { + const client = await this.getClient() let customer = await this.retrieveCustomerByEmail(fromOrder.email) // All sales orders must have a customer @@ -147,7 +190,7 @@ class BrightpearlService extends BaseService { const { shipping_address } = fromOrder const order = { currency: { - code: fromOrder.currency_code + code: fromOrder.currency_code, }, externalRef: fromOrder._id, customer: { @@ -160,7 +203,7 @@ class BrightpearlService extends BaseService { countryIsoCode: shipping_address.country_code, telephone: shipping_address.phone, email: fromOrder.email, - } + }, }, delivery: { shippingMethodId: 0, @@ -172,18 +215,22 @@ class BrightpearlService extends BaseService { countryIsoCode: shipping_address.country_code, telephone: shipping_address.phone, email: fromOrder.email, - } + }, }, - rows: await this.getBrightpearlRows(fromOrder) + rows: await this.getBrightpearlRows(fromOrder), } - return this.brightpearl_.orders.create(order) - .then(async salesOrderId => { - const order = await this.brightpearl_.orders.retrieve(salesOrderId) - const resResult = await this.brightpearl_.warehouses.createReservation(order, this.options.warehouse) + return client.orders + .create(order) + .then(async (salesOrderId) => { + const order = await client.orders.retrieve(salesOrderId) + const resResult = await client.warehouses.createReservation( + order, + this.options.warehouse + ) return salesOrderId }) - .then(async salesOrderId => { + .then(async (salesOrderId) => { const paymentMethod = fromOrder.payment_method const paymentType = "AUTH" const payment = { @@ -200,23 +247,31 @@ class BrightpearlService extends BaseService { if (paymentType === "AUTH") { const today = new Date() const authExpire = today.setDate(today.getDate() + 7) - payment.amountAuthorized = await this.totalsService_.getTotal(fromOrder) + payment.amountAuthorized = await this.totalsService_.getTotal( + fromOrder + ) payment.authorizationExpiry = new Date(authExpire) } else { // For captured } - await this.brightpearl_.payments.create(payment) + await client.payments.create(payment) return salesOrderId }) .then((salesOrderId) => { - return this.orderService_.setMetadata(fromOrder._id, "brightpearl_sales_order_id", salesOrderId) + return this.orderService_.setMetadata( + fromOrder._id, + "brightpearl_sales_order_id", + salesOrderId + ) }) } async createCapturedPayment(fromOrder) { - const soId = fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id + const client = await this.getClient() + const soId = + fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id if (!soId) { return } @@ -234,43 +289,54 @@ class BrightpearlService extends BaseService { paymentType, } - await this.brightpearl_.payments.create(payment) + await client.payments.create(payment) } - async getBrightpearlRows(fromOrder) { + async getBrightpearlRows(fromOrder) { const region = await this.regionService_.retrieve(fromOrder.region_id) - const discount = fromOrder.discounts.find(({ discount_rule }) => discount_rule.type !== "free_shipping") + const discount = fromOrder.discounts.find( + ({ discount_rule }) => discount_rule.type !== "free_shipping" + ) let lineDiscounts = [] if (discount) { - lineDiscounts = this.discountService_.getLineDiscounts(fromOrder, discount) + lineDiscounts = this.discountService_.getLineDiscounts( + fromOrder, + discount + ) } - const lines = await Promise.all(fromOrder.items.map(async item => { - const bpProduct = await this.retrieveProductBySKU(item.content.variant.sku) + const lines = await Promise.all( + fromOrder.items.map(async (item) => { + const bpProduct = await this.retrieveProductBySKU( + item.content.variant.sku + ) - const discount = lineDiscounts.find(l => l.item._id.equals(item._id)) || { amount: 0 } + const discount = lineDiscounts.find((l) => + l.item._id.equals(item._id) + ) || { amount: 0 } - const row = {} - if (bpProduct) { - row.productId = bpProduct.productId - } else { - row.name = item.title - } - row.net = item.content.unit_price * item.quantity - discount.amount - row.tax = row.net * fromOrder.tax_rate - row.quantity = item.quantity - row.taxCode = region.tax_code - row.externalRef = item._id - row.nominalCode = this.options.sales_account_code || "4000" + const row = {} + if (bpProduct) { + row.productId = bpProduct.productId + } else { + row.name = item.title + } + row.net = item.content.unit_price * item.quantity - discount.amount + row.tax = row.net * fromOrder.tax_rate + row.quantity = item.quantity + row.taxCode = region.tax_code + row.externalRef = item._id + row.nominalCode = this.options.sales_account_code || "4000" - return row - })) + return row + }) + ) const shippingTotal = this.totalsService_.getShippingTotal(fromOrder) const shippingMethods = fromOrder.shipping_methods if (shippingMethods.length > 0) { lines.push({ - name: `Shipping: ${shippingMethods.map(m => m.name).join(" + ")}`, + name: `Shipping: ${shippingMethods.map((m) => m.name).join(" + ")}`, quantity: 1, net: shippingTotal, tax: shippingTotal * fromOrder.tax_rate, @@ -281,17 +347,19 @@ class BrightpearlService extends BaseService { return lines } - retrieveCustomerByEmail(email) { - return this.brightpearl_.customers.retrieveByEmail(email).then(customers => { + async retrieveCustomerByEmail(email) { + const client = await this.getClient() + return client.customers.retrieveByEmail(email).then((customers) => { if (!customers.length) { return null } - return customers.find(c => c.primaryEmail === email) + return customers.find((c) => c.primaryEmail === email) }) } - retrieveProductBySKU(sku) { - return this.brightpearl_.products.retrieveBySKU(sku).then(products => { + async retrieveProductBySKU(sku) { + const client = await this.getClient() + return client.products.retrieveBySKU(sku).then((products) => { if (!products.length) { return null } @@ -300,21 +368,22 @@ class BrightpearlService extends BaseService { } async createCustomer(fromOrder) { - const address = await this.brightpearl_.addresses.create({ + const client = await this.getClient() + const address = await client.addresses.create({ addressLine1: fromOrder.shipping_address.address_1, addressLine2: fromOrder.shipping_address.address_2, postalCode: fromOrder.shipping_address.postal_code, countryIsoCode: fromOrder.shipping_address.country_code, }) - const customer = await this.brightpearl_.customers.create({ + const customer = await client.customers.create({ firstName: fromOrder.shipping_address.first_name, lastName: fromOrder.shipping_address.last_name, postAddressIds: { DEF: address, BIL: address, DEL: address, - } + }, }) return { contactId: customer } diff --git a/packages/medusa-plugin-brightpearl/src/services/oauth.js b/packages/medusa-plugin-brightpearl/src/services/oauth.js new file mode 100644 index 0000000000..1041043c60 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/src/services/oauth.js @@ -0,0 +1,40 @@ +import randomize from "randomatic" +import { OauthService } from "medusa-interfaces" +import Brightpearl from "../utils/brightpearl" + +const CLIENT_SECRET = process.env.BP_CLIENT_SECRET || "" + +class BrightpearlOauth extends OauthService { + constructor({}, options) { + super() + + this.account_ = options.account + } + + static getAppDetails(options) { + const client_id = "medusa-dev" + const client_secret = CLIENT_SECRET + const state = randomize("A0", 16) + const redirect = "https://localhost:8000/a/oauth/brightpearl" + return { + application_name: "brightpearl", + display_name: "Brightpearl", + install_url: `https://oauth.brightpearl.com/authorize/${options.account}?response_type=code&client_id=${client_id}&redirect_uri=${redirect}&state=${state}`, + state, + } + } + + async generateToken(code) { + const params = { + client_id: "medusa-dev", + client_secret: CLIENT_SECRET, + redirect: "https://localhost:8000/a/oauth/brightpearl", + code, + } + + const data = await Brightpearl.createToken(this.account_, params) + return data + } +} + +export default BrightpearlOauth diff --git a/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js b/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js index 2a0c87ae61..1c6702b9d9 100644 --- a/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js @@ -1,12 +1,33 @@ import axios from "axios" +import qs from "querystring" class BrightpearlClient { + static createToken(account, data) { + const params = { + grant_type: "authorization_code", + code: data.code, + client_id: data.client_id, + client_secret: data.client_secret, + redirect_uri: data.redirect, + } + + return axios({ + url: `https://ws-eu1.brightpearl.com/${account}/oauth/token`, + method: "POST", + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + data: qs.stringify(params), + }).then(({ data }) => data) + } + constructor(options) { this.client_ = axios.create({ - baseURL: `https://${options.datacenter}.brightpearl.com/public-api/${options.account}`, + baseURL: `${options.url}/public-api/${options.account}`, headers: { - 'brightpearl-app-ref': options.app_ref, - 'brightpearl-account-token': options.token + "brightpearl-app-ref": "medusa-dev", + "brightpearl-dev-ref": "sebrindom", + Authorization: `${options.auth_type} ${options.access_token}`, }, }) @@ -22,7 +43,7 @@ class BrightpearlClient { buildSearchResults_(response) { const { results, metaData } = response // Map the column names to the columns - return results.map(resColumns => { + return results.map((resColumns) => { const object = {} for (let i = 0; i < resColumns.length; i++) { const fieldName = metaData.columns[i].name @@ -35,99 +56,108 @@ class BrightpearlClient { buildWebhookEndpoints = () => { return { list: () => { - return this.client_.request({ - url: `/integration-service/webhook`, - method: "GET", - }) - .then(({ data }) => data.response) + return this.client_ + .request({ + url: `/integration-service/webhook`, + method: "GET", + }) + .then(({ data }) => data.response) }, create: (data) => { return this.client_.request({ url: `/integration-service/webhook`, method: "POST", - data + data, }) - } + }, } } buildPaymentEndpoints = () => { return { create: (payment) => { - return this.client_.request({ - url: `/accounting-service/customer-payment`, - method: "POST", - data: payment - }) - .then(({ data }) => data.response) - } + return this.client_ + .request({ + url: `/accounting-service/customer-payment`, + method: "POST", + data: payment, + }) + .then(({ data }) => data.response) + }, } } buildWarehouseEndpoints = () => { return { retrieveReservation: (orderId) => { - return this.client_.request({ - url: `/warehouse-service/order/${orderId}/reservation`, - method: "GET", - }) - .then(({ data }) => data.response) + return this.client_ + .request({ + url: `/warehouse-service/order/${orderId}/reservation`, + method: "GET", + }) + .then(({ data }) => data.response) }, createGoodsOutNote: (orderId, data) => { - return this.client_.request({ - url: `/warehouse-service/order/${orderId}/goods-note/goods-out`, - method: "POST", - data, - }).then(({ data }) => data.response) + return this.client_ + .request({ + url: `/warehouse-service/order/${orderId}/goods-note/goods-out`, + method: "POST", + data, + }) + .then(({ data }) => data.response) }, updateGoodsOutNote: (noteId, update) => { return this.client_.request({ url: `/warehouse-service/goods-note/goods-out/${noteId}`, method: "PUT", - data: update + data: update, }) }, registerGoodsOutEvent: (noteId, data) => { return this.client_.request({ url: `/warehouse-service/goods-note/goods-out/${noteId}/event`, method: "POST", - data + data, }) }, createReservation: (order, warehouse) => { const id = order.id - const data = order.rows.map(r => ({ + const data = order.rows.map((r) => ({ productId: r.productId, salesOrderRowId: r.id, - quantity: r.quantity + quantity: r.quantity, })) - return this.client_.request({ - url: `/warehouse-service/order/${id}/reservation/warehouse/${warehouse}`, - method: "POST", - data: { - products: data - } - }).then(({ data }) => data.response) - } + return this.client_ + .request({ + url: `/warehouse-service/order/${id}/reservation/warehouse/${warehouse}`, + method: "POST", + data: { + products: data, + }, + }) + .then(({ data }) => data.response) + }, } } buildOrderEndpoints = () => { return { retrieve: (orderId) => { - return this.client_.request({ - url: `/order-service/sales-order/${orderId}`, - method: "GET", - }) - .then(({ data }) => data.response.length && data.response[0]) - .catch(err => console.log(err)) + return this.client_ + .request({ + url: `/order-service/sales-order/${orderId}`, + method: "GET", + }) + .then(({ data }) => data.response.length && data.response[0]) + .catch((err) => console.log(err)) }, create: (order) => { - return this.client_.request({ - url: `/order-service/sales-order`, - method: "POST", - data: order - }) + return this.client_ + .request({ + url: `/order-service/sales-order`, + method: "POST", + data: order, + }) .then(({ data }) => data.response) }, } @@ -136,61 +166,66 @@ class BrightpearlClient { buildAddressEndpoints = () => { return { create: (address) => { - return this.client_.request({ - url: `/contact-service/postal-address`, - method: "POST", - data: address - }) - .then(({ data }) => data.response) - } + return this.client_ + .request({ + url: `/contact-service/postal-address`, + method: "POST", + data: address, + }) + .then(({ data }) => data.response) + }, } } - buildProductEndpoints = () => { + buildProductEndpoints = () => { return { - retrieveAvailability: productId => { - return this.client_.request({ - url: `/warehouse-service/product-availability/${productId}`, - }) - .then(({ data }) => data.response && data.response) + retrieveAvailability: (productId) => { + return this.client_ + .request({ + url: `/warehouse-service/product-availability/${productId}`, + }) + .then(({ data }) => data.response && data.response) }, retrieve: (productId) => { - return this.client_.request({ - url: `/product-service/product/${productId}`, - }) - .then(({ data }) => data.response && data.response[0]) + return this.client_ + .request({ + url: `/product-service/product/${productId}`, + }) + .then(({ data }) => data.response && data.response[0]) }, retrieveBySKU: (sku) => { - return this.client_.request({ - url: `/product-service/product-search?SKU=${sku}`, - }) - .then(({ data }) => { - return this.buildSearchResults_(data.response) - }) - } + return this.client_ + .request({ + url: `/product-service/product-search?SKU=${sku}`, + }) + .then(({ data }) => { + return this.buildSearchResults_(data.response) + }) + }, } } - buildCustomerEndpoints = () => { return { retrieveByEmail: (email) => { - return this.client_.request({ - url: `/contact-service/contact-search?primaryEmail=${email}`, - }) - .then(({data }) => { - return this.buildSearchResults_(data.response) - }) + return this.client_ + .request({ + url: `/contact-service/contact-search?primaryEmail=${email}`, + }) + .then(({ data }) => { + return this.buildSearchResults_(data.response) + }) }, create: (customerData) => { - return this.client_.request({ - url: `/contact-service/contact`, - method: "POST", - data: customerData - }) + return this.client_ + .request({ + url: `/contact-service/contact`, + method: "POST", + data: customerData, + }) .then(({ data }) => data.response) - } + }, } } } diff --git a/packages/medusa-plugin-brightpearl/utils/brightpearl.js b/packages/medusa-plugin-brightpearl/utils/brightpearl.js index 8e988a96c0..d55a57bc07 100644 --- a/packages/medusa-plugin-brightpearl/utils/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/utils/brightpearl.js @@ -7,6 +7,8 @@ exports["default"] = void 0; var _axios = _interopRequireDefault(require("axios")); +var _querystring = _interopRequireDefault(require("querystring")); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } } @@ -20,6 +22,30 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } var BrightpearlClient = /*#__PURE__*/function () { + _createClass(BrightpearlClient, null, [{ + key: "createToken", + value: function createToken(account, data) { + var params = { + grant_type: "authorization_code", + code: data.code, + client_id: data.client_id, + client_secret: data.client_secret, + redirect_uri: data.redirect + }; + return (0, _axios["default"])({ + url: "https://ws-eu1.brightpearl.com/".concat(account, "/oauth/token"), + method: "POST", + headers: { + "content-type": "application/x-www-form-urlencoded" + }, + data: _querystring["default"].stringify(params) + }).then(function (_ref) { + var data = _ref.data; + return data; + }); + } + }]); + function BrightpearlClient(options) { var _this = this; @@ -31,8 +57,8 @@ var BrightpearlClient = /*#__PURE__*/function () { return _this.client_.request({ url: "/integration-service/webhook", method: "GET" - }).then(function (_ref) { - var data = _ref.data; + }).then(function (_ref2) { + var data = _ref2.data; return data.response; }); }, @@ -53,8 +79,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/accounting-service/customer-payment", method: "POST", data: payment - }).then(function (_ref2) { - var data = _ref2.data; + }).then(function (_ref3) { + var data = _ref3.data; return data.response; }); } @@ -67,8 +93,8 @@ var BrightpearlClient = /*#__PURE__*/function () { return _this.client_.request({ url: "/warehouse-service/order/".concat(orderId, "/reservation"), method: "GET" - }).then(function (_ref3) { - var data = _ref3.data; + }).then(function (_ref4) { + var data = _ref4.data; return data.response; }); }, @@ -77,8 +103,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/warehouse-service/order/".concat(orderId, "/goods-note/goods-out"), method: "POST", data: data - }).then(function (_ref4) { - var data = _ref4.data; + }).then(function (_ref5) { + var data = _ref5.data; return data.response; }); }, @@ -111,8 +137,8 @@ var BrightpearlClient = /*#__PURE__*/function () { data: { products: data } - }).then(function (_ref5) { - var data = _ref5.data; + }).then(function (_ref6) { + var data = _ref6.data; return data.response; }); } @@ -125,8 +151,8 @@ var BrightpearlClient = /*#__PURE__*/function () { return _this.client_.request({ url: "/order-service/sales-order/".concat(orderId), method: "GET" - }).then(function (_ref6) { - var data = _ref6.data; + }).then(function (_ref7) { + var data = _ref7.data; return data.response.length && data.response[0]; })["catch"](function (err) { return console.log(err); @@ -137,8 +163,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/order-service/sales-order", method: "POST", data: order - }).then(function (_ref7) { - var data = _ref7.data; + }).then(function (_ref8) { + var data = _ref8.data; return data.response; }); } @@ -152,8 +178,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/contact-service/postal-address", method: "POST", data: address - }).then(function (_ref8) { - var data = _ref8.data; + }).then(function (_ref9) { + var data = _ref9.data; return data.response; }); } @@ -165,24 +191,24 @@ var BrightpearlClient = /*#__PURE__*/function () { retrieveAvailability: function retrieveAvailability(productId) { return _this.client_.request({ url: "/warehouse-service/product-availability/".concat(productId) - }).then(function (_ref9) { - var data = _ref9.data; + }).then(function (_ref10) { + var data = _ref10.data; return data.response && data.response; }); }, retrieve: function retrieve(productId) { return _this.client_.request({ url: "/product-service/product/".concat(productId) - }).then(function (_ref10) { - var data = _ref10.data; + }).then(function (_ref11) { + var data = _ref11.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 (_ref11) { - var data = _ref11.data; + }).then(function (_ref12) { + var data = _ref12.data; return _this.buildSearchResults_(data.response); }); } @@ -194,8 +220,8 @@ var BrightpearlClient = /*#__PURE__*/function () { retrieveByEmail: function retrieveByEmail(email) { return _this.client_.request({ url: "/contact-service/contact-search?primaryEmail=".concat(email) - }).then(function (_ref12) { - var data = _ref12.data; + }).then(function (_ref13) { + var data = _ref13.data; return _this.buildSearchResults_(data.response); }); }, @@ -204,8 +230,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/contact-service/contact", method: "POST", data: customerData - }).then(function (_ref13) { - var data = _ref13.data; + }).then(function (_ref14) { + var data = _ref14.data; return data.response; }); } @@ -213,10 +239,11 @@ var BrightpearlClient = /*#__PURE__*/function () { }); this.client_ = _axios["default"].create({ - baseURL: "https://".concat(options.datacenter, ".brightpearl.com/public-api/").concat(options.account), + baseURL: "".concat(options.url, "/public-api/").concat(options.account), headers: { - 'brightpearl-app-ref': options.app_ref, - 'brightpearl-account-token': options.token + "brightpearl-app-ref": "medusa-dev", + "brightpearl-dev-ref": "sebrindom", + Authorization: "".concat(options.auth_type, " ").concat(options.access_token) } }); this.webhooks = this.buildWebhookEndpoints(); diff --git a/packages/medusa-plugin-discount-generator/package.json b/packages/medusa-plugin-discount-generator/package.json index 881ca8af58..2f6aaafd69 100644 --- a/packages/medusa-plugin-discount-generator/package.json +++ b/packages/medusa-plugin-discount-generator/package.json @@ -14,6 +14,7 @@ "@babel/core": "^7.7.5", "@babel/node": "^7.7.4", "@babel/plugin-proposal-class-properties": "^7.7.4", + "@babel/plugin-transform-classes": "^7.9.5", "@babel/plugin-transform-instanceof": "^7.8.3", "@babel/plugin-transform-runtime": "^7.7.6", "@babel/preset-env": "^7.7.5", diff --git a/packages/medusa-plugin-discount-generator/yarn.lock b/packages/medusa-plugin-discount-generator/yarn.lock index 65165c0f1a..6926ea6a9d 100644 --- a/packages/medusa-plugin-discount-generator/yarn.lock +++ b/packages/medusa-plugin-discount-generator/yarn.lock @@ -497,7 +497,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-classes@^7.10.4": +"@babel/plugin-transform-classes@^7.10.4", "@babel/plugin-transform-classes@^7.9.5": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7" integrity sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA== diff --git a/packages/medusa-plugin-economic/yarn.lock b/packages/medusa-plugin-economic/yarn.lock index 4bd7a1e1cb..2e47e396b1 100644 --- a/packages/medusa-plugin-economic/yarn.lock +++ b/packages/medusa-plugin-economic/yarn.lock @@ -881,44 +881,6 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@hapi/address@^2.1.2": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" - integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ== - -"@hapi/formula@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-1.2.0.tgz#994649c7fea1a90b91a0a1e6d983523f680e10cd" - integrity sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA== - -"@hapi/hoek@^8.2.4", "@hapi/hoek@^8.3.0": - version "8.5.1" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06" - integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow== - -"@hapi/joi@^16.1.8": - version "16.1.8" - resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-16.1.8.tgz#84c1f126269489871ad4e2decc786e0adef06839" - integrity sha512-wAsVvTPe+FwSrsAurNt5vkg3zo+TblvC5Bb1zMVK6SJzZqw9UrJnexxR+76cpePmtUZKHAPxcQ2Bf7oVHyahhg== - dependencies: - "@hapi/address" "^2.1.2" - "@hapi/formula" "^1.2.0" - "@hapi/hoek" "^8.2.4" - "@hapi/pinpoint" "^1.0.2" - "@hapi/topo" "^3.1.3" - -"@hapi/pinpoint@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-1.0.2.tgz#025b7a36dbbf4d35bf1acd071c26b20ef41e0d13" - integrity sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ== - -"@hapi/topo@^3.1.3": - version "3.1.6" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.6.tgz#68d935fa3eae7fdd5ab0d7f953f3205d8b2bfc29" - integrity sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ== - dependencies: - "@hapi/hoek" "^8.3.0" - "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -1500,19 +1462,6 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.0.tgz#e1a574cdf528e4053019bb800b041c0ac88da493" - integrity sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA== - dependencies: - readable-stream "^2.3.5" - safe-buffer "^5.1.1" - -bluebird@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" - integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== - body-parser@1.19.0, body-parser@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -1589,11 +1538,6 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -bson@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.4.tgz#f76870d799f15b854dffb7ee32f0a874797f7e89" - integrity sha512-S/yKGU1syOMzO86+dGpg2qGoDL0zvzcb262G+gqEy6TgP6rt6z6qxSFX/8X6vLC91P7G7C3nLs0+bvDzmvBA3Q== - buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -1934,7 +1878,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@3.1.0, debug@=3.1.0: +debug@=3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -2002,11 +1946,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -denque@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" - integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== - depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -3516,11 +3455,6 @@ jest@^25.5.2: import-local "^3.0.2" jest-cli "^25.5.4" -joi-objectid@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/joi-objectid/-/joi-objectid-3.0.1.tgz#63ace7860f8e1a993a28d40c40ffd8eff01a3668" - integrity sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -3623,11 +3557,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -kareem@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.1.tgz#def12d9c941017fabfb00f873af95e9c99e1be87" - integrity sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw== - keygrip@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc" @@ -3765,33 +3694,6 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -medusa-core-utils@^0.3.0: - version "0.1.39" - resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-0.1.39.tgz#d57816c9bd43f9a92883650c1e66add1665291df" - integrity sha512-R8+U1ile7if+nR6Cjh5exunx0ETV0OfkWUUBUpz1KmHSDv0V0CcvQqU9lcZesPFDEbu3Y2iEjsCqidVA4nG2nQ== - dependencies: - "@hapi/joi" "^16.1.8" - joi-objectid "^3.0.1" - -medusa-interfaces@^0.3.0: - version "0.1.39" - resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-0.1.39.tgz#633db3c8c6afd7fec9ae24496737369d840105cf" - integrity sha512-byKIcK7o3L4shmGn+pgZAUyLrT991zCqK4jWXIleQJbGImQy6TmdXido+tEzFptVBJWMIQ8BWnP/b7r29D8EXA== - dependencies: - mongoose "^5.8.0" - -medusa-test-utils@^0.3.0: - version "0.1.39" - resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-0.1.39.tgz#b7c166006a2fa4f02e52ab3bfafc19a3ae787f3e" - integrity sha512-M/Br8/HYvl7x2oLnme4NxdQwoyV0XUyOWiCyvPp7q1HUTB684lhJf1MikZVrcSjsh2L1rpyi3GRbKdf4cpJWvw== - dependencies: - mongoose "^5.8.0" - -memory-pager@^1.0.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" - integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== - merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -3888,57 +3790,6 @@ moment@^2.27.0: resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== -mongodb@3.5.9: - version "3.5.9" - resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.5.9.tgz#799b72be8110b7e71a882bb7ce0d84d05429f772" - integrity sha512-vXHBY1CsGYcEPoVWhwgxIBeWqP3dSu9RuRDsoLRPTITrcrgm1f0Ubu1xqF9ozMwv53agmEiZm0YGo+7WL3Nbug== - dependencies: - bl "^2.2.0" - bson "^1.1.4" - denque "^1.4.1" - require_optional "^1.0.1" - safe-buffer "^5.1.2" - optionalDependencies: - saslprep "^1.0.0" - -mongoose-legacy-pluralize@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" - integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== - -mongoose@^5.8.0: - version "5.9.23" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.9.23.tgz#cb16687fbd19082bbbf0b8fab4778e2e4d85e7b1" - integrity sha512-fMYlMRJz0T6Ax2K2P0jt+kxXd4qaRxyfZCha1YBMczmA2EBlT5SnBlcDyJ4YQa4/z+GoDh06uH090w7BfBcdWg== - dependencies: - bson "^1.1.4" - kareem "2.3.1" - mongodb "3.5.9" - mongoose-legacy-pluralize "1.0.2" - mpath "0.7.0" - mquery "3.2.2" - ms "2.1.2" - regexp-clone "1.0.0" - safe-buffer "5.2.1" - sift "7.0.1" - sliced "1.0.1" - -mpath@0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.7.0.tgz#20e8102e276b71709d6e07e9f8d4d0f641afbfb8" - integrity sha512-Aiq04hILxhz1L+f7sjGyn7IxYzWm1zLNNXcfhDtx04kZ2Gk7uvFdgZ8ts1cWa/6d0TQmag2yR8zSGZUmp0tFNg== - -mquery@3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.2.tgz#e1383a3951852ce23e37f619a9b350f1fb3664e7" - integrity sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q== - dependencies: - bluebird "3.5.1" - debug "3.1.0" - regexp-clone "^1.0.0" - safe-buffer "5.1.2" - sliced "1.0.1" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -3949,7 +3800,7 @@ ms@2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -ms@2.1.2, ms@^2.1.1: +ms@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== @@ -4437,7 +4288,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^2.0.2, readable-stream@^2.3.5: +readable-stream@^2.0.2: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -4496,11 +4347,6 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp-clone@1.0.0, regexp-clone@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" - integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== - regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" @@ -4597,14 +4443,6 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -require_optional@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" - integrity sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g== - dependencies: - resolve-from "^2.0.0" - semver "^5.1.0" - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -4612,11 +4450,6 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" -resolve-from@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" - integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c= - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -4693,7 +4526,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2: +safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -4725,13 +4558,6 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -saslprep@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" - integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== - dependencies: - sparse-bitfield "^3.0.3" - saxes@^3.1.9: version "3.1.11" resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b" @@ -4739,7 +4565,7 @@ saxes@^3.1.9: dependencies: xmlchars "^2.1.1" -"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -4832,11 +4658,6 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== -sift@7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/sift/-/sift-7.0.1.tgz#47d62c50b159d316f1372f8b53f9c10cd21a4b08" - integrity sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g== - signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -4866,11 +4687,6 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" -sliced@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" - integrity sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E= - snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -4940,13 +4756,6 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== -sparse-bitfield@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" - integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE= - dependencies: - memory-pager "^1.0.2" - spdx-correct@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" diff --git a/packages/medusa-plugin-slack-notification/yarn.lock b/packages/medusa-plugin-slack-notification/yarn.lock index 4bd7a1e1cb..2e47e396b1 100644 --- a/packages/medusa-plugin-slack-notification/yarn.lock +++ b/packages/medusa-plugin-slack-notification/yarn.lock @@ -881,44 +881,6 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@hapi/address@^2.1.2": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" - integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ== - -"@hapi/formula@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-1.2.0.tgz#994649c7fea1a90b91a0a1e6d983523f680e10cd" - integrity sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA== - -"@hapi/hoek@^8.2.4", "@hapi/hoek@^8.3.0": - version "8.5.1" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06" - integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow== - -"@hapi/joi@^16.1.8": - version "16.1.8" - resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-16.1.8.tgz#84c1f126269489871ad4e2decc786e0adef06839" - integrity sha512-wAsVvTPe+FwSrsAurNt5vkg3zo+TblvC5Bb1zMVK6SJzZqw9UrJnexxR+76cpePmtUZKHAPxcQ2Bf7oVHyahhg== - dependencies: - "@hapi/address" "^2.1.2" - "@hapi/formula" "^1.2.0" - "@hapi/hoek" "^8.2.4" - "@hapi/pinpoint" "^1.0.2" - "@hapi/topo" "^3.1.3" - -"@hapi/pinpoint@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-1.0.2.tgz#025b7a36dbbf4d35bf1acd071c26b20ef41e0d13" - integrity sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ== - -"@hapi/topo@^3.1.3": - version "3.1.6" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.6.tgz#68d935fa3eae7fdd5ab0d7f953f3205d8b2bfc29" - integrity sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ== - dependencies: - "@hapi/hoek" "^8.3.0" - "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -1500,19 +1462,6 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.0.tgz#e1a574cdf528e4053019bb800b041c0ac88da493" - integrity sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA== - dependencies: - readable-stream "^2.3.5" - safe-buffer "^5.1.1" - -bluebird@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" - integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== - body-parser@1.19.0, body-parser@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -1589,11 +1538,6 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -bson@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.4.tgz#f76870d799f15b854dffb7ee32f0a874797f7e89" - integrity sha512-S/yKGU1syOMzO86+dGpg2qGoDL0zvzcb262G+gqEy6TgP6rt6z6qxSFX/8X6vLC91P7G7C3nLs0+bvDzmvBA3Q== - buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -1934,7 +1878,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@3.1.0, debug@=3.1.0: +debug@=3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -2002,11 +1946,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -denque@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" - integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== - depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -3516,11 +3455,6 @@ jest@^25.5.2: import-local "^3.0.2" jest-cli "^25.5.4" -joi-objectid@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/joi-objectid/-/joi-objectid-3.0.1.tgz#63ace7860f8e1a993a28d40c40ffd8eff01a3668" - integrity sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -3623,11 +3557,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -kareem@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.1.tgz#def12d9c941017fabfb00f873af95e9c99e1be87" - integrity sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw== - keygrip@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc" @@ -3765,33 +3694,6 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -medusa-core-utils@^0.3.0: - version "0.1.39" - resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-0.1.39.tgz#d57816c9bd43f9a92883650c1e66add1665291df" - integrity sha512-R8+U1ile7if+nR6Cjh5exunx0ETV0OfkWUUBUpz1KmHSDv0V0CcvQqU9lcZesPFDEbu3Y2iEjsCqidVA4nG2nQ== - dependencies: - "@hapi/joi" "^16.1.8" - joi-objectid "^3.0.1" - -medusa-interfaces@^0.3.0: - version "0.1.39" - resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-0.1.39.tgz#633db3c8c6afd7fec9ae24496737369d840105cf" - integrity sha512-byKIcK7o3L4shmGn+pgZAUyLrT991zCqK4jWXIleQJbGImQy6TmdXido+tEzFptVBJWMIQ8BWnP/b7r29D8EXA== - dependencies: - mongoose "^5.8.0" - -medusa-test-utils@^0.3.0: - version "0.1.39" - resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-0.1.39.tgz#b7c166006a2fa4f02e52ab3bfafc19a3ae787f3e" - integrity sha512-M/Br8/HYvl7x2oLnme4NxdQwoyV0XUyOWiCyvPp7q1HUTB684lhJf1MikZVrcSjsh2L1rpyi3GRbKdf4cpJWvw== - dependencies: - mongoose "^5.8.0" - -memory-pager@^1.0.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" - integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== - merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -3888,57 +3790,6 @@ moment@^2.27.0: resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== -mongodb@3.5.9: - version "3.5.9" - resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.5.9.tgz#799b72be8110b7e71a882bb7ce0d84d05429f772" - integrity sha512-vXHBY1CsGYcEPoVWhwgxIBeWqP3dSu9RuRDsoLRPTITrcrgm1f0Ubu1xqF9ozMwv53agmEiZm0YGo+7WL3Nbug== - dependencies: - bl "^2.2.0" - bson "^1.1.4" - denque "^1.4.1" - require_optional "^1.0.1" - safe-buffer "^5.1.2" - optionalDependencies: - saslprep "^1.0.0" - -mongoose-legacy-pluralize@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" - integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== - -mongoose@^5.8.0: - version "5.9.23" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.9.23.tgz#cb16687fbd19082bbbf0b8fab4778e2e4d85e7b1" - integrity sha512-fMYlMRJz0T6Ax2K2P0jt+kxXd4qaRxyfZCha1YBMczmA2EBlT5SnBlcDyJ4YQa4/z+GoDh06uH090w7BfBcdWg== - dependencies: - bson "^1.1.4" - kareem "2.3.1" - mongodb "3.5.9" - mongoose-legacy-pluralize "1.0.2" - mpath "0.7.0" - mquery "3.2.2" - ms "2.1.2" - regexp-clone "1.0.0" - safe-buffer "5.2.1" - sift "7.0.1" - sliced "1.0.1" - -mpath@0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.7.0.tgz#20e8102e276b71709d6e07e9f8d4d0f641afbfb8" - integrity sha512-Aiq04hILxhz1L+f7sjGyn7IxYzWm1zLNNXcfhDtx04kZ2Gk7uvFdgZ8ts1cWa/6d0TQmag2yR8zSGZUmp0tFNg== - -mquery@3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.2.tgz#e1383a3951852ce23e37f619a9b350f1fb3664e7" - integrity sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q== - dependencies: - bluebird "3.5.1" - debug "3.1.0" - regexp-clone "^1.0.0" - safe-buffer "5.1.2" - sliced "1.0.1" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -3949,7 +3800,7 @@ ms@2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -ms@2.1.2, ms@^2.1.1: +ms@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== @@ -4437,7 +4288,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^2.0.2, readable-stream@^2.3.5: +readable-stream@^2.0.2: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -4496,11 +4347,6 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp-clone@1.0.0, regexp-clone@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" - integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== - regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" @@ -4597,14 +4443,6 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -require_optional@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" - integrity sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g== - dependencies: - resolve-from "^2.0.0" - semver "^5.1.0" - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -4612,11 +4450,6 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" -resolve-from@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" - integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c= - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -4693,7 +4526,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2: +safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -4725,13 +4558,6 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -saslprep@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" - integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== - dependencies: - sparse-bitfield "^3.0.3" - saxes@^3.1.9: version "3.1.11" resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b" @@ -4739,7 +4565,7 @@ saxes@^3.1.9: dependencies: xmlchars "^2.1.1" -"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -4832,11 +4658,6 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== -sift@7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/sift/-/sift-7.0.1.tgz#47d62c50b159d316f1372f8b53f9c10cd21a4b08" - integrity sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g== - signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -4866,11 +4687,6 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" -sliced@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" - integrity sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E= - snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -4940,13 +4756,6 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== -sparse-bitfield@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" - integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE= - dependencies: - memory-pager "^1.0.2" - spdx-correct@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" diff --git a/packages/medusa/src/api/routes/admin/apps/authorize-app.js b/packages/medusa/src/api/routes/admin/apps/authorize-app.js new file mode 100644 index 0000000000..bd47844fb0 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/apps/authorize-app.js @@ -0,0 +1,25 @@ +import { MedusaError, Validator } from "medusa-core-utils" + +export default async (req, res) => { + const schema = Validator.object().keys({ + application_name: Validator.string().required(), + state: Validator.string().required(), + code: Validator.string().required(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + try { + const oauthService = req.scope.resolve("oauthService") + const data = await oauthService.generateToken( + value.application_name, + value.code, + value.state + ) + res.status(200).json({ apps: data }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/apps/index.js b/packages/medusa/src/api/routes/admin/apps/index.js new file mode 100644 index 0000000000..33af4ab43e --- /dev/null +++ b/packages/medusa/src/api/routes/admin/apps/index.js @@ -0,0 +1,16 @@ +import { Router } from "express" +import middlewares from "../../../middlewares" + +const route = Router() + +export default app => { + app.use("/apps", route) + + route.get("/", middlewares.wrap(require("./list").default)) + route.post( + "/authorizations", + middlewares.wrap(require("./authorize-app").default) + ) + + return app +} diff --git a/packages/medusa/src/api/routes/admin/apps/list.js b/packages/medusa/src/api/routes/admin/apps/list.js new file mode 100644 index 0000000000..bf925616f8 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/apps/list.js @@ -0,0 +1,12 @@ +import { MedusaError, Validator } from "medusa-core-utils" + +export default async (req, res) => { + try { + const oauthService = req.scope.resolve("oauthService") + const data = await oauthService.list({}) + + res.status(200).json({ apps: data }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/index.js b/packages/medusa/src/api/routes/admin/index.js index b1a3e05ee9..ea345ee3f7 100644 --- a/packages/medusa/src/api/routes/admin/index.js +++ b/packages/medusa/src/api/routes/admin/index.js @@ -13,6 +13,7 @@ import orderRoutes from "./orders" import storeRoutes from "./store" import uploadRoutes from "./uploads" import customerRoutes from "./customers" +import appRoutes from "./apps" const route = Router() @@ -40,6 +41,7 @@ export default (app, container, config) => { // Calls all middleware that has been registered to run after authentication. middlewareService.usePostAuthentication(app) + appRoutes(route) productRoutes(route) userRoutes(route) regionRoutes(route) diff --git a/packages/medusa/src/loaders/plugins.js b/packages/medusa/src/loaders/plugins.js index bcc2e92d4e..b328214246 100644 --- a/packages/medusa/src/loaders/plugins.js +++ b/packages/medusa/src/loaders/plugins.js @@ -5,6 +5,7 @@ import { PaymentService, FulfillmentService, FileService, + OauthService, } from "medusa-interfaces" import { getConfigFile, createRequireFromPath } from "medusa-core-utils" import _ from "lodash" @@ -47,14 +48,16 @@ export default async ({ rootDirectory, container, app }) => { version: createFileContentHash(process.cwd(), `**`), }) - resolved.forEach(pluginDetails => { - registerModels(pluginDetails, container) - registerServices(pluginDetails, container) - registerMedusaApi(pluginDetails, container) - registerApi(pluginDetails, app) - registerCoreRouters(pluginDetails, container) - registerSubscribers(pluginDetails, container) - }) + await Promise.all( + resolved.map(async pluginDetails => { + registerModels(pluginDetails, container) + await registerServices(pluginDetails, container) + registerMedusaApi(pluginDetails, container) + registerApi(pluginDetails, app) + registerCoreRouters(pluginDetails, container) + registerSubscribers(pluginDetails, container) + }) + ) await Promise.all( resolved.map(async pluginDetails => runLoaders(pluginDetails, container)) @@ -156,58 +159,80 @@ function registerApi(pluginDetails, app) { * registered * @return {void} */ -function registerServices(pluginDetails, container) { +async function registerServices(pluginDetails, container) { const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*`, {}) - files.forEach(fn => { - const loaded = require(fn).default - const name = formatRegistrationName(fn) + await Promise.all( + files.map(async fn => { + const loaded = require(fn).default + const name = formatRegistrationName(fn) - if (!(loaded.prototype instanceof BaseService)) { - const logger = container.resolve("logger") - const message = `Services must inherit from BaseService, please check ${fn}` - logger.error(message) - throw new Error(message) - } + if (!(loaded.prototype instanceof BaseService)) { + const logger = container.resolve("logger") + const message = `Services must inherit from BaseService, please check ${fn}` + logger.error(message) + throw new Error(message) + } - if (loaded.prototype instanceof PaymentService) { - // Register our payment providers to paymentProviders - container.registerAdd( - "paymentProviders", - asFunction(cradle => new loaded(cradle, pluginDetails.options)) - ) + if (loaded.prototype instanceof PaymentService) { + // Register our payment providers to paymentProviders + container.registerAdd( + "paymentProviders", + asFunction(cradle => new loaded(cradle, pluginDetails.options)) + ) - // Add the service directly to the container in order to make simple - // resolution if we already know which payment provider we need to use - container.register({ - [name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)), - [`pp_${loaded.identifier}`]: aliasTo(name), - }) - } else if (loaded.prototype instanceof FulfillmentService) { - // Register our payment providers to paymentProviders - container.registerAdd( - "fulfillmentProviders", - asFunction(cradle => new loaded(cradle, pluginDetails.options)) - ) + // Add the service directly to the container in order to make simple + // resolution if we already know which payment provider we need to use + container.register({ + [name]: asFunction( + cradle => new loaded(cradle, pluginDetails.options) + ), + [`pp_${loaded.identifier}`]: aliasTo(name), + }) + } else if (loaded.prototype instanceof OauthService) { + const oauthService = container.resolve("oauthService") - // Add the service directly to the container in order to make simple - // resolution if we already know which payment provider we need to use - container.register({ - [name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)), - [`fp_${loaded.identifier}`]: aliasTo(name), - }) - } else if (loaded.prototype instanceof FileService) { - // Add the service directly to the container in order to make simple - // resolution if we already know which payment provider we need to use - container.register({ - [name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)), - [`fileService`]: aliasTo(name), - }) - } else { - container.register({ - [name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)), - }) - } - }) + const appDetails = loaded.getAppDetails(pluginDetails.options) + await oauthService.registerOauthApp(appDetails) + + const name = appDetails.application_name + container.register({ + [`${name}Oauth`]: asFunction( + cradle => new loaded(cradle, pluginDetails.options) + ), + }) + } else if (loaded.prototype instanceof FulfillmentService) { + // Register our payment providers to paymentProviders + container.registerAdd( + "fulfillmentProviders", + asFunction(cradle => new loaded(cradle, pluginDetails.options)) + ) + + // Add the service directly to the container in order to make simple + // resolution if we already know which payment provider we need to use + container.register({ + [name]: asFunction( + cradle => new loaded(cradle, pluginDetails.options) + ), + [`fp_${loaded.identifier}`]: aliasTo(name), + }) + } else if (loaded.prototype instanceof FileService) { + // Add the service directly to the container in order to make simple + // resolution if we already know which payment provider we need to use + container.register({ + [name]: asFunction( + cradle => new loaded(cradle, pluginDetails.options) + ), + [`fileService`]: aliasTo(name), + }) + } else { + container.register({ + [name]: asFunction( + cradle => new loaded(cradle, pluginDetails.options) + ), + }) + } + }) + ) } /** diff --git a/packages/medusa/src/models/oauth.js b/packages/medusa/src/models/oauth.js new file mode 100644 index 0000000000..7729e8e87e --- /dev/null +++ b/packages/medusa/src/models/oauth.js @@ -0,0 +1,16 @@ +import mongoose from "mongoose" +import { BaseModel } from "medusa-interfaces" + +class OauthModel extends BaseModel { + static modelName = "Oauth" + + static schema = { + display_name: { type: String, required: true }, + application_name: { type: String, required: true, unique: true }, + install_url: { type: String, required: true }, + uninstall_url: { type: String, default: "" }, + data: { type: mongoose.Schema.Types.Mixed, default: {} }, + } +} + +export default OauthModel diff --git a/packages/medusa/src/services/oauth.js b/packages/medusa/src/services/oauth.js new file mode 100644 index 0000000000..5bbfad13e6 --- /dev/null +++ b/packages/medusa/src/services/oauth.js @@ -0,0 +1,110 @@ +import _ from "lodash" +import { Validator, MedusaError } from "medusa-core-utils" +import { OauthService } from "medusa-interfaces" + +class Oauth extends OauthService { + static Events = { + TOKEN_GENERATED: "oauth.token_generated", + TOKEN_REFRESHED: "oauth.token_refreshed", + } + + constructor(cradle) { + super() + this.container_ = cradle + this.model_ = cradle.oauthModel + this.eventBus_ = cradle.eventBusService + } + + retrieveByName(appName) { + return this.model_.findOne({ + application_name: appName, + }) + } + + list(selector) { + return this.model_.find(selector) + } + + create(data) { + return this.model_.create({ + display_name: data.display_name, + application_name: data.application_name, + install_url: data.install_url, + uninstall_url: data.uninstall_url, + }) + } + + update(id, update) { + return this.model_.updateOne( + { + _id: id, + }, + update + ) + } + + async registerOauthApp(appDetails) { + const { application_name } = appDetails + const existing = await this.retrieveByName(application_name) + if (existing) { + return + } + + return this.create(appDetails) + } + + async generateToken(appName, code, state) { + const app = await this.retrieveByName(appName) + const service = this.container_[`${app.application_name}Oauth`] + if (!service) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `An OAuth handler for ${app.display_name} could not be found make sure the plugin is installed` + ) + } + + if (!app.state === state) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `${app.display_name} could not match state` + ) + } + + const authData = await service.generateToken(code) + + return this.update(app._id, { + data: authData, + }).then(result => { + this.eventBus_.emit( + `${Oauth.Events.TOKEN_GENERATED}.${appName}`, + authData + ) + return result + }) + } + + async refreshToken(appName, refreshToken) { + const app = await this.retrieveByName(appName) + const service = this.container_[`${app.application_name}Oauth`] + if (!service) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `An OAuth handler for ${app.display_name} could not be found make sure the plugin is installed` + ) + } + + const authData = await service.refreshToken(refreshToken) + + return this.update(app._id, { + data: authData, + }).then(result => { + this.eventBus_.emit( + `${Oauth.Events.TOKEN_REFRESHED}.${appName}`, + authData + ) + return result + }) + } +} + +export default Oauth diff --git a/packages/medusa/yarn.lock b/packages/medusa/yarn.lock index d8c0b200f7..c3775ee84d 100644 --- a/packages/medusa/yarn.lock +++ b/packages/medusa/yarn.lock @@ -4568,14 +4568,6 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -medusa-core-utils@^0.3.0: - version "0.1.39" - resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-0.1.39.tgz#d57816c9bd43f9a92883650c1e66add1665291df" - integrity sha512-R8+U1ile7if+nR6Cjh5exunx0ETV0OfkWUUBUpz1KmHSDv0V0CcvQqU9lcZesPFDEbu3Y2iEjsCqidVA4nG2nQ== - dependencies: - "@hapi/joi" "^16.1.8" - joi-objectid "^3.0.1" - medusa-interfaces@^0.1.27: version "0.1.27" resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-0.1.27.tgz#e77f9a9f82a7118eac8b35c1498ef8a5cec78898" @@ -4583,13 +4575,6 @@ medusa-interfaces@^0.1.27: 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: - mongoose "^5.8.0" - memory-pager@^1.0.2: version "1.5.0" resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" From ee93256e69bb907c7a1858a7f58506ad50132451 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Wed, 5 Aug 2020 15:26:00 +0200 Subject: [PATCH 5/5] Tests passing --- .../src/subscribers/order.js | 14 -- .../src/subscribers/cart.js | 5 - .../loaders/inventory.js | 56 +++++++- .../loaders/webhooks.js | 32 ++++- .../src/loaders/inventory.js | 20 ++- .../src/loaders/webhooks.js | 10 +- .../src/services/brightpearl.js | 1 + .../admin/orders/__tests__/return-order.js | 30 +--- .../api/routes/admin/orders/return-order.js | 8 +- packages/medusa/src/models/__mocks__/order.js | 4 + .../medusa/src/models/schemas/line-item.js | 1 + .../src/services/__tests__/event-bus.js | 2 +- .../medusa/src/services/__tests__/order.js | 105 +++++++++----- .../medusa/src/services/__tests__/totals.js | 16 +-- packages/medusa/src/services/order.js | 116 +++++++++++----- packages/medusa/src/services/totals.js | 128 +++++------------- packages/medusa/src/subscribers/order.js | 8 -- 17 files changed, 311 insertions(+), 245 deletions(-) delete mode 100644 packages/medusa-payment-klarna/src/subscribers/order.js diff --git a/packages/medusa-payment-klarna/src/subscribers/order.js b/packages/medusa-payment-klarna/src/subscribers/order.js deleted file mode 100644 index cb703722b8..0000000000 --- a/packages/medusa-payment-klarna/src/subscribers/order.js +++ /dev/null @@ -1,14 +0,0 @@ -class OrderSubscriber { - constructor({ klarnaProviderService, eventBusService }) { - this.klarnaProviderService_ = klarnaProviderService - - this.eventBus_ = eventBusService - - this.eventBus_.subscribe("order.completed", async (order) => { - const klarnaOrderId = order.payment_method.data.id - await this.klarnaProviderService_.acknowledgeOrder(klarnaOrderId) - }) - } -} - -export default OrderSubscriber diff --git a/packages/medusa-payment-stripe/src/subscribers/cart.js b/packages/medusa-payment-stripe/src/subscribers/cart.js index b1c694ad82..95356a0667 100644 --- a/packages/medusa-payment-stripe/src/subscribers/cart.js +++ b/packages/medusa-payment-stripe/src/subscribers/cart.js @@ -13,11 +13,6 @@ class CartSubscriber { this.eventBus_.subscribe("cart.customer_updated", async (cart) => { await this.onCustomerUpdated(cart) }) - - this.eventBus_.subscribe("order.completed", async (order) => { - const paymentData = order.payment_method.data - await this.stripeProviderService_.capturePayment(paymentData) - }) } async onCustomerUpdated(cart) { diff --git a/packages/medusa-plugin-brightpearl/loaders/inventory.js b/packages/medusa-plugin-brightpearl/loaders/inventory.js index c534686a7d..edbcb5c6bb 100644 --- a/packages/medusa-plugin-brightpearl/loaders/inventory.js +++ b/packages/medusa-plugin-brightpearl/loaders/inventory.js @@ -5,13 +5,57 @@ Object.defineProperty(exports, "__esModule", { }); exports["default"] = void 0; -var inventorySync = function inventorySync(container) { - var brightpearlService = container.resolve("brightpearlService"); - var eventBus = container.resolve("eventBusService"); - var pattern = "43 4,10,14,20 * * *"; // nice for tests "*/10 * * * * *" +function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } - eventBus.createCronJob("inventory-sync", {}, pattern, brightpearlService.syncInventory()); -}; +function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } + +var inventorySync = /*#__PURE__*/function () { + var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(container) { + var brightpearlService, eventBus, client, pattern; + return regeneratorRuntime.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + brightpearlService = container.resolve("brightpearlService"); + eventBus = container.resolve("eventBusService"); + _context.prev = 2; + _context.next = 5; + return brightpearlService.getClient(); + + case 5: + client = _context.sent; + pattern = "43 4,10,14,20 * * *"; // nice for tests "*/10 * * * * *" + + eventBus.createCronJob("inventory-sync", {}, pattern, brightpearlService.syncInventory()); + _context.next = 15; + break; + + case 10: + _context.prev = 10; + _context.t0 = _context["catch"](2); + + if (!(_context.t0.name === "not_allowed")) { + _context.next = 14; + break; + } + + return _context.abrupt("return"); + + case 14: + throw _context.t0; + + case 15: + case "end": + return _context.stop(); + } + } + }, _callee, null, [[2, 10]]); + })); + + return function inventorySync(_x) { + return _ref.apply(this, arguments); + }; +}(); var _default = inventorySync; exports["default"] = _default; \ No newline at end of file diff --git a/packages/medusa-plugin-brightpearl/loaders/webhooks.js b/packages/medusa-plugin-brightpearl/loaders/webhooks.js index 2373540143..b4616e9d82 100644 --- a/packages/medusa-plugin-brightpearl/loaders/webhooks.js +++ b/packages/medusa-plugin-brightpearl/loaders/webhooks.js @@ -11,21 +11,45 @@ function _asyncToGenerator(fn) { return function () { var self = this, args = ar var webhookLoader = /*#__PURE__*/function () { var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(container) { - var brightpearlService; + var brightpearlService, client; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: brightpearlService = container.resolve("brightpearlService"); - _context.next = 3; + _context.prev = 1; + _context.next = 4; + return brightpearlService.getClient(); + + case 4: + client = _context.sent; + _context.next = 7; return brightpearlService.verifyWebhooks(); - case 3: + case 7: + _context.next = 14; + break; + + case 9: + _context.prev = 9; + _context.t0 = _context["catch"](1); + + if (!(_context.t0.name === "not_allowed")) { + _context.next = 13; + break; + } + + return _context.abrupt("return"); + + case 13: + throw _context.t0; + + case 14: case "end": return _context.stop(); } } - }, _callee); + }, _callee, null, [[1, 9]]); })); return function webhookLoader(_x) { diff --git a/packages/medusa-plugin-brightpearl/src/loaders/inventory.js b/packages/medusa-plugin-brightpearl/src/loaders/inventory.js index 00f3751e02..97b44b189d 100644 --- a/packages/medusa-plugin-brightpearl/src/loaders/inventory.js +++ b/packages/medusa-plugin-brightpearl/src/loaders/inventory.js @@ -1,8 +1,22 @@ -const inventorySync = container => { +const inventorySync = async (container) => { const brightpearlService = container.resolve("brightpearlService") const eventBus = container.resolve("eventBusService") - const pattern = "43 4,10,14,20 * * *" // nice for tests "*/10 * * * * *" - eventBus.createCronJob("inventory-sync", {}, pattern, brightpearlService.syncInventory()) + + try { + const client = await brightpearlService.getClient() + const pattern = "43 4,10,14,20 * * *" // nice for tests "*/10 * * * * *" + eventBus.createCronJob( + "inventory-sync", + {}, + pattern, + brightpearlService.syncInventory() + ) + } catch (err) { + if (err.name === "not_allowed") { + return + } + throw err + } } export default inventorySync diff --git a/packages/medusa-plugin-brightpearl/src/loaders/webhooks.js b/packages/medusa-plugin-brightpearl/src/loaders/webhooks.js index ea78833176..eb9d1b0d24 100644 --- a/packages/medusa-plugin-brightpearl/src/loaders/webhooks.js +++ b/packages/medusa-plugin-brightpearl/src/loaders/webhooks.js @@ -1,6 +1,14 @@ const webhookLoader = async (container) => { const brightpearlService = container.resolve("brightpearlService") - await brightpearlService.verifyWebhooks() + try { + const client = await brightpearlService.getClient() + await brightpearlService.verifyWebhooks() + } catch (err) { + if (err.name === "not_allowed") { + return + } + throw err + } } export default webhookLoader diff --git a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js index 50594f5e77..d5bc031e43 100644 --- a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js @@ -1,3 +1,4 @@ +import { MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" import Brightpearl from "../utils/brightpearl" diff --git a/packages/medusa/src/api/routes/admin/orders/__tests__/return-order.js b/packages/medusa/src/api/routes/admin/orders/__tests__/return-order.js index 2b935fd227..4596b51715 100644 --- a/packages/medusa/src/api/routes/admin/orders/__tests__/return-order.js +++ b/packages/medusa/src/api/routes/admin/orders/__tests__/return-order.js @@ -14,20 +14,7 @@ describe("POST /admin/orders/:id/return", () => { payload: { items: [ { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("validId"), - }, - quantity: 1, - }, + item_id: IdMap.getId("existingLine"), quantity: 10, }, ], @@ -51,20 +38,7 @@ describe("POST /admin/orders/:id/return", () => { IdMap.getId("test-order"), [ { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("validId"), - }, - quantity: 1, - }, + item_id: IdMap.getId("existingLine"), quantity: 10, }, ] 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 0f7cc17b3b..46d35ec3b9 100644 --- a/packages/medusa/src/api/routes/admin/orders/return-order.js +++ b/packages/medusa/src/api/routes/admin/orders/return-order.js @@ -4,7 +4,13 @@ export default async (req, res) => { const { id } = req.params const schema = Validator.object().keys({ - items: Validator.array().required(), + items: Validator.array() + .items({ + item_id: Validator.string().required(), + quantity: Validator.number().required(), + }) + .required(), + refund: Validator.number().optional(), }) const { value, error } = schema.validate(req.body) diff --git a/packages/medusa/src/models/__mocks__/order.js b/packages/medusa/src/models/__mocks__/order.js index 5b8d16be3e..99ff8f1ea6 100644 --- a/packages/medusa/src/models/__mocks__/order.js +++ b/packages/medusa/src/models/__mocks__/order.js @@ -109,6 +109,9 @@ export const orders = { customer_id: IdMap.getId("test-customer"), payment_method: { provider_id: "default_provider", + data: { + hi: "hi", + }, }, shipping_methods: [ { @@ -133,6 +136,7 @@ export const orders = { orderToRefund: { _id: IdMap.getId("refund-order"), email: "oliver@test.dk", + tax_rate: 0.25, billing_address: { first_name: "Oli", last_name: "Medusa", diff --git a/packages/medusa/src/models/schemas/line-item.js b/packages/medusa/src/models/schemas/line-item.js index aafcc942d8..e434eb1c83 100644 --- a/packages/medusa/src/models/schemas/line-item.js +++ b/packages/medusa/src/models/schemas/line-item.js @@ -36,5 +36,6 @@ export default new mongoose.Schema({ content: { type: mongoose.Schema.Types.Mixed, required: true }, quantity: { type: Number, required: true }, returned: { type: Boolean, default: false }, + returned_quantity: { type: Number, default: 0 }, metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, }) diff --git a/packages/medusa/src/services/__tests__/event-bus.js b/packages/medusa/src/services/__tests__/event-bus.js index 1c9e1f4c5f..14867ebc42 100644 --- a/packages/medusa/src/services/__tests__/event-bus.js +++ b/packages/medusa/src/services/__tests__/event-bus.js @@ -22,7 +22,7 @@ describe("EventBusService", () => { }) it("creates bull queue", () => { - expect(Bull).toHaveBeenCalledTimes(1) + expect(Bull).toHaveBeenCalledTimes(2) expect(Bull).toHaveBeenCalledWith("EventBusService:queue", "testhost") }) }) diff --git a/packages/medusa/src/services/__tests__/order.js b/packages/medusa/src/services/__tests__/order.js index feb9f78b7c..20af230dae 100644 --- a/packages/medusa/src/services/__tests__/order.js +++ b/packages/medusa/src/services/__tests__/order.js @@ -2,7 +2,10 @@ import { IdMap } from "medusa-test-utils" import { OrderModelMock, orders } from "../../models/__mocks__/order" import { carts } from "../../models/__mocks__/cart" import OrderService from "../order" -import { PaymentProviderServiceMock } from "../__mocks__/payment-provider" +import { + PaymentProviderServiceMock, + DefaultProviderMock, +} from "../__mocks__/payment-provider" import { DiscountServiceMock } from "../__mocks__/discount" import { FulfillmentProviderServiceMock } from "../__mocks__/fulfillment-provider" import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile" @@ -53,6 +56,7 @@ describe("OrderService", () => { ...carts.completeCart, currency_code: "eur", cart_id: carts.completeCart._id, + tax_rate: 0.25, } delete order._id delete order.payment_sessions @@ -112,6 +116,7 @@ describe("OrderService", () => { ], currency_code: "eur", cart_id: carts.withGiftCard._id, + tax_rate: 0.25, } delete order._id @@ -359,6 +364,7 @@ describe("OrderService", () => { const orderService = new OrderService({ orderModel: OrderModelMock, paymentProviderService: PaymentProviderServiceMock, + eventBusService: EventBusServiceMock, }) beforeEach(async () => { @@ -439,6 +445,7 @@ describe("OrderService", () => { orderModel: OrderModelMock, paymentProviderService: PaymentProviderServiceMock, totalsService: TotalsServiceMock, + eventBusService: EventBusServiceMock, }) beforeEach(async () => { @@ -448,20 +455,7 @@ describe("OrderService", () => { it("calls order model functions", async () => { await orderService.return(IdMap.getId("processed-order"), [ { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 123, - variant: { - _id: IdMap.getId("can-cover"), - }, - product: { - _id: IdMap.getId("validId"), - }, - quantity: 1, - }, + item_id: IdMap.getId("existingLine"), quantity: 10, }, ]) @@ -489,31 +483,75 @@ describe("OrderService", () => { returned_quantity: 10, thumbnail: "test-img-yeah.com/thumb", title: "merge line", + returned: true, }, ], fulfillment_status: "returned", }, } ) + + expect(DefaultProviderMock.refundPayment).toHaveBeenCalledTimes(1) + expect(DefaultProviderMock.refundPayment).toHaveBeenCalledWith( + { hi: "hi" }, + 1230 + ) }) - it("calls order model functions and sets partially_fulfilled", async () => { + it("return with custom refund", async () => { + await orderService.return( + IdMap.getId("processed-order"), + [ + { + item_id: IdMap.getId("existingLine"), + quantity: 10, + }, + ], + 102 + ) + + expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) + expect(OrderModelMock.updateOne).toHaveBeenCalledWith( + { _id: IdMap.getId("processed-order") }, + { + $set: { + items: [ + { + _id: IdMap.getId("existingLine"), + content: { + product: { + _id: IdMap.getId("validId"), + }, + quantity: 1, + unit_price: 123, + variant: { + _id: IdMap.getId("can-cover"), + }, + }, + description: "This is a new line", + quantity: 10, + returned_quantity: 10, + thumbnail: "test-img-yeah.com/thumb", + title: "merge line", + returned: true, + }, + ], + fulfillment_status: "returned", + }, + } + ) + + expect(DefaultProviderMock.refundPayment).toHaveBeenCalledTimes(1) + expect(DefaultProviderMock.refundPayment).toHaveBeenCalledWith( + { hi: "hi" }, + 102 + ) + }) + + it("calls order model functions and sets partially_returned", async () => { await orderService.return(IdMap.getId("order-refund"), [ { - _id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 100, - variant: { - _id: IdMap.getId("eur-8-us-10"), - }, - product: { - _id: IdMap.getId("product"), - }, - quantity: 1, - }, + item_id: IdMap.getId("existingLine"), quantity: 2, }, ]) @@ -538,6 +576,7 @@ describe("OrderService", () => { }, description: "This is a new line", quantity: 10, + returned: false, returned_quantity: 2, thumbnail: "test-img-yeah.com/thumb", title: "merge line", @@ -560,7 +599,7 @@ describe("OrderService", () => { quantity: 10, }, ], - fulfillment_status: "partially_fulfilled", + fulfillment_status: "partially_returned", }, } ) @@ -568,7 +607,7 @@ describe("OrderService", () => { it("throws if payment is already processed", async () => { try { - await orderService.return(IdMap.getId("fulfilled-order")) + await orderService.return(IdMap.getId("fulfilled-order"), []) } catch (error) { expect(error.message).toEqual( "Can't return an order with payment unprocessed" @@ -578,7 +617,7 @@ describe("OrderService", () => { it("throws if return is attempted on unfulfilled order", async () => { try { - await orderService.return(IdMap.getId("not-fulfilled-order")) + await orderService.return(IdMap.getId("not-fulfilled-order"), []) } catch (error) { expect(error.message).toEqual( "Can't return an unfulfilled or already returned order" diff --git a/packages/medusa/src/services/__tests__/totals.js b/packages/medusa/src/services/__tests__/totals.js index 2a7a2ab75f..f43f9e57cc 100644 --- a/packages/medusa/src/services/__tests__/totals.js +++ b/packages/medusa/src/services/__tests__/totals.js @@ -193,7 +193,7 @@ describe("TotalsService", () => { description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", content: { - unit_price: 123, + unit_price: 100, variant: { _id: IdMap.getId("can-cover"), }, @@ -206,7 +206,7 @@ describe("TotalsService", () => { }, ]) - expect(res).toEqual(1107) + expect(res).toEqual(1125) }) it("calculates refund with total fixed discount", async () => { @@ -218,7 +218,7 @@ describe("TotalsService", () => { description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", content: { - unit_price: 123, + unit_price: 100, variant: { _id: IdMap.getId("can-cover"), }, @@ -231,7 +231,7 @@ describe("TotalsService", () => { }, ]) - expect(res).toEqual(359) + expect(res).toEqual(373.125) }) it("calculates refund with item fixed discount", async () => { @@ -243,7 +243,7 @@ describe("TotalsService", () => { description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", content: { - unit_price: 123, + unit_price: 100, variant: { _id: IdMap.getId("eur-8-us-10"), }, @@ -256,7 +256,7 @@ describe("TotalsService", () => { }, ]) - expect(res).toEqual(363) + expect(res).toEqual(367.5) }) it("calculates refund with item percentage discount", async () => { @@ -268,7 +268,7 @@ describe("TotalsService", () => { description: "This is a new line", thumbnail: "test-img-yeah.com/thumb", content: { - unit_price: 123, + unit_price: 100, variant: { _id: IdMap.getId("eur-8-us-10"), }, @@ -281,7 +281,7 @@ describe("TotalsService", () => { }, ]) - expect(res).toEqual(332.1) + expect(res).toEqual(337.5) }) it("throws if line items to return is not in order", async () => { diff --git a/packages/medusa/src/services/order.js b/packages/medusa/src/services/order.js index c261b0cb44..f1c9b02037 100644 --- a/packages/medusa/src/services/order.js +++ b/packages/medusa/src/services/order.js @@ -7,6 +7,7 @@ class OrderService extends BaseService { GIFT_CARD_CREATED: "order.gift_card_created", PAYMENT_CAPTURED: "order.payment_captured", SHIPMENT_CREATED: "order.shipment_created", + ITEMS_RETURNED: "order.items_returned", PLACED: "order.placed", UPDATED: "order.updated", CANCELLED: "order.cancelled", @@ -218,28 +219,28 @@ class OrderService extends BaseService { */ async completeOrder(orderId) { const order = await this.retrieve(orderId) - this.orderModel_ + + // Capture the payment + await this.capturePayment(orderId) + + // Run all other registered events + const completeOrderJob = await this.eventBus_.emit( + OrderService.Events.COMPLETED, + result + ) + + await completeOrderJob.finished().catch(error => { + throw error + }) + + return this.orderModel_ .updateOne( { _id: order._id }, { $set: { status: "completed" }, } ) - .then(async result => { - const completeOrderJob = await this.eventBus_.emit( - OrderService.Events.COMPLETED, - result - ) - - return completeOrderJob - .finished() - .then(async () => { - return this.retrieve(order._id) - }) - .catch(error => { - throw error - }) - }) + .then(async result => {}) } /** @@ -672,9 +673,25 @@ class OrderService extends BaseService { * @param {string[]} lineItems - the line items to return * @return {Promise} the result of the update operation */ - async return(orderId, lineItems) { + async return(orderId, lineItems, refundAmount) { const order = await this.retrieve(orderId) + // Find the lines to return + const returnLines = lineItems.map(({ item_id, quantity }) => { + const item = order.items.find(i => i._id.equals(item_id)) + if (!item) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Return contains invalid line item" + ) + } + + return { + ...item, + quantity, + } + }) + if ( order.fulfillment_status === "not_fulfilled" || order.fulfillment_status === "returned" @@ -697,31 +714,50 @@ class OrderService extends BaseService { provider_id ) - const amount = this.totalsService_.getRefundTotal(order, lineItems) + const amount = + refundAmount || this.totalsService_.getRefundTotal(order, returnLines) await paymentProvider.refundPayment(data, amount) - lineItems.map(item => { - const returnedItem = order.items.find(({ _id }) => _id === item._id) - if (returnedItem) { - returnedItem.returned_quantity = item.quantity + let isFullReturn = true + const newItems = order.items.map(i => { + const isReturn = returnLines.find(r => r._id.equals(i._id)) + if (isReturn) { + let returned = false + if (i.quantity === isReturn.quantity) { + returned = true + } + return { + ...i, + returned_quantity: isReturn.quantity, + returned, + } + } else { + isFullReturn = false + return i } }) - const fullReturn = order.items.every( - item => item.quantity === item.returned_quantity - ) - - return this.orderModel_.updateOne( - { - _id: orderId, - }, - { - $set: { - items: order.items, - fulfillment_status: fullReturn ? "returned" : "partially_fulfilled", + return this.orderModel_ + .updateOne( + { + _id: orderId, }, - } - ) + { + $set: { + items: newItems, + fulfillment_status: isFullReturn + ? "returned" + : "partially_returned", + }, + } + ) + .then(result => { + this.eventBus_.emit(OrderService.Events.ITEMS_RETURNED, { + order: result, + items: returnLines, + }) + return result + }) } /** @@ -768,6 +804,14 @@ class OrderService extends BaseService { if (expandFields.includes("region")) { o.region = await this.regionService_.retrieve(order.region_id) } + + o.items = o.items.map(i => { + return { + ...i, + refundable: this.totalsService_.getLineItemRefund(o, i), + } + }) + return o } diff --git a/packages/medusa/src/services/totals.js b/packages/medusa/src/services/totals.js index 40ee9d98ce..976de7f863 100644 --- a/packages/medusa/src/services/totals.js +++ b/packages/medusa/src/services/totals.js @@ -80,6 +80,32 @@ class TotalsService extends BaseService { return (subtotal - discountTotal + shippingTotal) * tax_rate } + getLineItemRefund(order, lineItem) { + const { tax_rate, discounts } = order + const taxRate = tax_rate || 0 + + const discount = discounts.find( + ({ discount_rule }) => discount_rule.type !== "free_shipping" + ) + + if (!discount) { + return lineItem.content.unit_price * lineItem.quantity * (1 + taxRate) + } + + const lineDiscounts = this.getLineDiscounts(order, discount) + const discountedLine = lineDiscounts.find(line => + line.item._id.equals(lineItem._id) + ) + + const discountAmount = + (discountedLine.amount / discountedLine.item.quantity) * lineItem.quantity + + return ( + (lineItem.content.unit_price * lineItem.quantity - discountAmount) * + (1 + taxRate) + ) + } + /** * Calculates refund total of line items. * If any of the items to return have been discounted, we need to @@ -88,101 +114,9 @@ class TotalsService extends BaseService { * @param {[LineItem]} lineItems - * @return {int} the calculated subtotal */ - async getRefundTotal(order, lineItems) { - const discount = order.discounts.find( - ({ discount_rule }) => discount_rule.type !== "free_shipping" - ) - - if (_.differenceBy(lineItems, order.items, "_id").length !== 0) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "Line items does not exist on order" - ) - } - - const subtotal = this.getSubtotal({ items: lineItems }) - - const region = await this.regionService_.retrieve(order.region_id) - - // if nothing is discounted, return the subtotal of line items - if (!discount) { - return subtotal * (1 + region.tax_rate) - } - - const { value, type, allocation } = discount.discount_rule - - if (type === "percentage" && allocation === "total") { - const discountTotal = (subtotal / 100) * value - return subtotal - discountTotal - } - - if (type === "fixed" && allocation === "total") { - return subtotal - value - } - - if (type === "percentage" && allocation === "item") { - // Find discounted items - const itemPercentageDiscounts = await this.getAllocationItemDiscounts( - discount, - { items: lineItems }, - "percentage" - ) - - // Find discount total by taking each discounted item, reducing it by - // its discount value. Then summing all those items together. - const discountRefundTotal = _.sumBy( - itemPercentageDiscounts, - d => d.lineItem.content.unit_price * d.lineItem.quantity - d.amount - ) - - // Find the items that weren't discounted - const notDiscountedItems = _.differenceBy( - lineItems, - Array.from(itemPercentageDiscounts, el => el.lineItem), - "_id" - ) - - // If all items were discounted, we return the total of the discounted - // items - if (!notDiscountedItems) { - return discountRefundTotal - } - - // Otherwise, we find the total those not discounted - const notDiscRefundTotal = this.getSubtotal({ items: notDiscountedItems }) - - // Finally, return the sum of discounted and not discounted items - return notDiscRefundTotal + discountRefundTotal - } - - // See immediate `if`-statement above for a elaboration on the following - // calculations. This time with fixed discount type. - if (type === "fixed" && allocation === "item") { - const itemPercentageDiscounts = await this.getAllocationItemDiscounts( - discount, - { items: lineItems }, - "fixed" - ) - - const discountRefundTotal = _.sumBy( - itemPercentageDiscounts, - d => d.lineItem.content.unit_price * d.lineItem.quantity - d.amount - ) - - const notDiscountedItems = _.differenceBy( - lineItems, - Array.from(itemPercentageDiscounts, el => el.lineItem), - "_id" - ) - - if (!notDiscountedItems) { - return notDiscRefundTotal - } - - const notDiscRefundTotal = this.getSubtotal({ items: notDiscountedItems }) - - return notDiscRefundTotal + discountRefundTotal - } + getRefundTotal(order, lineItems) { + const refunds = lineItems.map(i => this.getLineItemRefund(order, i)) + return refunds.reduce((acc, next) => acc + next, 0) } /** @@ -225,7 +159,7 @@ class TotalsService extends BaseService { * @return {[{ string, string, int }]} array of triples of lineitem, variant * and applied discount */ - async getAllocationItemDiscounts(discount, cart) { + getAllocationItemDiscounts(discount, cart) { const discounts = [] for (const item of cart.items) { if (discount.discount_rule.valid_for.length > 0) { @@ -252,7 +186,7 @@ class TotalsService extends BaseService { return discounts } - async getLineDiscounts(cart, discount) { + getLineDiscounts(cart, discount) { const subtotal = this.getSubtotal(cart) const { type, allocation, value } = discount.discount_rule if (allocation === "total") { diff --git a/packages/medusa/src/subscribers/order.js b/packages/medusa/src/subscribers/order.js index 3f63381c65..aa2fd2c8a6 100644 --- a/packages/medusa/src/subscribers/order.js +++ b/packages/medusa/src/subscribers/order.js @@ -19,14 +19,6 @@ class OrderSubscriber { this.eventBus_ = eventBusService - this.eventBus_.subscribe("order.completed", async order => { - const paymentProvider = this.paymentProviderService_.retrieveProvider( - order.payment_method.provider_id - ) - - await paymentProvider.capturePayment(order._id) - }) - this.eventBus_.subscribe("order.placed", async order => { await this.customerService_.addOrder(order.customer_id, order._id)