Adds Regions (#33)

Regions are a collection of countries that share some common functionality, e.g., currency, available fulfillment and shipping providers and a taxrate. The store operator can have as many Regions as needed.
This commit is contained in:
Sebastian Rindom
2020-04-08 09:44:41 +02:00
committed by GitHub
parent ea4185eddd
commit b7557db9f9
31 changed files with 2686 additions and 9 deletions

View File

@@ -3,6 +3,7 @@ import middlewares from "../../middlewares"
import authRoutes from "./auth"
import productRoutes from "./products"
import productVariantRoutes from "./product-variants"
import regionRoutes from "./regions"
import shippingOptionRoutes from "./shipping-options"
import shippingProfileRoutes from "./shipping-profiles"
@@ -18,6 +19,7 @@ export default app => {
route.use(middlewares.authenticate())
productRoutes(route)
regionRoutes(route)
shippingOptionRoutes(route)
shippingProfileRoutes(route)
// productVariantRoutes(route)

View File

@@ -0,0 +1,35 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { RegionServiceMock } from "../../../../../services/__mocks__/region"
describe("POST /admin/regions/:region_id/countries", () => {
describe("successful creation", () => {
let subject
beforeAll(async () => {
const id = IdMap.getId("region")
subject = await request("POST", `/admin/regions/${id}/countries`, {
payload: {
country_code: "se",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service addCountry", () => {
expect(RegionServiceMock.addCountry).toHaveBeenCalledTimes(1)
expect(RegionServiceMock.addCountry).toHaveBeenCalledWith(
IdMap.getId("region"),
"se"
)
})
})
})

View File

@@ -0,0 +1,39 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { RegionServiceMock } from "../../../../../services/__mocks__/region"
describe("POST /admin/regions/:region_id/fulfillment-providers", () => {
describe("successful creation", () => {
let subject
beforeAll(async () => {
const id = IdMap.getId("region")
subject = await request(
"POST",
`/admin/regions/${id}/fulfillment-providers`,
{
payload: {
provider_id: "default_provider",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service addCountry", () => {
expect(RegionServiceMock.addFulfillmentProvider).toHaveBeenCalledTimes(1)
expect(RegionServiceMock.addFulfillmentProvider).toHaveBeenCalledWith(
IdMap.getId("region"),
"default_provider"
)
})
})
})

View File

@@ -0,0 +1,39 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { RegionServiceMock } from "../../../../../services/__mocks__/region"
describe("POST /admin/regions/:region_id/payment-providers", () => {
describe("successful creation", () => {
let subject
beforeAll(async () => {
const id = IdMap.getId("region")
subject = await request(
"POST",
`/admin/regions/${id}/payment-providers`,
{
payload: {
provider_id: "default_provider",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service addCountry", () => {
expect(RegionServiceMock.addPaymentProvider).toHaveBeenCalledTimes(1)
expect(RegionServiceMock.addPaymentProvider).toHaveBeenCalledWith(
IdMap.getId("region"),
"default_provider"
)
})
})
})

View File

@@ -0,0 +1,43 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { RegionServiceMock } from "../../../../../services/__mocks__/region"
describe("POST /admin/regions", () => {
describe("successful creation", () => {
let subject
beforeAll(async () => {
subject = await request("POST", "/admin/regions", {
payload: {
name: "New Region",
currency_code: "dkk",
countries: ["dk"],
tax_rate: 0.3,
payment_providers: ["default_provider"],
fulfillment_providers: ["default_provider"],
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service create", () => {
expect(RegionServiceMock.create).toHaveBeenCalledTimes(1)
expect(RegionServiceMock.create).toHaveBeenCalledWith({
name: "New Region",
currency_code: "dkk",
countries: ["dk"],
tax_rate: 0.3,
payment_providers: ["default_provider"],
fulfillment_providers: ["default_provider"],
})
})
})
})

View File

@@ -0,0 +1,31 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { RegionServiceMock } from "../../../../../services/__mocks__/region"
describe("DELETE /admin/regions/:region_id", () => {
describe("successful deletion", () => {
let subject
beforeAll(async () => {
const id = IdMap.getId("region")
subject = await request("DELETE", `/admin/regions/${id}`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service addCountry", () => {
expect(RegionServiceMock.delete).toHaveBeenCalledTimes(1)
expect(RegionServiceMock.delete).toHaveBeenCalledWith(
IdMap.getId("region")
)
})
})
})

View File

@@ -0,0 +1,31 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { RegionServiceMock } from "../../../../../services/__mocks__/region"
describe("GET /admin/regions/:region_id", () => {
describe("successful creation", () => {
let subject
beforeAll(async () => {
const id = IdMap.getId("region")
subject = await request("GET", `/admin/regions/${id}`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service addCountry", () => {
expect(RegionServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(RegionServiceMock.retrieve).toHaveBeenCalledWith(
IdMap.getId("region")
)
})
})
})

View File

@@ -0,0 +1,28 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { RegionServiceMock } from "../../../../../services/__mocks__/region"
describe("GET /admin/regions", () => {
describe("successful creation", () => {
let subject
beforeAll(async () => {
subject = await request("GET", `/admin/regions`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service addCountry", () => {
expect(RegionServiceMock.list).toHaveBeenCalledTimes(1)
expect(RegionServiceMock.list).toHaveBeenCalledWith({})
})
})
})

View File

@@ -0,0 +1,32 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { RegionServiceMock } from "../../../../../services/__mocks__/region"
describe("DELETE /admin/regions/:region_id/countries/:country_code", () => {
describe("successful creation", () => {
let subject
beforeAll(async () => {
const id = IdMap.getId("region")
subject = await request("DELETE", `/admin/regions/${id}/countries/DK`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service addCountry", () => {
expect(RegionServiceMock.removeCountry).toHaveBeenCalledTimes(1)
expect(RegionServiceMock.removeCountry).toHaveBeenCalledWith(
IdMap.getId("region"),
"DK"
)
})
})
})

View File

@@ -0,0 +1,38 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { RegionServiceMock } from "../../../../../services/__mocks__/region"
describe("DELETE /admin/regions/:region_id/fulfillment-providers/:provider_id", () => {
describe("successful deletion", () => {
let subject
beforeAll(async () => {
const id = IdMap.getId("region")
subject = await request(
"DELETE",
`/admin/regions/${id}/fulfillment-providers/default_provider`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service addCountry", () => {
expect(RegionServiceMock.removeFulfillmentProvider).toHaveBeenCalledTimes(
1
)
expect(RegionServiceMock.removeFulfillmentProvider).toHaveBeenCalledWith(
IdMap.getId("region"),
"default_provider"
)
})
})
})

View File

@@ -0,0 +1,36 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { RegionServiceMock } from "../../../../../services/__mocks__/region"
describe("DELETE /admin/regions/:region_id/payment-providers/:provider_id", () => {
describe("successful deletion", () => {
let subject
beforeAll(async () => {
const id = IdMap.getId("region")
subject = await request(
"DELETE",
`/admin/regions/${id}/payment-providers/default_provider`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service addCountry", () => {
expect(RegionServiceMock.removePaymentProvider).toHaveBeenCalledTimes(1)
expect(RegionServiceMock.removePaymentProvider).toHaveBeenCalledWith(
IdMap.getId("region"),
"default_provider"
)
})
})
})

View File

@@ -0,0 +1,47 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { RegionServiceMock } from "../../../../../services/__mocks__/region"
describe("POST /admin/regions/:region_id", () => {
describe("successful deletion", () => {
let subject
beforeAll(async () => {
const id = IdMap.getId("region")
subject = await request("POST", `/admin/regions/${id}`, {
payload: {
name: "Updated Region",
currency_code: "dkk",
countries: ["dk"],
tax_rate: 0.3,
payment_providers: ["default_provider"],
fulfillment_providers: ["default_provider"],
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service addCountry", () => {
expect(RegionServiceMock.update).toHaveBeenCalledTimes(1)
expect(RegionServiceMock.update).toHaveBeenCalledWith(
IdMap.getId("region"),
{
name: "Updated Region",
currency_code: "dkk",
countries: ["dk"],
tax_rate: 0.3,
payment_providers: ["default_provider"],
fulfillment_providers: ["default_provider"],
}
)
})
})
})

View File

@@ -0,0 +1,23 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { region_id } = req.params
const schema = Validator.object().keys({
country_code: Validator.string().required(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const regionService = req.scope.resolve("regionService")
await regionService.addCountry(region_id, value.country_code)
const data = await regionService.retrieve(region_id)
res.status(200).json(data)
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,23 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { region_id } = req.params
const schema = Validator.object().keys({
provider_id: Validator.string().required(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const regionService = req.scope.resolve("regionService")
await regionService.addFulfillmentProvider(region_id, value.provider_id)
const data = await regionService.retrieve(region_id)
res.status(200).json(data)
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,23 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { region_id } = req.params
const schema = Validator.object().keys({
provider_id: Validator.string().required(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const regionService = req.scope.resolve("regionService")
await regionService.addPaymentProvider(region_id, value.provider_id)
const data = await regionService.retrieve(region_id)
res.status(200).json(data)
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,25 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const schema = Validator.object().keys({
name: Validator.string().required(),
currency_code: Validator.string().required(),
tax_rate: Validator.number().required(),
payment_providers: Validator.array().items(Validator.string()),
fulfillment_providers: Validator.array().items(Validator.string()),
countries: 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 regionService = req.scope.resolve("regionService")
const data = await regionService.create(value)
res.status(200).json(data)
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,17 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { region_id } = req.params
try {
const regionService = req.scope.resolve("regionService")
await regionService.delete(region_id)
res.status(200).json({
id: region_id,
object: "region",
deleted: true,
})
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,13 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { region_id } = req.params
try {
const regionService = req.scope.resolve("regionService")
const data = await regionService.retrieve(region_id)
res.status(200).json(data)
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,51 @@
import { Router } from "express"
import middlewares from "../../../middlewares"
const route = Router()
export default app => {
app.use("/regions", route)
route.get("/", middlewares.wrap(require("./list-regions").default))
route.get("/:region_id", middlewares.wrap(require("./get-region").default))
route.post("/", middlewares.wrap(require("./create-region").default))
route.post(
"/:region_id",
middlewares.wrap(require("./update-region").default)
)
route.delete(
"/:region_id",
middlewares.wrap(require("./delete-region").default)
)
route.post(
"/:region_id/countries",
middlewares.wrap(require("./add-country").default)
)
route.delete(
"/:region_id/countries/:country_code",
middlewares.wrap(require("./remove-country").default)
)
route.post(
"/:region_id/payment-providers",
middlewares.wrap(require("./add-payment-provider").default)
)
route.delete(
"/:region_id/payment-providers/:provider_id",
middlewares.wrap(require("./remove-payment-provider").default)
)
route.post(
"/:region_id/fulfillment-providers",
middlewares.wrap(require("./add-fulfillment-provider").default)
)
route.delete(
"/:region_id/fulfillment-providers/:provider_id",
middlewares.wrap(require("./remove-fulfillment-provider").default)
)
return app
}

View File

@@ -0,0 +1,12 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
try {
const regionService = req.scope.resolve("regionService")
const data = await regionService.list({})
res.status(200).json(data)
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,13 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { region_id, country_code } = req.params
try {
const regionService = req.scope.resolve("regionService")
await regionService.removeCountry(region_id, country_code)
res.sendStatus(200)
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,13 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { region_id, provider_id } = req.params
try {
const regionService = req.scope.resolve("regionService")
await regionService.removeFulfillmentProvider(region_id, provider_id)
res.sendStatus(200)
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,13 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { region_id, provider_id } = req.params
try {
const regionService = req.scope.resolve("regionService")
await regionService.removePaymentProvider(region_id, provider_id)
res.sendStatus(200)
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,26 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { region_id } = req.params
const schema = Validator.object().keys({
name: Validator.string(),
currency_code: Validator.string(),
tax_rate: Validator.number(),
payment_providers: Validator.array().items(Validator.string()),
fulfillment_providers: Validator.array().items(Validator.string()),
countries: 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 regionService = req.scope.resolve("regionService")
const data = await regionService.update(region_id, value)
res.status(200).json(data)
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,66 @@
import { IdMap } from "medusa-test-utils"
export const regions = {
testRegion: {
_id: IdMap.getId("testRegion"),
name: "Test Region",
countries: ["DK", "US", "DE"],
tax_rate: 0.25,
payment_providers: ["default_provider", "unregistered"],
fulfillment_providers: ["test_shipper"],
currency_code: "usd",
},
regionFrance: {
_id: IdMap.getId("region-france"),
name: "France",
countries: ["FR"],
payment_providers: ["default_provider", "france-provider"],
currency_code: "eur",
},
regionUs: {
_id: IdMap.getId("region-us"),
name: "USA",
countries: ["US"],
currency_code: "usd",
},
regionGermany: {
_id: IdMap.getId("region-de"),
name: "Germany",
countries: ["DE"],
currency_code: "eur",
},
regionSweden: {
_id: IdMap.getId("region-se"),
name: "Sweden",
countries: ["SE"],
payment_providers: ["sweden_provider"],
fulfillment_providers: ["sweden_provider"],
currency_code: "SEK",
},
}
export const RegionModelMock = {
create: jest.fn().mockReturnValue(Promise.resolve()),
updateOne: jest.fn().mockImplementation((query, update) => {}),
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
findOne: jest.fn().mockImplementation(query => {
if (query.countries === "SE") {
return Promise.resolve(regions.regionSweden)
}
switch (query._id) {
case IdMap.getId("testRegion"):
return Promise.resolve(regions.testRegion)
case IdMap.getId("region-france"):
return Promise.resolve(regions.regionFrance)
case IdMap.getId("region-us"):
return Promise.resolve(regions.regionUs)
case IdMap.getId("region-de"):
return Promise.resolve(regions.regionGermany)
case IdMap.getId("region-se"):
return Promise.resolve(regions.regionSweden)
default:
return Promise.resolve(undefined)
}
}),
}

View File

@@ -0,0 +1,17 @@
import mongoose from "mongoose"
import { BaseModel } from "medusa-interfaces"
class RegionModel extends BaseModel {
static modelName = "Region"
static schema = {
name: { type: String, required: true },
currency_code: { type: String, required: true },
tax_rate: { type: Number, required: true, default: 0 },
countries: { type: [String], default: [] },
payment_providers: { type: [String], default: [] },
fulfillment_providers: { type: [String], default: [] },
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
}
}
export default RegionModel

View File

@@ -57,6 +57,21 @@ export const RegionServiceMock = {
}
return Promise.resolve(undefined)
}),
delete: jest.fn().mockImplementation(data => Promise.resolve()),
create: jest.fn().mockImplementation(data => Promise.resolve()),
addCountry: jest.fn().mockImplementation(data => Promise.resolve()),
addFulfillmentProvider: jest
.fn()
.mockImplementation(data => Promise.resolve()),
addPaymentProvider: jest.fn().mockImplementation(data => Promise.resolve()),
removeCountry: jest.fn().mockImplementation(data => Promise.resolve()),
removeFulfillmentProvider: jest
.fn()
.mockImplementation(data => Promise.resolve()),
removePaymentProvider: jest
.fn()
.mockImplementation(data => Promise.resolve()),
update: jest.fn().mockImplementation(data => Promise.resolve()),
list: jest.fn().mockImplementation(data => {
return Promise.resolve([
regions.testRegion,

View File

@@ -0,0 +1,516 @@
import mongoose from "mongoose"
import { IdMap } from "medusa-test-utils"
import RegionService from "../region"
import { RegionModelMock } from "../../models/__mocks__/region"
import { PaymentProviderServiceMock } from "../__mocks__/payment-provider"
import { FulfillmentProviderServiceMock } from "../__mocks__/fulfillment-provider"
describe("RegionService", () => {
describe("create", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it("successfully creates a new region", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
})
await regionService.create({
name: "Denmark",
currency_code: "dkk",
tax_rate: 0.25,
countries: ["DK"],
})
expect(RegionModelMock.create).toHaveBeenCalledTimes(1)
expect(RegionModelMock.create).toHaveBeenCalledWith({
name: "Denmark",
currency_code: "DKK",
tax_rate: 0.25,
countries: ["DK"],
})
})
it("create with payment/fulfillment providers", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
paymentProviderService: PaymentProviderServiceMock,
fulfillmentProviderService: FulfillmentProviderServiceMock,
})
await regionService.create({
name: "Denmark",
currency_code: "dkk",
tax_rate: 0.25,
countries: ["DK"],
payment_providers: ["default_provider"],
fulfillment_providers: ["default_provider"],
})
expect(PaymentProviderServiceMock.retrieveProvider).toHaveBeenCalledTimes(
1
)
expect(PaymentProviderServiceMock.retrieveProvider).toHaveBeenCalledWith(
"default_provider"
)
expect(
FulfillmentProviderServiceMock.retrieveProvider
).toHaveBeenCalledTimes(1)
expect(
FulfillmentProviderServiceMock.retrieveProvider
).toHaveBeenCalledWith("default_provider")
expect(RegionModelMock.create).toHaveBeenCalledTimes(1)
expect(RegionModelMock.create).toHaveBeenCalledWith({
name: "Denmark",
currency_code: "DKK",
tax_rate: 0.25,
countries: ["DK"],
payment_providers: ["default_provider"],
fulfillment_providers: ["default_provider"],
})
})
})
describe("retrieve", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it("successfully retrieves a region", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
})
await regionService.retrieve(IdMap.getId("region-se"))
expect(RegionModelMock.findOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("region-se"),
})
})
})
describe("validateFields_", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it("throws on invalid currency code", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
paymentProviderService: PaymentProviderServiceMock,
fulfillmentProviderService: FulfillmentProviderServiceMock,
})
await expect(
regionService.validateFields_({ currency_code: "1cw" })
).rejects.toThrow("Invalid currency code")
})
it("throws on invalid country code", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
paymentProviderService: PaymentProviderServiceMock,
fulfillmentProviderService: FulfillmentProviderServiceMock,
})
await expect(
regionService.validateFields_({ countries: ["ddd"] })
).rejects.toThrow("Invalid country code")
})
it("throws on in use country code", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
paymentProviderService: PaymentProviderServiceMock,
fulfillmentProviderService: FulfillmentProviderServiceMock,
})
await expect(
regionService.validateFields_({ countries: ["se"] })
).rejects.toThrow(
"Sweden already exists in Sweden, delete it in that region before adding it"
)
})
it("throws on invalid tax_rate", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
paymentProviderService: PaymentProviderServiceMock,
fulfillmentProviderService: FulfillmentProviderServiceMock,
})
await expect(
regionService.validateFields_({ tax_rate: 12 })
).rejects.toThrow("The tax_rate must be between 0 and 1")
})
it("throws on metadata", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
paymentProviderService: PaymentProviderServiceMock,
fulfillmentProviderService: FulfillmentProviderServiceMock,
})
await expect(
regionService.validateFields_({ metadata: { key: "Valie" } })
).rejects.toThrow("Please use setMetadata")
})
it("throws on unknown payment providers", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
paymentProviderService: PaymentProviderServiceMock,
fulfillmentProviderService: FulfillmentProviderServiceMock,
})
await expect(
regionService.validateFields_({ payment_providers: ["hi"] })
).rejects.toThrow("Provider Not Found")
})
it("throws on unknown fulfillment providers", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
paymentProviderService: PaymentProviderServiceMock,
fulfillmentProviderService: FulfillmentProviderServiceMock,
})
await expect(
regionService.validateFields_({ fulfillment_providers: ["hi"] })
).rejects.toThrow("Provider Not Found")
})
})
describe("update", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it("successfully updates a region", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
paymentProviderService: PaymentProviderServiceMock,
fulfillmentProviderService: FulfillmentProviderServiceMock,
})
await regionService.update(IdMap.getId("region-se"), {
name: "New Name",
currency_code: "gbp",
tax_rate: 0.25,
countries: ["DK", "se"],
payment_providers: ["default_provider"],
fulfillment_providers: ["default_provider"],
})
expect(PaymentProviderServiceMock.retrieveProvider).toHaveBeenCalledTimes(
1
)
expect(PaymentProviderServiceMock.retrieveProvider).toHaveBeenCalledWith(
"default_provider"
)
expect(
FulfillmentProviderServiceMock.retrieveProvider
).toHaveBeenCalledTimes(1)
expect(
FulfillmentProviderServiceMock.retrieveProvider
).toHaveBeenCalledWith("default_provider")
expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("region-se"),
},
{
$set: {
name: "New Name",
currency_code: "GBP",
tax_rate: 0.25,
countries: ["DK", "SE"],
payment_providers: ["default_provider"],
fulfillment_providers: ["default_provider"],
},
}
)
})
})
describe("delete", () => {
beforeAll(() => {
jest.clearAllMocks()
})
it("successfully deletes", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
})
await regionService.delete(IdMap.getId("region-se"))
expect(RegionModelMock.deleteOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.deleteOne).toHaveBeenCalledWith({
_id: IdMap.getId("region-se"),
})
})
})
describe("addCountry", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it("successfully adds to the countries array", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
})
await regionService.addCountry(IdMap.getId("region-se"), "dk")
expect(RegionModelMock.findOne).toHaveBeenCalledTimes(2)
expect(RegionModelMock.findOne).toHaveBeenCalledWith({
countries: "DK",
})
expect(RegionModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("region-se"),
})
expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("region-se"),
},
{
$push: { countries: "DK" },
}
)
})
it("resolves if exists", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
})
await regionService.addCountry(IdMap.getId("region-se"), "SE")
expect(RegionModelMock.findOne).toHaveBeenCalledTimes(2)
expect(RegionModelMock.findOne).toHaveBeenCalledWith({
countries: "SE",
})
expect(RegionModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("region-se"),
})
expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(0)
})
})
describe("removeCountry", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it("successfully removes country", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
})
await regionService.removeCountry(IdMap.getId("region-se"), "dk")
expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("region-se"),
},
{
$pull: { countries: "DK" },
}
)
})
})
describe("addPaymentProvider", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it("successfully adds to the countries array", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
paymentProviderService: PaymentProviderServiceMock,
})
await regionService.addPaymentProvider(
IdMap.getId("region-se"),
"default_provider"
)
expect(RegionModelMock.findOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("region-se"),
})
expect(PaymentProviderServiceMock.retrieveProvider).toHaveBeenCalledTimes(
1
)
expect(PaymentProviderServiceMock.retrieveProvider).toHaveBeenCalledWith(
"default_provider"
)
expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("region-se"),
},
{
$push: { payment_providers: "default_provider" },
}
)
})
it("resolves if exists", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
paymentProviderService: PaymentProviderServiceMock,
})
await regionService.addPaymentProvider(
IdMap.getId("region-se"),
"sweden_provider"
)
expect(RegionModelMock.findOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("region-se"),
})
expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(0)
})
})
describe("addFulfillmentProvider", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it("successfully adds to the fulfillment_provider array", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
fulfillmentProviderService: FulfillmentProviderServiceMock,
})
await regionService.addFulfillmentProvider(
IdMap.getId("region-se"),
"default_provider"
)
expect(RegionModelMock.findOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("region-se"),
})
expect(
FulfillmentProviderServiceMock.retrieveProvider
).toHaveBeenCalledTimes(1)
expect(
FulfillmentProviderServiceMock.retrieveProvider
).toHaveBeenCalledWith("default_provider")
expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("region-se"),
},
{
$push: { fulfillment_providers: "default_provider" },
}
)
})
it("resolves if exists", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
fulfillmentProviderService: FulfillmentProviderServiceMock,
})
await regionService.addFulfillmentProvider(
IdMap.getId("region-se"),
"sweden_provider"
)
expect(RegionModelMock.findOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("region-se"),
})
expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(0)
})
})
describe("removePaymentProvider", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it("removes payment provider", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
})
await regionService.removePaymentProvider(
IdMap.getId("region-se"),
"sweden_provider"
)
expect(RegionModelMock.findOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("region-se"),
})
expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("region-se"),
},
{
$pull: { payment_providers: "sweden_provider" },
}
)
})
})
describe("removeFulfillmentProvider", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it("removes fulfillment provider", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
})
await regionService.removeFulfillmentProvider(
IdMap.getId("region-se"),
"sweden_provider"
)
expect(RegionModelMock.findOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("region-se"),
})
expect(RegionModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(RegionModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("region-se"),
},
{
$pull: { fulfillment_providers: "sweden_provider" },
}
)
})
})
})

