Adds Store Service to control store settings (#76)

Also adds support for `projectConfig.admin_cors` & `projectConfig.store_cors`
This commit is contained in:
Sebastian Rindom
2020-06-29 10:23:24 +02:00
committed by GitHub
parent ef43fb2030
commit bc5ff91a02
28 changed files with 1000 additions and 27 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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())

View File

@@ -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
}

View File

@@ -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")
})
})
})

View File

@@ -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()
})
})
})

View File

@@ -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")
})
})
})

View File

@@ -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"],
})
})
})
})

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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 }

View File

@@ -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)),
})

View File

@@ -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)
}),
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"), {

View File

@@ -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" },
}
)
})
})
})

View File

@@ -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"

View File

@@ -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<Store>} 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