Adds Store Service to control store settings (#76)
Also adds support for `projectConfig.admin_cors` & `projectConfig.store_cors`
This commit is contained in:
193
packages/medusa-payment-stripe/__mocks__/cart.js
Normal file
193
packages/medusa-payment-stripe/__mocks__/cart.js
Normal 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;
|
||||
45
packages/medusa-payment-stripe/__mocks__/customer.js
Normal file
45
packages/medusa-payment-stripe/__mocks__/customer.js
Normal 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;
|
||||
16
packages/medusa-payment-stripe/__mocks__/eventbus.js
Normal file
16
packages/medusa-payment-stripe/__mocks__/eventbus.js
Normal 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;
|
||||
96
packages/medusa-payment-stripe/__mocks__/stripe.js
Normal file
96
packages/medusa-payment-stripe/__mocks__/stripe.js
Normal 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;
|
||||
15
packages/medusa-payment-stripe/__mocks__/totals.js
Normal file
15
packages/medusa-payment-stripe/__mocks__/totals.js
Normal 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;
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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"],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
11
packages/medusa/src/api/routes/admin/store/add-currency.js
Normal file
11
packages/medusa/src/api/routes/admin/store/add-currency.js
Normal 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
|
||||
}
|
||||
}
|
||||
9
packages/medusa/src/api/routes/admin/store/get-store.js
Normal file
9
packages/medusa/src/api/routes/admin/store/get-store.js
Normal 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
|
||||
}
|
||||
}
|
||||
21
packages/medusa/src/api/routes/admin/store/index.js
Normal file
21
packages/medusa/src/api/routes/admin/store/index.js
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
21
packages/medusa/src/api/routes/admin/store/update-store.js
Normal file
21
packages/medusa/src/api/routes/admin/store/update-store.js
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)),
|
||||
})
|
||||
|
||||
17
packages/medusa/src/models/__mocks__/store.js
Normal file
17
packages/medusa/src/models/__mocks__/store.js
Normal 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)
|
||||
}),
|
||||
}
|
||||
13
packages/medusa/src/models/store.js
Normal file
13
packages/medusa/src/models/store.js
Normal 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
|
||||
28
packages/medusa/src/services/__mocks__/store.js
Normal file
28
packages/medusa/src/services/__mocks__/store.js
Normal 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
|
||||
@@ -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"), {
|
||||
|
||||
127
packages/medusa/src/services/__tests__/store.js
Normal file
127
packages/medusa/src/services/__tests__/store.js
Normal 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" },
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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"
|
||||
|
||||
175
packages/medusa/src/services/store.js
Normal file
175
packages/medusa/src/services/store.js
Normal 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
|
||||
Reference in New Issue
Block a user