View File

@@ -1,9 +1,346 @@
// paymentProviders
// fulfillmentProviders
// setCurrency
// setTaxRate
// putShippingProvider
// putPaymentProvider
// removeShippingProvider
// removePaymentMethod
// listShippingMethods
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.
* @implements BaseService
*/
class RegionService extends BaseService {
constructor({
regionModel,
paymentProviderService,
fulfillmentProviderService,
}) {
super()
/** @private @const {RegionModel} */
this.regionModel_ = regionModel
/** @private @const {PaymentProviderService} */
this.paymentProviderService_ = paymentProviderService
/** @private @const {FulfillmentProviderService} */
this.fulfillmentProviderService_ = fulfillmentProviderService
}
/**
* Creates a region.
* @param {Region} rawRegion - the unvalidated region
* @return {Region} the newly created region
*/
async create(rawRegion) {
const region = await this.validateFields_(rawRegion)
return this.regionModel_.create(region)
}
/**
* Updates a region. Note metadata cannot be set with the update function, use
* setMetadata instead.
* @param {string} regionId - the region to update
* @param {object} update - the data to update the region with
* @return {Promise} the result of the update operation
*/
async update(regionId, update) {
const region = await this.validateFields_(update, regionId)
return this.regionModel_.updateOne(
{
_id: regionId,
},
{
$set: region,
}
)
}
/**
* Validates fields for creation and updates. If the region already exisits
* the id can be passed to check that country updates are allowed.
* @param {object} region - the region data to validate
* @param {string?} id - optional id of the region to check against
* @return {object} the validated region data
*/
async validateFields_(region, id = undefined) {
if (region.tax_rate) {
this.validateTaxRate_(region.tax_rate)
}
if (region.currency_code) {
region.currency_code = region.currency_code.toUpperCase()
this.validateCurrency_(region.currency_code)
}
if (region.countries) {
region.countries = await Promise.all(
region.countries.map(countryCode =>
this.validateCountry_(countryCode, id)
)
).catch(err => {
throw err
})
}
if (region.metadata) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Please use setMetadata"
)
}
if (region.fulfillment_providers) {
// Will throw if we do not find the provider
region.fulfillment_providers.forEach(pId => {
this.fulfillmentProviderService_.retrieveProvider(pId)
})
}
if (region.payment_providers) {
// Will throw if we do not find the provider
region.payment_providers.forEach(pId => {
this.paymentProviderService_.retrieveProvider(pId)
})
}
return region
}
/**
* Validates a tax rate. Will throw if the tax rate is not between 0 and 1.
* @param {number} taxRate - a number representing the tax rate of the region
*/
validateTaxRate_(taxRate) {
if (taxRate > 1 || taxRate < 0) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The tax_rate must be between 0 and 1"
)
}
}
/**
* 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]) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Invalid currency code"
)
}
}
/**
* Validates a country code. Will normalize the code before checking for
* existence.
* @param {string} code - a 2 digit alphanumeric ISO country code
* @param {string} id - the id of the current region to check against
*/
async validateCountry_(code, id) {
const countryCode = code.toUpperCase()
const country = countries.find(c => c.alpha2 === countryCode)
if (!country) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Invalid country code"
)
}
const existing = await this.regionModel_.findOne({ countries: countryCode })
if (existing && existing._id !== id) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`${country.name} already exists in ${existing.name}, delete it in that region before adding it`
)
}
return countryCode
}
/**
* Used to validate region ids. Throws an error if the cast fails
* @param {string} rawId - the raw region 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 regionId could not be casted to an ObjectId"
)
}
return value
}
/**
* Retrieves a region by its id.
* @param {string} regionId - the id of the region to retrieve
* @return {Region} the region
*/
async retrieve(regionId) {
const validatedId = this.validateId_(regionId)
const region = await this.regionModel_.findOne({ _id: validatedId })
if (!region) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Region with ${regionId} was not found`
)
}
return region
}
/**
* Deletes a region.
* @param {string} regionId - the region to delete
* @return {Promise} the result of the delete operation
*/
delete(regionId) {
return this.regionModel_.deleteOne({
_id: regionId,
})
}
/**
* Adds a country to the region.
* @param {string} regionId - the region to add a country to
* @param {string} code - a 2 digit alphanumeric ISO country code.
* @return {Promise} the result of the update operation
*/
async addCountry(regionId, code) {
const region = await this.retrieve(regionId)
const countryCode = await this.validateCountry_(code, regionId)
if (region.countries.includes(countryCode)) {
return Promise.resolve()
}
return this.regionModel_.updateOne(
{
_id: region._id,
},
{
$push: { countries: countryCode },
}
)
}
/**
* Removes a country from a Region
* @param {string} regionId - the region to remove from
* @param {string} code - a 2 digit alphanumeric ISO country code to remove
* @return {Promise} the result of the update operation
*/
async removeCountry(regionId, code) {
const countryCode = code.toUpperCase()
const region = await this.retrieve(regionId)
return this.regionModel_.updateOne(
{ _id: region._id },
{
$pull: {
countries: countryCode,
},
}
)
}
/**
* Adds a payment provider that is available in the region. Fails if the
* provider doesn't exist.
* @param {string} regionId - the region to add the provider to
* @param {string} providerId - the provider to add to the region
* @return {Promise} the result of the update operation
*/
async addPaymentProvider(regionId, providerId) {
const region = await this.retrieve(regionId)
if (region.payment_providers.includes(providerId)) {
return Promise.resolve()
}
// Will throw if we do not find the provider
this.paymentProviderService_.retrieveProvider(providerId)
return this.regionModel_.updateOne(
{
_id: region._id,
},
{
$push: { payment_providers: providerId },
}
)
}
/**
* Adds a fulfillment provider that is available in the region. Fails if the
* provider doesn't exist.
* @param {string} regionId - the region to add the provider to
* @param {string} providerId - the provider to add to the region
* @return {Promise} the result of the update operation
*/
async addFulfillmentProvider(regionId, providerId) {
const region = await this.retrieve(regionId)
if (region.fulfillment_providers.includes(providerId)) {
return Promise.resolve()
}
// Will throw if we do not find the provider
this.fulfillmentProviderService_.retrieveProvider(providerId)
return this.regionModel_.updateOne(
{
_id: region._id,
},
{
$push: { fulfillment_providers: providerId },
}
)
}
/**
* Removes a payment provider from a region. Is idempotent.
* @param {string} regionId - the region to remove the provider from
* @param {string} providerId - the provider to remove from the region
* @return {Promise} the result of the update operation
*/
async removePaymentProvider(regionId, providerId) {
const region = await this.retrieve(regionId)
return this.regionModel_.updateOne(
{ _id: region._id },
{
$pull: {
payment_providers: providerId,
},
}
)
}
/**
* Removes a fulfillment provider from a region. Is idempotent.
* @param {string} regionId - the region to remove the provider from
* @param {string} providerId - the provider to remove from the region
* @return {Promise} the result of the update operation
*/
async removeFulfillmentProvider(regionId, providerId) {
const region = await this.retrieve(regionId)
return this.regionModel_.updateOne(
{ _id: region._id },
{
$pull: {
fulfillment_providers: providerId,
},
}
)
}
}
export default RegionService

File diff suppressed because it is too large Load Diff