From bc5ff91a024b3ae6a8bcfafeeae67e76965255c1 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Mon, 29 Jun 2020 10:23:24 +0200 Subject: [PATCH] Adds Store Service to control store settings (#76) Also adds support for `projectConfig.admin_cors` & `projectConfig.store_cors` --- .../medusa-payment-stripe/__mocks__/cart.js | 193 ++++++++++++++++++ .../__mocks__/customer.js | 45 ++++ .../__mocks__/eventbus.js | 16 ++ .../medusa-payment-stripe/__mocks__/stripe.js | 96 +++++++++ .../medusa-payment-stripe/__mocks__/totals.js | 15 ++ packages/medusa/src/api/index.js | 6 +- packages/medusa/src/api/routes/admin/index.js | 18 +- .../admin/store/__tests__/add-currency.js | 28 +++ .../routes/admin/store/__tests__/get-store.js | 28 +++ .../admin/store/__tests__/remove-currency.js | 28 +++ .../admin/store/__tests__/update-store.js | 63 ++++++ .../api/routes/admin/store/add-currency.js | 11 + .../src/api/routes/admin/store/get-store.js | 9 + .../src/api/routes/admin/store/index.js | 21 ++ .../api/routes/admin/store/remove-currency.js | 11 + .../api/routes/admin/store/update-store.js | 21 ++ packages/medusa/src/api/routes/store/index.js | 11 +- packages/medusa/src/loaders/api.js | 10 +- packages/medusa/src/loaders/express.js | 3 - packages/medusa/src/loaders/index.js | 2 +- packages/medusa/src/loaders/plugins.js | 14 +- packages/medusa/src/models/__mocks__/store.js | 17 ++ packages/medusa/src/models/store.js | 13 ++ .../medusa/src/services/__mocks__/store.js | 28 +++ .../medusa/src/services/__tests__/region.js | 5 + .../medusa/src/services/__tests__/store.js | 127 ++++++++++++ packages/medusa/src/services/region.js | 13 +- packages/medusa/src/services/store.js | 175 ++++++++++++++++ 28 files changed, 1000 insertions(+), 27 deletions(-) create mode 100644 packages/medusa-payment-stripe/__mocks__/cart.js create mode 100644 packages/medusa-payment-stripe/__mocks__/customer.js create mode 100644 packages/medusa-payment-stripe/__mocks__/eventbus.js create mode 100644 packages/medusa-payment-stripe/__mocks__/stripe.js create mode 100644 packages/medusa-payment-stripe/__mocks__/totals.js create mode 100644 packages/medusa/src/api/routes/admin/store/__tests__/add-currency.js create mode 100644 packages/medusa/src/api/routes/admin/store/__tests__/get-store.js create mode 100644 packages/medusa/src/api/routes/admin/store/__tests__/remove-currency.js create mode 100644 packages/medusa/src/api/routes/admin/store/__tests__/update-store.js create mode 100644 packages/medusa/src/api/routes/admin/store/add-currency.js create mode 100644 packages/medusa/src/api/routes/admin/store/get-store.js create mode 100644 packages/medusa/src/api/routes/admin/store/index.js create mode 100644 packages/medusa/src/api/routes/admin/store/remove-currency.js create mode 100644 packages/medusa/src/api/routes/admin/store/update-store.js create mode 100644 packages/medusa/src/models/__mocks__/store.js create mode 100644 packages/medusa/src/models/store.js create mode 100644 packages/medusa/src/services/__mocks__/store.js create mode 100644 packages/medusa/src/services/__tests__/store.js create mode 100644 packages/medusa/src/services/store.js diff --git a/packages/medusa-payment-stripe/__mocks__/cart.js b/packages/medusa-payment-stripe/__mocks__/cart.js new file mode 100644 index 0000000000..25a3734edb --- /dev/null +++ b/packages/medusa-payment-stripe/__mocks__/cart.js @@ -0,0 +1,193 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = exports.CartServiceMock = exports.carts = void 0; + +var _medusaTestUtils = require("medusa-test-utils"); + +var carts = { + emptyCart: { + _id: _medusaTestUtils.IdMap.getId("emptyCart"), + items: [], + region_id: _medusaTestUtils.IdMap.getId("testRegion"), + shipping_options: [{ + _id: _medusaTestUtils.IdMap.getId("freeShipping"), + profile_id: "default_profile", + data: { + some_data: "yes" + } + }] + }, + frCart: { + _id: _medusaTestUtils.IdMap.getId("fr-cart"), + email: "lebron@james.com", + title: "test", + region_id: _medusaTestUtils.IdMap.getId("region-france"), + items: [{ + _id: _medusaTestUtils.IdMap.getId("line"), + title: "merge line", + description: "This is a new line", + thumbnail: "test-img-yeah.com/thumb", + content: [{ + unit_price: 8, + variant: { + _id: _medusaTestUtils.IdMap.getId("eur-8-us-10") + }, + product: { + _id: _medusaTestUtils.IdMap.getId("product") + }, + quantity: 1 + }, { + unit_price: 10, + variant: { + _id: _medusaTestUtils.IdMap.getId("eur-10-us-12") + }, + product: { + _id: _medusaTestUtils.IdMap.getId("product") + }, + quantity: 1 + }], + quantity: 10 + }, { + _id: _medusaTestUtils.IdMap.getId("existingLine"), + title: "merge line", + description: "This is a new line", + thumbnail: "test-img-yeah.com/thumb", + content: { + unit_price: 10, + variant: { + _id: _medusaTestUtils.IdMap.getId("eur-10-us-12") + }, + product: { + _id: _medusaTestUtils.IdMap.getId("product") + }, + quantity: 1 + }, + quantity: 10 + }], + shipping_methods: [{ + _id: _medusaTestUtils.IdMap.getId("freeShipping"), + profile_id: "default_profile" + }], + shipping_options: [{ + _id: _medusaTestUtils.IdMap.getId("freeShipping"), + profile_id: "default_profile" + }], + payment_sessions: [{ + provider_id: "stripe", + data: { + id: "pi_123456789", + customer: _medusaTestUtils.IdMap.getId("not-lebron") + } + }], + payment_method: { + provider_id: "stripe", + data: { + id: "pi_123456789", + customer: _medusaTestUtils.IdMap.getId("not-lebron") + } + }, + shipping_address: {}, + billing_address: {}, + discounts: [], + customer_id: _medusaTestUtils.IdMap.getId("lebron") + }, + frCartNoStripeCustomer: { + _id: _medusaTestUtils.IdMap.getId("fr-cart-no-customer"), + title: "test", + region_id: _medusaTestUtils.IdMap.getId("region-france"), + items: [{ + _id: _medusaTestUtils.IdMap.getId("line"), + title: "merge line", + description: "This is a new line", + thumbnail: "test-img-yeah.com/thumb", + content: [{ + unit_price: 8, + variant: { + _id: _medusaTestUtils.IdMap.getId("eur-8-us-10") + }, + product: { + _id: _medusaTestUtils.IdMap.getId("product") + }, + quantity: 1 + }, { + unit_price: 10, + variant: { + _id: _medusaTestUtils.IdMap.getId("eur-10-us-12") + }, + product: { + _id: _medusaTestUtils.IdMap.getId("product") + }, + quantity: 1 + }], + quantity: 10 + }, { + _id: _medusaTestUtils.IdMap.getId("existingLine"), + title: "merge line", + description: "This is a new line", + thumbnail: "test-img-yeah.com/thumb", + content: { + unit_price: 10, + variant: { + _id: _medusaTestUtils.IdMap.getId("eur-10-us-12") + }, + product: { + _id: _medusaTestUtils.IdMap.getId("product") + }, + quantity: 1 + }, + quantity: 10 + }], + shipping_methods: [{ + _id: _medusaTestUtils.IdMap.getId("freeShipping"), + profile_id: "default_profile" + }], + shipping_options: [{ + _id: _medusaTestUtils.IdMap.getId("freeShipping"), + profile_id: "default_profile" + }], + payment_sessions: [{ + provider_id: "stripe", + data: { + id: "pi_123456789", + customer: _medusaTestUtils.IdMap.getId("not-lebron") + } + }], + payment_method: { + provider_id: "stripe", + data: { + id: "pi_123456789", + customer: _medusaTestUtils.IdMap.getId("not-lebron") + } + }, + shipping_address: {}, + billing_address: {}, + discounts: [], + customer_id: _medusaTestUtils.IdMap.getId("vvd") + } +}; +exports.carts = carts; +var CartServiceMock = { + retrieve: jest.fn().mockImplementation(function (cartId) { + if (cartId === _medusaTestUtils.IdMap.getId("fr-cart")) { + return Promise.resolve(carts.frCart); + } + + if (cartId === _medusaTestUtils.IdMap.getId("emptyCart")) { + return Promise.resolve(carts.emptyCart); + } + + return Promise.resolve(undefined); + }), + updatePaymentSession: jest.fn().mockImplementation(function (cartId, stripe, paymentIntent) { + return Promise.resolve(); + }) +}; +exports.CartServiceMock = CartServiceMock; +var mock = jest.fn().mockImplementation(function () { + return CartServiceMock; +}); +var _default = mock; +exports["default"] = _default; \ No newline at end of file diff --git a/packages/medusa-payment-stripe/__mocks__/customer.js b/packages/medusa-payment-stripe/__mocks__/customer.js new file mode 100644 index 0000000000..72cd299080 --- /dev/null +++ b/packages/medusa-payment-stripe/__mocks__/customer.js @@ -0,0 +1,45 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = exports.CustomerServiceMock = void 0; + +var _medusaTestUtils = require("medusa-test-utils"); + +var CustomerServiceMock = { + retrieve: jest.fn().mockImplementation(function (id) { + if (id === _medusaTestUtils.IdMap.getId("lebron")) { + return Promise.resolve({ + _id: _medusaTestUtils.IdMap.getId("lebron"), + first_name: "LeBron", + last_name: "James", + email: "lebron@james.com", + password_hash: "1234", + metadata: { + stripe_id: "cus_123456789_new" + } + }); + } + + if (id === _medusaTestUtils.IdMap.getId("vvd")) { + return Promise.resolve({ + _id: _medusaTestUtils.IdMap.getId("vvd"), + first_name: "Virgil", + last_name: "Van Dijk", + email: "virg@vvd.com", + password_hash: "1234", + metadata: {} + }); + } + + return Promise.resolve(undefined); + }), + setMetadata: jest.fn().mockReturnValue(Promise.resolve()) +}; +exports.CustomerServiceMock = CustomerServiceMock; +var mock = jest.fn().mockImplementation(function () { + return CustomerServiceMock; +}); +var _default = mock; +exports["default"] = _default; \ No newline at end of file diff --git a/packages/medusa-payment-stripe/__mocks__/eventbus.js b/packages/medusa-payment-stripe/__mocks__/eventbus.js new file mode 100644 index 0000000000..b445554189 --- /dev/null +++ b/packages/medusa-payment-stripe/__mocks__/eventbus.js @@ -0,0 +1,16 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = exports.EventBusServiceMock = void 0; +var EventBusServiceMock = { + emit: jest.fn(), + subscribe: jest.fn() +}; +exports.EventBusServiceMock = EventBusServiceMock; +var mock = jest.fn().mockImplementation(function () { + return EventBusServiceMock; +}); +var _default = mock; +exports["default"] = _default; \ No newline at end of file diff --git a/packages/medusa-payment-stripe/__mocks__/stripe.js b/packages/medusa-payment-stripe/__mocks__/stripe.js new file mode 100644 index 0000000000..0e24cd810c --- /dev/null +++ b/packages/medusa-payment-stripe/__mocks__/stripe.js @@ -0,0 +1,96 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = exports.StripeMock = void 0; +var StripeMock = { + customers: { + create: jest.fn().mockImplementation(function (data) { + if (data.email === "virg@vvd.com") { + return Promise.resolve({ + id: "cus_vvd", + email: "virg@vvd.com" + }); + } + + if (data.email === "lebron@james.com") { + return Promise.resolve({ + id: "cus_lebron", + email: "lebron@james.com" + }); + } + }) + }, + paymentIntents: { + create: jest.fn().mockImplementation(function (data) { + if (data.customer === "cus_123456789_new") { + return Promise.resolve({ + id: "pi_lebron", + amount: 100, + customer: "cus_123456789_new" + }); + } + + if (data.customer === "cus_lebron") { + return Promise.resolve({ + id: "pi_lebron", + amount: 100, + customer: "cus_lebron" + }); + } + }), + retrieve: jest.fn().mockImplementation(function (data) { + return Promise.resolve({ + id: "pi_lebron", + customer: "cus_lebron" + }); + }), + update: jest.fn().mockImplementation(function (pi, data) { + if (data.customer === "cus_lebron_2") { + return Promise.resolve({ + id: "pi_lebron", + customer: "cus_lebron_2", + amount: 1000 + }); + } + + return Promise.resolve({ + id: "pi_lebron", + customer: "cus_lebron", + amount: 1000 + }); + }), + capture: jest.fn().mockImplementation(function (data) { + return Promise.resolve({ + id: "pi_lebron", + customer: "cus_lebron", + amount: 1000, + status: "succeeded" + }); + }), + cancel: jest.fn().mockImplementation(function (data) { + return Promise.resolve({ + id: "pi_lebron", + customer: "cus_lebron", + status: "cancelled" + }); + }) + }, + refunds: { + create: jest.fn().mockImplementation(function (data) { + return Promise.resolve({ + id: "re_123", + payment_intent: "pi_lebron", + amount: 1000, + status: "succeeded" + }); + }) + } +}; +exports.StripeMock = StripeMock; +var stripe = jest.fn(function () { + return StripeMock; +}); +var _default = stripe; +exports["default"] = _default; \ No newline at end of file diff --git a/packages/medusa-payment-stripe/__mocks__/totals.js b/packages/medusa-payment-stripe/__mocks__/totals.js new file mode 100644 index 0000000000..576d1231e9 --- /dev/null +++ b/packages/medusa-payment-stripe/__mocks__/totals.js @@ -0,0 +1,15 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = exports.TotalsServiceMock = void 0; +var TotalsServiceMock = { + getTotal: jest.fn() +}; +exports.TotalsServiceMock = TotalsServiceMock; +var mock = jest.fn().mockImplementation(function () { + return TotalsServiceMock; +}); +var _default = mock; +exports["default"] = _default; \ No newline at end of file diff --git a/packages/medusa/src/api/index.js b/packages/medusa/src/api/index.js index d409a4e2c7..4d0b4b9002 100644 --- a/packages/medusa/src/api/index.js +++ b/packages/medusa/src/api/index.js @@ -4,11 +4,11 @@ import store from "./routes/store" import errorHandler from "./middlewares/error-handler" // guaranteed to get dependencies -export default container => { +export default (container, config) => { const app = Router() - admin(app, container) - store(app, container) + admin(app, container, config) + store(app, container, config) app.use(errorHandler()) diff --git a/packages/medusa/src/api/routes/admin/index.js b/packages/medusa/src/api/routes/admin/index.js index 814421aa8a..ca92c3d105 100644 --- a/packages/medusa/src/api/routes/admin/index.js +++ b/packages/medusa/src/api/routes/admin/index.js @@ -1,4 +1,6 @@ import { Router } from "express" +import cors from "cors" + import middlewares from "../../middlewares" import authRoutes from "./auth" import productRoutes from "./products" @@ -8,17 +10,25 @@ import shippingOptionRoutes from "./shipping-options" import shippingProfileRoutes from "./shipping-profiles" import discountRoutes from "./discounts" import orderRoutes from "./orders" +import storeRoutes from "./store" const route = Router() -export default (app, container) => { - const middlewareService = container.resolve("middlewareService") - +export default (app, container, config) => { app.use("/admin", route) + const adminCors = config.admin_cors || "" + route.use( + cors({ + origin: adminCors.split(","), + credentials: true, + }) + ) + // Unauthenticated routes authRoutes(route) + const middlewareService = container.resolve("middlewareService") // Calls all middleware that has been registered to run before authentication. middlewareService.usePreAuthentication(app) @@ -35,6 +45,8 @@ export default (app, container) => { shippingProfileRoutes(route) discountRoutes(route) orderRoutes(route) + productVariantRoutes(route) + storeRoutes(route) return app } diff --git a/packages/medusa/src/api/routes/admin/store/__tests__/add-currency.js b/packages/medusa/src/api/routes/admin/store/__tests__/add-currency.js new file mode 100644 index 0000000000..328545be47 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/store/__tests__/add-currency.js @@ -0,0 +1,28 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { StoreServiceMock } from "../../../../../services/__mocks__/store" + +describe("POST /admin/store/currencies/:currency_code", () => { + describe("successful addition", () => { + let subject + + beforeAll(async () => { + subject = await request("POST", `/admin/store/currencies/dkk`, { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + }) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls service retrieve", () => { + expect(StoreServiceMock.addCurrency).toHaveBeenCalledTimes(1) + expect(StoreServiceMock.addCurrency).toHaveBeenCalledWith("dkk") + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/store/__tests__/get-store.js b/packages/medusa/src/api/routes/admin/store/__tests__/get-store.js new file mode 100644 index 0000000000..45348ad475 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/store/__tests__/get-store.js @@ -0,0 +1,28 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { StoreServiceMock } from "../../../../../services/__mocks__/store" + +describe("GET /admin/store", () => { + describe("successful addition", () => { + let subject + + beforeAll(async () => { + subject = await request("GET", `/admin/store`, { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + }) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls service retrieve", () => { + expect(StoreServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(StoreServiceMock.retrieve).toHaveBeenCalledWith() + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/store/__tests__/remove-currency.js b/packages/medusa/src/api/routes/admin/store/__tests__/remove-currency.js new file mode 100644 index 0000000000..bdb606fc3c --- /dev/null +++ b/packages/medusa/src/api/routes/admin/store/__tests__/remove-currency.js @@ -0,0 +1,28 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { StoreServiceMock } from "../../../../../services/__mocks__/store" + +describe("DELETE /admin/store/currencies/:currency_code", () => { + describe("successful addition", () => { + let subject + + beforeAll(async () => { + subject = await request("DELETE", `/admin/store/currencies/dkk`, { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + }) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls service retrieve", () => { + expect(StoreServiceMock.removeCurrency).toHaveBeenCalledTimes(1) + expect(StoreServiceMock.removeCurrency).toHaveBeenCalledWith("dkk") + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/store/__tests__/update-store.js b/packages/medusa/src/api/routes/admin/store/__tests__/update-store.js new file mode 100644 index 0000000000..b9f9ca5cf8 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/store/__tests__/update-store.js @@ -0,0 +1,63 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { StoreServiceMock } from "../../../../../services/__mocks__/store" + +describe("POST /admin/store", () => { + describe("successful creation", () => { + let subject + + beforeAll(async () => { + jest.clearAllMocks() + subject = await request("POST", "/admin/store", { + payload: { + name: "New Name", + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + }) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls service update", () => { + expect(StoreServiceMock.update).toHaveBeenCalledTimes(1) + expect(StoreServiceMock.update).toHaveBeenCalledWith({ + name: "New Name", + }) + }) + }) + + describe("successful creation", () => { + let subject + + beforeAll(async () => { + jest.clearAllMocks() + subject = await request("POST", "/admin/store", { + payload: { + currencies: ["DKK", "USD"], + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + }) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls service update", () => { + expect(StoreServiceMock.update).toHaveBeenCalledTimes(1) + expect(StoreServiceMock.update).toHaveBeenCalledWith({ + currencies: ["DKK", "USD"], + }) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/store/add-currency.js b/packages/medusa/src/api/routes/admin/store/add-currency.js new file mode 100644 index 0000000000..270782bd2d --- /dev/null +++ b/packages/medusa/src/api/routes/admin/store/add-currency.js @@ -0,0 +1,11 @@ +export default async (req, res) => { + const { currency_code } = req.params + + try { + const storeService = req.scope.resolve("storeService") + const data = await storeService.addCurrency(currency_code) + res.status(200).json({ store: data }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/store/get-store.js b/packages/medusa/src/api/routes/admin/store/get-store.js new file mode 100644 index 0000000000..909443b444 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/store/get-store.js @@ -0,0 +1,9 @@ +export default async (req, res) => { + try { + const storeService = req.scope.resolve("storeService") + const data = await storeService.retrieve() + res.status(200).json({ store: data }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/store/index.js b/packages/medusa/src/api/routes/admin/store/index.js new file mode 100644 index 0000000000..2bd8039bd7 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/store/index.js @@ -0,0 +1,21 @@ +import { Router } from "express" +import middlewares from "../../../middlewares" + +const route = Router() + +export default app => { + app.use("/store", route) + + route.get("/", middlewares.wrap(require("./get-store").default)) + route.post("/", middlewares.wrap(require("./update-store").default)) + route.post( + "/currencies/:currency_code", + middlewares.wrap(require("./add-currency").default) + ) + route.delete( + "/currencies/:currency_code", + middlewares.wrap(require("./remove-currency").default) + ) + + return app +} diff --git a/packages/medusa/src/api/routes/admin/store/remove-currency.js b/packages/medusa/src/api/routes/admin/store/remove-currency.js new file mode 100644 index 0000000000..4b73f37c3d --- /dev/null +++ b/packages/medusa/src/api/routes/admin/store/remove-currency.js @@ -0,0 +1,11 @@ +export default async (req, res) => { + const { currency_code } = req.params + + try { + const storeService = req.scope.resolve("storeService") + const data = await storeService.removeCurrency(currency_code) + res.status(200).json({ store: data }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/store/update-store.js b/packages/medusa/src/api/routes/admin/store/update-store.js new file mode 100644 index 0000000000..4cc23296a2 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/store/update-store.js @@ -0,0 +1,21 @@ +import { MedusaError, Validator } from "medusa-core-utils" + +export default async (req, res) => { + const schema = Validator.object().keys({ + name: Validator.string(), + currencies: Validator.array().items(Validator.string()), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const storeService = req.scope.resolve("storeService") + const data = await storeService.update(value) + res.status(200).json({ store: data }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/store/index.js b/packages/medusa/src/api/routes/store/index.js index a81c02c9bf..9d5193f372 100644 --- a/packages/medusa/src/api/routes/store/index.js +++ b/packages/medusa/src/api/routes/store/index.js @@ -1,4 +1,5 @@ import { Router } from "express" +import cors from "cors" import productRoutes from "./products" import cartRoutes from "./carts" @@ -8,9 +9,17 @@ import shippingOptionRoutes from "./shipping-options" const route = Router() -export default app => { +export default (app, container, config) => { app.use("/store", route) + const storeCors = config.store_cors || "" + route.use( + cors({ + origin: storeCors.split(","), + credentials: true, + }) + ) + customerRoutes(route) productRoutes(route) orderRoutes(route) diff --git a/packages/medusa/src/loaders/api.js b/packages/medusa/src/loaders/api.js index 9beb2edb02..966c7c8a64 100644 --- a/packages/medusa/src/loaders/api.js +++ b/packages/medusa/src/loaders/api.js @@ -1,9 +1,11 @@ +import { getConfigFile, createRequireFromPath } from "medusa-core-utils" + import routes from "../api" -import glob from "glob" -import path from "path" +export default async ({ app, rootDirectory, container }) => { + const { configModule } = getConfigFile(rootDirectory, `medusa-config`) + const config = configModule.projectConfig || {} -export default async ({ app, container }) => { - app.use("/", routes(container)) + app.use("/", routes(container, config)) return app } diff --git a/packages/medusa/src/loaders/express.js b/packages/medusa/src/loaders/express.js index c80d9d1c88..59917d0c19 100644 --- a/packages/medusa/src/loaders/express.js +++ b/packages/medusa/src/loaders/express.js @@ -2,15 +2,12 @@ import express from "express" import bodyParser from "body-parser" import session from "client-sessions" import cookieParser from "cookie-parser" -import cors from "cors" import morgan from "morgan" import config from "../config" export default async ({ app }) => { app.enable("trust proxy") - - app.use(cors()) app.use( morgan("combined", { skip: () => process.env.NODE_ENV === "test", diff --git a/packages/medusa/src/loaders/index.js b/packages/medusa/src/loaders/index.js index b82165ddf4..d8e6e894b3 100644 --- a/packages/medusa/src/loaders/index.js +++ b/packages/medusa/src/loaders/index.js @@ -54,7 +54,7 @@ export default async ({ directory: rootDirectory, expressApp }) => { await pluginsLoader({ container, rootDirectory, app: expressApp }) Logger.info("Plugins Intialized") - await apiLoader({ container, app: expressApp }) + await apiLoader({ container, rootDirectory, app: expressApp }) Logger.info("API initialized") return { container, dbConnection, app: expressApp } diff --git a/packages/medusa/src/loaders/plugins.js b/packages/medusa/src/loaders/plugins.js index 5cd115f09f..ecd8fba5da 100644 --- a/packages/medusa/src/loaders/plugins.js +++ b/packages/medusa/src/loaders/plugins.js @@ -9,7 +9,7 @@ import { getConfigFile, createRequireFromPath } from "medusa-core-utils" import _ from "lodash" import path from "path" import fs from "fs" -import { asFunction } from "awilix" +import { asFunction, aliasTo } from "awilix" import { sync as existsSync } from "fs-exists-cached" /** @@ -112,6 +112,7 @@ function registerServices(pluginDetails, container) { const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*`, {}) files.forEach(fn => { const loaded = require(fn).default + const name = formatRegistrationName(fn) if (!(loaded.prototype instanceof BaseService)) { const logger = container.resolve("logger") @@ -130,9 +131,8 @@ function registerServices(pluginDetails, container) { // 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({ - [`pp_${loaded.identifier}`]: asFunction( - cradle => new loaded(cradle, pluginDetails.options) - ), + [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 @@ -144,12 +144,10 @@ function registerServices(pluginDetails, container) { // 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({ - [`fp_${loaded.identifier}`]: asFunction( - cradle => new loaded(cradle, pluginDetails.options) - ), + [name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)), + [`fp_${loaded.identifier}`]: aliasTo(name), }) } else { - const name = formatRegistrationName(fn) container.register({ [name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)), }) diff --git a/packages/medusa/src/models/__mocks__/store.js b/packages/medusa/src/models/__mocks__/store.js new file mode 100644 index 0000000000..586a849be4 --- /dev/null +++ b/packages/medusa/src/models/__mocks__/store.js @@ -0,0 +1,17 @@ +import { IdMap } from "medusa-test-utils" + +export const store = { + _id: IdMap.getId("store"), + name: "test store", + currencies: ["DKK"], +} + +export const StoreModelMock = { + create: jest.fn().mockReturnValue(Promise.resolve()), + updateOne: jest.fn().mockImplementation((query, update) => { + return Promise.resolve() + }), + findOne: jest.fn().mockImplementation(query => { + return Promise.resolve(store) + }), +} diff --git a/packages/medusa/src/models/store.js b/packages/medusa/src/models/store.js new file mode 100644 index 0000000000..95dde91655 --- /dev/null +++ b/packages/medusa/src/models/store.js @@ -0,0 +1,13 @@ +import mongoose from "mongoose" +import { BaseModel } from "medusa-interfaces" + +class StoreModel extends BaseModel { + static modelName = "Store" + static schema = { + name: { type: String, required: true, default: "Medusa Store" }, + currencies: { type: [String], default: [] }, + metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, + } +} + +export default StoreModel diff --git a/packages/medusa/src/services/__mocks__/store.js b/packages/medusa/src/services/__mocks__/store.js new file mode 100644 index 0000000000..e31d3c0a90 --- /dev/null +++ b/packages/medusa/src/services/__mocks__/store.js @@ -0,0 +1,28 @@ +import { IdMap } from "medusa-test-utils" + +export const store = { + _id: IdMap.getId("store"), + name: "Test store", + currencies: ["DKK", "SEK", "GBP"], +} + +export const StoreServiceMock = { + addCurrency: jest.fn().mockImplementation(data => { + return Promise.resolve() + }), + removeCurrency: jest.fn().mockImplementation(data => { + return Promise.resolve() + }), + update: jest.fn().mockImplementation(data => { + return Promise.resolve() + }), + retrieve: jest.fn().mockImplementation(data => { + return Promise.resolve(store) + }), +} + +const mock = jest.fn().mockImplementation(() => { + return StoreServiceMock +}) + +export default mock diff --git a/packages/medusa/src/services/__tests__/region.js b/packages/medusa/src/services/__tests__/region.js index 7b4502cab0..3c805353bb 100644 --- a/packages/medusa/src/services/__tests__/region.js +++ b/packages/medusa/src/services/__tests__/region.js @@ -4,6 +4,7 @@ import RegionService from "../region" import { RegionModelMock } from "../../models/__mocks__/region" import { PaymentProviderServiceMock } from "../__mocks__/payment-provider" import { FulfillmentProviderServiceMock } from "../__mocks__/fulfillment-provider" +import { StoreServiceMock } from "../__mocks__/store" describe("RegionService", () => { describe("create", () => { @@ -14,6 +15,7 @@ describe("RegionService", () => { it("successfully creates a new region", async () => { const regionService = new RegionService({ regionModel: RegionModelMock, + storeService: StoreServiceMock, }) await regionService.create({ @@ -37,6 +39,7 @@ describe("RegionService", () => { regionModel: RegionModelMock, paymentProviderService: PaymentProviderServiceMock, fulfillmentProviderService: FulfillmentProviderServiceMock, + storeService: StoreServiceMock, }) await regionService.create({ @@ -103,6 +106,7 @@ describe("RegionService", () => { regionModel: RegionModelMock, paymentProviderService: PaymentProviderServiceMock, fulfillmentProviderService: FulfillmentProviderServiceMock, + storeService: StoreServiceMock, }) await expect( @@ -195,6 +199,7 @@ describe("RegionService", () => { regionModel: RegionModelMock, paymentProviderService: PaymentProviderServiceMock, fulfillmentProviderService: FulfillmentProviderServiceMock, + storeService: StoreServiceMock, }) await regionService.update(IdMap.getId("region-se"), { diff --git a/packages/medusa/src/services/__tests__/store.js b/packages/medusa/src/services/__tests__/store.js new file mode 100644 index 0000000000..e3ae44db50 --- /dev/null +++ b/packages/medusa/src/services/__tests__/store.js @@ -0,0 +1,127 @@ +import StoreService from "../store" +import { StoreModelMock } from "../../models/__mocks__/store" +import { IdMap } from "medusa-test-utils" + +describe("StoreService", () => { + describe("retrieve", () => { + const storeService = new StoreService({ + storeModel: StoreModelMock, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("retrieves store", async () => { + await storeService.retrieve() + + expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1) + expect(StoreModelMock.findOne).toHaveBeenCalledWith() + }) + }) + + describe("update", () => { + const storeService = new StoreService({ + storeModel: StoreModelMock, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("retrieves store", async () => { + await storeService.update({ + name: "New Name", + currencies: ["DKK", "sek", "uSd"], + }) + + expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1) + + expect(StoreModelMock.updateOne).toHaveBeenCalledTimes(1) + expect(StoreModelMock.updateOne).toHaveBeenCalledWith( + { _id: IdMap.getId("store") }, + { + $set: { + name: "New Name", + currencies: ["DKK", "SEK", "USD"], + }, + }, + { runValidators: true } + ) + }) + + it("fails if currency not ok", async () => { + await expect( + storeService.update({ + currencies: ["notacurrence"], + }) + ).rejects.toThrow("Invalid currency NOTACURRENCE") + + expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1) + }) + }) + + describe("addCurrency", () => { + const storeService = new StoreService({ + storeModel: StoreModelMock, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("retrieves store", async () => { + await storeService.addCurrency("sek") + + expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1) + + expect(StoreModelMock.updateOne).toHaveBeenCalledTimes(1) + expect(StoreModelMock.updateOne).toHaveBeenCalledWith( + { _id: IdMap.getId("store") }, + { + $push: { currencies: "SEK" }, + } + ) + }) + + it("fails if currency not ok", async () => { + await expect(storeService.addCurrency("notacurrence")).rejects.toThrow( + "Invalid currency NOTACURRENCE" + ) + + expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1) + }) + + it("fails if currency already existis", async () => { + await expect(storeService.addCurrency("DKK")).rejects.toThrow( + "Currency already added" + ) + + expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1) + }) + }) + + describe("removeCurrency", () => { + const storeService = new StoreService({ + storeModel: StoreModelMock, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("retrieves store", async () => { + await storeService.removeCurrency("sek") + + expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1) + + expect(StoreModelMock.updateOne).toHaveBeenCalledTimes(1) + expect(StoreModelMock.updateOne).toHaveBeenCalledWith( + { _id: IdMap.getId("store") }, + { + $pull: { currencies: "SEK" }, + } + ) + }) + }) +}) diff --git a/packages/medusa/src/services/region.js b/packages/medusa/src/services/region.js index ba9a5661e4..6bbb646901 100644 --- a/packages/medusa/src/services/region.js +++ b/packages/medusa/src/services/region.js @@ -2,7 +2,6 @@ import _ from "lodash" import { Validator, MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" import { countries } from "../utils/countries" -import { currencies } from "../utils/currencies" /** * Provides layer to manipulate regions. @@ -11,6 +10,7 @@ import { currencies } from "../utils/currencies" class RegionService extends BaseService { constructor({ regionModel, + storeService, paymentProviderService, fulfillmentProviderService, }) { @@ -19,6 +19,9 @@ class RegionService extends BaseService { /** @private @const {RegionModel} */ this.regionModel_ = regionModel + /** @private @const {StoreService} */ + this.storeService_ = storeService + /** @private @const {PaymentProviderService} */ this.paymentProviderService_ = paymentProviderService @@ -69,7 +72,7 @@ class RegionService extends BaseService { if (region.currency_code) { region.currency_code = region.currency_code.toUpperCase() - this.validateCurrency_(region.currency_code) + await this.validateCurrency_(region.currency_code) } if (region.countries) { @@ -123,8 +126,10 @@ class RegionService extends BaseService { * Validates a currency code. Will throw if the currency code doesn't exist. * @param {string} currencyCode - an ISO currency code */ - validateCurrency_(currencyCode) { - if (!currencies[currencyCode]) { + async validateCurrency_(currencyCode) { + const store = await this.storeService_.retrieve() + + if (!store.currencies.includes(currencyCode.toUpperCase())) { throw new MedusaError( MedusaError.Types.INVALID_DATA, "Invalid currency code" diff --git a/packages/medusa/src/services/store.js b/packages/medusa/src/services/store.js new file mode 100644 index 0000000000..a43457bb0c --- /dev/null +++ b/packages/medusa/src/services/store.js @@ -0,0 +1,175 @@ +import mongoose from "mongoose" +import bcrypt from "bcrypt" +import _ from "lodash" +import { Validator, MedusaError } from "medusa-core-utils" +import { BaseService } from "medusa-interfaces" + +import { currencies } from "../utils/currencies" + +/** + * Provides layer to manipulate store settings. + * @implements BaseService + */ +class StoreService extends BaseService { + constructor({ storeModel, eventBusService }) { + super() + + /** @private @const {storeModel} */ + this.storeModel_ = storeModel + + /** @private @const {EventBus} */ + this.eventBus_ = eventBusService + } + + /** + * Used to validate customer ids. Throws an error if the cast fails + * @param {string} rawId - the raw customer id to validate. + * @return {string} the validated id + */ + validateId_(rawId) { + const schema = Validator.objectId() + const { value, error } = schema.validate(rawId) + if (error) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "The customerId could not be casted to an ObjectId" + ) + } + + return value + } + + /** + * Retrieve the store settings. There is always a maximum of one store. + * @return {Promise} the customer document. + */ + retrieve() { + return this.storeModel_.findOne().catch(err => { + throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + }) + } + + /** + * Updates a customer. Metadata updates and address updates should + * use dedicated methods, e.g. `setMetadata`, etc. The function + * will throw errors if metadata updates and address updates are attempted. + * @param {string} variantId - the id of the variant. Must be a string that + * can be casted to an ObjectId + * @param {object} update - an object with the update values. + * @return {Promise} resolves to the update result. + */ + async update(update) { + const store = await this.retrieve() + + if (update.metadata) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Use setMetadata to update metadata fields" + ) + } + + if (update.currencies) { + update.currencies = update.currencies.map(c => c.toUpperCase()) + update.currencies.forEach(c => { + if (!currencies[c]) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Invalid currency ${c}` + ) + } + }) + } + + return this.storeModel_ + .updateOne({ _id: store._id }, { $set: update }, { runValidators: true }) + .catch(err => { + throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + }) + } + + /** + * Add a currency to the store + * @param {string} code - 3 character ISO currency code + * @return {Promise} result after update + */ + async addCurrency(code) { + code = code.toUpperCase() + const store = await this.retrieve() + + if (!currencies[code]) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Invalid currency ${code}` + ) + } + + if (store.currencies.includes(code)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Currency already added` + ) + } + + return this.storeModel_.updateOne( + { + _id: store._id, + }, + { $push: { currencies: code } } + ) + } + + /** + * Removes a currency from the store + * @param {string} code - 3 character ISO currency code + * @return {Promise} result after update + */ + async removeCurrency(code) { + const store = await this.retrieve() + code = code.toUpperCase() + return this.storeModel_.updateOne( + { + _id: store._id, + }, + { $pull: { currencies: code } } + ) + } + + /** + * Decorates a store object. + * @param {Store} store - the store to decorate. + * @param {string[]} fields - the fields to include. + * @param {string[]} expandFields - fields to expand. + * @return {Store} return the decorated Store. + */ + async decorate(store, fields, expandFields = []) { + return store + } + + /** + * Dedicated method to set metadata for a store. + * To ensure that plugins does not overwrite each + * others metadata fields, setMetadata is provided. + * @param {string} customerId - the customer to apply metadata to. + * @param {string} key - key for metadata field + * @param {string} value - value for metadata field. + * @return {Promise} resolves to the updated result. + */ + async setMetadata(key, value) { + const store = await this.retrieve() + if (typeof key !== "string") { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Key type is invalid. Metadata keys must be strings" + ) + } + + const keyPath = `metadata.${key}` + return this.storeModel_ + .updateOne({ _id: store._id }, { $set: { [keyPath]: value } }) + .catch(err => { + throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + }) + } +} + +export default StoreService