diff --git a/.changeset/shaggy-steaks-change.md b/.changeset/shaggy-steaks-change.md new file mode 100644 index 0000000000..8d3342d1cc --- /dev/null +++ b/.changeset/shaggy-steaks-change.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Convert RegionService to TypeScript diff --git a/integration-tests/api/__tests__/admin/discount.js b/integration-tests/api/__tests__/admin/discount.js index 5a83ec7a00..0e8ea631f7 100644 --- a/integration-tests/api/__tests__/admin/discount.js +++ b/integration-tests/api/__tests__/admin/discount.js @@ -2309,7 +2309,6 @@ describe("/admin/discounts", () => { }) const cond = discountCondition.data.discount_condition - console.log(cond.products) expect(discountCondition.status).toEqual(200) expect(cond).toMatchSnapshot({ diff --git a/packages/medusa/src/api/routes/admin/regions/create-region.ts b/packages/medusa/src/api/routes/admin/regions/create-region.ts index 80960b81f3..6027d52209 100644 --- a/packages/medusa/src/api/routes/admin/regions/create-region.ts +++ b/packages/medusa/src/api/routes/admin/regions/create-region.ts @@ -1,10 +1,16 @@ -import { IsArray, IsNumber, IsOptional, IsString } from "class-validator" -import { defaultAdminRegionFields, defaultAdminRegionRelations } from "." - +import { + IsArray, + IsNumber, + IsObject, + IsOptional, + IsString, +} from "class-validator" import { EntityManager } from "typeorm" + +import { validator } from "../../../../utils/validator" import { Region } from "../../../.." import RegionService from "../../../../services/region" -import { validator } from "../../../../utils/validator" +import { defaultAdminRegionRelations, defaultAdminRegionFields } from "." /** * @oas [post] /regions @@ -113,4 +119,8 @@ export class AdminPostRegionsReq { @IsArray() @IsString({ each: true }) countries: string[] + + @IsObject() + @IsOptional() + metadata?: Record } diff --git a/packages/medusa/src/api/routes/admin/regions/index.ts b/packages/medusa/src/api/routes/admin/regions/index.ts index 4fffb786a1..d8abb85286 100644 --- a/packages/medusa/src/api/routes/admin/regions/index.ts +++ b/packages/medusa/src/api/routes/admin/regions/index.ts @@ -58,7 +58,7 @@ export default (app) => { return app } -export const defaultAdminRegionFields = [ +export const defaultAdminRegionFields: (keyof Region)[] = [ "id", "name", "automatic_taxes", diff --git a/packages/medusa/src/api/routes/admin/regions/update-region.ts b/packages/medusa/src/api/routes/admin/regions/update-region.ts index fa67f12a10..7cbce73dfe 100644 --- a/packages/medusa/src/api/routes/admin/regions/update-region.ts +++ b/packages/medusa/src/api/routes/admin/regions/update-region.ts @@ -2,14 +2,15 @@ import { IsArray, IsBoolean, IsNumber, + IsObject, IsOptional, IsString, } from "class-validator" -import { defaultAdminRegionFields, defaultAdminRegionRelations } from "." - import { EntityManager } from "typeorm" -import RegionService from "../../../../services/region" + import { validator } from "../../../../utils/validator" +import RegionService from "../../../../services/region" +import { defaultAdminRegionRelations, defaultAdminRegionFields } from "." /** * @oas [post] /regions/{id} @@ -139,4 +140,8 @@ export class AdminPostRegionsRegionReq { @IsString({ each: true }) @IsOptional() countries?: string[] + + @IsObject() + @IsOptional() + metadata?: Record } diff --git a/packages/medusa/src/services/__tests__/region.js b/packages/medusa/src/services/__tests__/region.ts similarity index 77% rename from packages/medusa/src/services/__tests__/region.js rename to packages/medusa/src/services/__tests__/region.ts index 7b1b573c24..921fd8999b 100644 --- a/packages/medusa/src/services/__tests__/region.js +++ b/packages/medusa/src/services/__tests__/region.ts @@ -1,69 +1,83 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import RegionService from "../region" +import { + EventBusService, + FulfillmentProviderService, + PaymentProviderService, + StoreService, +} from "../index" +import { CreateRegionInput } from "../../types/region" const eventBusService = { emit: jest.fn(), - withTransaction: function() { + withTransaction: function () { return this }, -} +} as unknown as EventBusService describe("RegionService", () => { + const regionRepository = MockRepository({}) + + const ppRepository = MockRepository({ + findOne: (query) => { + if (query.where.id === "should_fail") { + return Promise.resolve(undefined) + } + return Promise.resolve({ + id: "default_provider", + }) + }, + }) + const fpRepository = MockRepository({ + findOne: (query) => { + if (query.where.id === "should_fail") { + return Promise.resolve(undefined) + } + return Promise.resolve({ + id: "default_provider", + }) + }, + }) + const countryRepository = MockRepository({ + findOne: (query) => { + if (query.where.iso_2 === "dk") { + return Promise.resolve({ + id: IdMap.getId("dk"), + name: "Denmark", + display_name: "Denmark", + region_id: IdMap.getId("dk-reg"), + }) + } + return Promise.resolve({ + id: IdMap.getId("test-country"), + name: "World", + }) + }, + }) + + const currencyRepository = MockRepository({ + findOne: () => Promise.resolve({ code: "usd" }), + }) + + const storeService = { + withTransaction: function () { + return this + }, + retrieve: () => { + return { + id: IdMap.getId("test-store"), + currencies: [{ code: "dkk" }, { code: "usd" }, { code: "eur" }], + } + }, + } as unknown as StoreService + + const taxProviderRepository = MockRepository({}) + + const fulfillmentProviderService = {} as FulfillmentProviderService + + const paymentProviderService = {} as PaymentProviderService + describe("create", () => { - const regionRepository = MockRepository({}) - const ppRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) - const fpRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) - const countryRepository = MockRepository({ - findOne: (query) => { - if (query.where.iso_2 === "dk") { - return Promise.resolve({ - id: IdMap.getId("dk"), - name: "Denmark", - display_name: "Denmark", - region_id: IdMap.getId("dk-reg"), - }) - } - return Promise.resolve({ - id: IdMap.getId("test-country"), - name: "World", - }) - }, - }) - - const currencyRepository = MockRepository({ - findOne: () => Promise.resolve({ code: "usd" }), - }) - - const storeService = { - withTransaction: function() { - return this - }, - retrieve: () => { - return { - id: IdMap.getId("test-store"), - currencies: [{ code: "dkk" }, { code: "usd" }, { code: "eur" }], - } - }, - } - const regionService = new RegionService({ manager: MockManager, eventBusService, @@ -73,6 +87,9 @@ describe("RegionService", () => { regionRepository, countryRepository, storeService, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, }) beforeEach(async () => { @@ -85,7 +102,7 @@ describe("RegionService", () => { currency_code: "USD", tax_rate: 0.25, countries: ["US"], - }) + } as CreateRegionInput) expect(regionRepository.create).toHaveBeenCalledTimes(1) expect(regionRepository.create).toHaveBeenCalledWith({ @@ -106,7 +123,7 @@ describe("RegionService", () => { currency_code: "EUR", tax_rate: 0.25, countries: ["DK"], - }) + } as CreateRegionInput) } catch (error) { expect(error.message).toBe( `Denmark already exists in region ${IdMap.getId("dk-reg")}` @@ -149,7 +166,7 @@ describe("RegionService", () => { tax_rate: 0.25, countries: ["US"], payment_providers: ["should_fail"], - }) + } as CreateRegionInput) } catch (error) { expect(error.message).toBe("Payment provider not found") } @@ -163,7 +180,7 @@ describe("RegionService", () => { tax_rate: 0.25, countries: ["US"], fulfillment_providers: ["should_fail"], - }) + } as CreateRegionInput) } catch (error) { expect(error.message).toBe("Fulfillment provider not found") } @@ -179,6 +196,14 @@ describe("RegionService", () => { manager: MockManager, eventBusService, regionRepository, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, + fulfillmentProviderRepository: fpRepository, + paymentProviderRepository: ppRepository, + currencyRepository, + countryRepository, + storeService, }) beforeEach(async () => { @@ -196,27 +221,6 @@ describe("RegionService", () => { }) describe("validateFields_", () => { - const regionRepository = MockRepository({}) - const ppRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) - const fpRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) const countryRepository = MockRepository({ findOne: (query) => { if (query.where.iso_2 === "dk") { @@ -234,24 +238,16 @@ describe("RegionService", () => { }, }) - const storeService = { - withTransaction: function() { - return this - }, - retrieve: () => { - return { - id: IdMap.getId("test-store"), - currencies: [{ code: "dkk" }, { code: "usd" }, { code: "eur" }], - } - }, - } - const regionService = new RegionService({ manager: MockManager, eventBusService, + regionRepository, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, fulfillmentProviderRepository: fpRepository, paymentProviderRepository: ppRepository, - regionRepository, + currencyRepository, countryRepository, storeService, }) @@ -262,13 +258,19 @@ describe("RegionService", () => { it("throws on invalid country code", async () => { await expect( - regionService.validateFields_({ countries: ["ddd"] }) + // @ts-ignore + regionService.validateFields({ + countries: ["ddd"], + } as CreateRegionInput) ).rejects.toThrow("Invalid country code") }) it("throws on in use country code", async () => { await expect( - regionService.validateFields_({ countries: ["DK"] }) + // @ts-ignore + regionService.validateFields({ + countries: ["DK"], + } as CreateRegionInput) ).rejects.toThrow( `Denmark already exists in region ${IdMap.getId("dk-reg")}` ) @@ -276,15 +278,19 @@ describe("RegionService", () => { it("throws on unknown payment providers", async () => { await expect( - regionService.validateFields_({ payment_providers: ["should_fail"] }) + // @ts-ignore + regionService.validateFields({ + payment_providers: ["should_fail"], + } as CreateRegionInput) ).rejects.toThrow("Payment provider not found") }) it("throws on unknown fulfillment providers", async () => { await expect( - regionService.validateFields_({ + // @ts-ignore + regionService.validateFields({ fulfillment_providers: ["should_fail"], - }) + } as CreateRegionInput) ).rejects.toThrow("Fulfillment provider not found") }) }) @@ -293,66 +299,18 @@ describe("RegionService", () => { const regionRepository = MockRepository({ findOne: () => Promise.resolve({ id: IdMap.getId("test-region") }), }) - const ppRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) - const fpRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) - const countryRepository = MockRepository({ - findOne: (query) => { - if (query.where.iso_2 === "dk") { - return Promise.resolve({ - id: IdMap.getId("dk"), - name: "Denmark", - region_id: IdMap.getId("dk-reg"), - }) - } - return Promise.resolve({ - id: IdMap.getId("test-country"), - name: "World", - }) - }, - }) - - const storeService = { - withTransaction: function() { - return this - }, - retrieve: () => { - return { - id: IdMap.getId("test-store"), - currencies: [{ code: "dkk" }, { code: "usd" }, { code: "eur" }], - } - }, - } - - const currencyRepository = MockRepository({ - findOne: () => Promise.resolve({ code: "eur" }), - }) const regionService = new RegionService({ manager: MockManager, eventBusService, + regionRepository, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, fulfillmentProviderRepository: fpRepository, paymentProviderRepository: ppRepository, - regionRepository, - countryRepository, currencyRepository, + countryRepository, storeService, }) @@ -395,14 +353,21 @@ describe("RegionService", () => { }), }) const countryRepository = MockRepository({ - findOne: (query) => Promise.resolve(), + findOne: () => Promise.resolve(), }) const regionService = new RegionService({ manager: MockManager, eventBusService, regionRepository, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, + fulfillmentProviderRepository: fpRepository, + paymentProviderRepository: ppRepository, + currencyRepository, countryRepository, + storeService, }) beforeEach(async () => { @@ -434,6 +399,7 @@ describe("RegionService", () => { return Promise.resolve({ id: IdMap.getId("region") }) }, }) + const countryRepository = MockRepository({ findOne: (query) => { if (query.where.iso_2 === "dk") { @@ -454,7 +420,14 @@ describe("RegionService", () => { manager: MockManager, eventBusService, regionRepository, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, + fulfillmentProviderRepository: fpRepository, + paymentProviderRepository: ppRepository, + currencyRepository, countryRepository, + storeService, }) beforeEach(async () => { @@ -499,7 +472,7 @@ describe("RegionService", () => { manager: MockManager, eventBusService, regionRepository, - }) + } as any) beforeEach(async () => { jest.clearAllMocks() @@ -524,33 +497,19 @@ describe("RegionService", () => { payment_providers: [{ id: "sweden_provider" }], }), }) - const ppRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) - const fpRepository = MockRepository({ - findOne: (query) => { - if (query.where.id === "should_fail") { - return Promise.resolve(undefined) - } - return Promise.resolve({ - id: "default_provider", - }) - }, - }) const regionService = new RegionService({ manager: MockManager, eventBusService, + regionRepository, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, fulfillmentProviderRepository: fpRepository, paymentProviderRepository: ppRepository, - regionRepository, + currencyRepository, + countryRepository, + storeService, }) beforeEach(async () => { @@ -609,8 +568,15 @@ describe("RegionService", () => { const regionService = new RegionService({ manager: MockManager, eventBusService, - fulfillmentProviderRepository: fpRepository, regionRepository, + fulfillmentProviderService, + taxProviderRepository, + paymentProviderService, + fulfillmentProviderRepository: fpRepository, + paymentProviderRepository: ppRepository, + currencyRepository, + countryRepository, + storeService, }) beforeEach(async () => { @@ -660,7 +626,7 @@ describe("RegionService", () => { manager: MockManager, eventBusService, regionRepository, - }) + } as any) beforeEach(async () => { jest.clearAllMocks() @@ -695,7 +661,7 @@ describe("RegionService", () => { manager: MockManager, eventBusService, regionRepository, - }) + } as any) beforeEach(async () => { jest.clearAllMocks() diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index b7eb19395c..826b3e735b 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -393,7 +393,7 @@ class CartService extends TransactionBaseService { } } else { if (data.shipping_address) { - if (!regCountries.includes(data.shipping_address.country_code)) { + if (!regCountries.includes(data.shipping_address.country_code!)) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "Shipping country not in region" @@ -403,7 +403,10 @@ class CartService extends TransactionBaseService { } if (data.shipping_address_id) { const addr = await addressRepo.findOne(data.shipping_address_id) - if (addr && !regCountries.includes(addr.country_code)) { + if ( + addr?.country_code && + !regCountries.includes(addr.country_code) + ) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "Shipping country not in region" diff --git a/packages/medusa/src/services/pricing.ts b/packages/medusa/src/services/pricing.ts index 5840bb55d3..6af05e28ef 100644 --- a/packages/medusa/src/services/pricing.ts +++ b/packages/medusa/src/services/pricing.ts @@ -64,7 +64,7 @@ class PricingService extends TransactionBaseService { context: PriceSelectionContext ): Promise { let automaticTaxes = false - let taxRate = null + let taxRate: number | null = null let currencyCode = context.currency_code if (context.region_id) { diff --git a/packages/medusa/src/services/product-variant.ts b/packages/medusa/src/services/product-variant.ts index 311e2ce117..c96827ab4d 100644 --- a/packages/medusa/src/services/product-variant.ts +++ b/packages/medusa/src/services/product-variant.ts @@ -360,7 +360,7 @@ class ProductVariantService extends BaseService { /** * Updates a variant's prices. * Deletes any prices that are not in the update object, and is not associated with a price list. - * @param variantId - the id of variant variant + * @param variantId - the id of variant * @param prices - the update prices * @returns {Promise} empty promise */ diff --git a/packages/medusa/src/services/region.js b/packages/medusa/src/services/region.ts similarity index 62% rename from packages/medusa/src/services/region.js rename to packages/medusa/src/services/region.ts index d31d3af67c..290c955f6b 100644 --- a/packages/medusa/src/services/region.js +++ b/packages/medusa/src/services/region.ts @@ -1,18 +1,64 @@ +import { DeepPartial, EntityManager } from "typeorm" + import { MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" + +import StoreService from "./store" +import EventBusService from "./event-bus" import { countries } from "../utils/countries" +import { TransactionBaseService } from "../interfaces" +import { RegionRepository } from "../repositories/region" +import { CountryRepository } from "../repositories/country" +import { CurrencyRepository } from "../repositories/currency" +import { PaymentProviderRepository } from "../repositories/payment-provider" +import { FulfillmentProviderRepository } from "../repositories/fulfillment-provider" +import { TaxProviderRepository } from "../repositories/tax-provider" +import FulfillmentProviderService from "./fulfillment-provider" +import { Country, Currency, Region } from "../models" +import { FindConfig, Selector } from "../types/common" +import { CreateRegionInput, UpdateRegionInput } from "../types/region" +import { buildQuery, setMetadata } from "../utils" +import { PaymentProviderService } from "./index" + +type InjectedDependencies = { + manager: EntityManager + storeService: StoreService + eventBusService: EventBusService + paymentProviderService: PaymentProviderService + fulfillmentProviderService: FulfillmentProviderService + + regionRepository: typeof RegionRepository + countryRepository: typeof CountryRepository + currencyRepository: typeof CurrencyRepository + taxProviderRepository: typeof TaxProviderRepository + paymentProviderRepository: typeof PaymentProviderRepository + fulfillmentProviderRepository: typeof FulfillmentProviderRepository +} /** * Provides layer to manipulate regions. * @extends BaseService */ -class RegionService extends BaseService { +class RegionService extends TransactionBaseService { static Events = { UPDATED: "region.updated", CREATED: "region.created", DELETED: "region.deleted", } + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + protected readonly eventBus_: EventBusService + protected readonly storeService_: StoreService + protected readonly paymentProviderService_: PaymentProviderService + protected readonly fulfillmentProviderService_: FulfillmentProviderService + protected readonly regionRepository_: typeof RegionRepository + protected readonly countryRepository_: typeof CountryRepository + protected readonly currencyRepository_: typeof CurrencyRepository + protected readonly paymentProviderRepository_: typeof PaymentProviderRepository + protected readonly fulfillmentProviderRepository_: typeof FulfillmentProviderRepository + protected readonly taxProviderRepository_: typeof TaxProviderRepository + constructor({ manager, regionRepository, @@ -25,75 +71,42 @@ class RegionService extends BaseService { taxProviderRepository, paymentProviderService, fulfillmentProviderService, - }) { - super() - - /** @private @const {EntityManager} */ - this.manager_ = manager - - /** @private @const {RegionRepository} */ - this.regionRepository_ = regionRepository - - /** @private @const {CountryRepository} */ - this.countryRepository_ = countryRepository - - /** @private @const {StoreService} */ - this.storeService_ = storeService - - /** @private @const {EventBus} */ - this.eventBus_ = eventBusService - - /** @private @const {CurrencyRepository} */ - this.currencyRepository_ = currencyRepository - - /** @private @const {PaymentProviderRepository} */ - this.paymentProviderRepository_ = paymentProviderRepository - - /** @private @const {FulfillmentProviderRepository} */ - this.fulfillmentProviderRepository_ = fulfillmentProviderRepository - - /** @private @const {PaymentProviderService} */ - this.paymentProviderService_ = paymentProviderService - - /** @private @const {typeof TaxProviderService} */ - this.taxProviderRepository_ = taxProviderRepository - - /** @private @const {FulfillmentProviderService} */ - this.fulfillmentProviderService_ = fulfillmentProviderService - } - - withTransaction(transactionManager) { - if (!transactionManager) { - return this - } - - const cloned = new RegionService({ - manager: transactionManager, - regionRepository: this.regionRepository_, - currencyRepository: this.currencyRepository_, - countryRepository: this.countryRepository_, - storeService: this.storeService_, - eventBusService: this.eventBus_, - paymentProviderRepository: this.paymentProviderRepository_, - paymentProviderService: this.paymentProviderService_, - taxProviderRepository: this.taxProviderRepository_, - taxProviderService: this.taxProviderService_, - fulfillmentProviderRepository: this.fulfillmentProviderRepository_, - fulfillmentProviderService: this.fulfillmentProviderService_, + }: InjectedDependencies) { + super({ + manager, + regionRepository, + countryRepository, + storeService, + eventBusService, + currencyRepository, + paymentProviderRepository, + fulfillmentProviderRepository, + taxProviderRepository, + paymentProviderService, + fulfillmentProviderService, }) - cloned.transactionManager_ = transactionManager - - return cloned + this.manager_ = manager + this.regionRepository_ = regionRepository + this.countryRepository_ = countryRepository + this.storeService_ = storeService + this.eventBus_ = eventBusService + this.currencyRepository_ = currencyRepository + this.paymentProviderRepository_ = paymentProviderRepository + this.fulfillmentProviderRepository_ = fulfillmentProviderRepository + this.paymentProviderService_ = paymentProviderService + this.taxProviderRepository_ = taxProviderRepository + this.fulfillmentProviderService_ = fulfillmentProviderService } /** * Creates a region. - * @param {Region} regionObject - the unvalidated region - * @return {Region} the newly created region + * + * @param data - the unvalidated region + * @return the newly created region */ - async create(regionObject) { - return this.atomicPhase_(async (manager) => { + async create(data: CreateRegionInput): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepository = manager.getCustomRepository( this.regionRepository_ ) @@ -101,13 +114,14 @@ class RegionService extends BaseService { this.currencyRepository_ ) - const { metadata, currency_code, ...toValidate } = regionObject + const regionObject = { ...data } as DeepPartial + const { metadata, currency_code, ...toValidate } = data - const validated = await this.validateFields_(toValidate) + const validated = await this.validateFields(toValidate) if (currency_code) { // will throw if currency is not added to store currencies - await this.validateCurrency_(currency_code) + await this.validateCurrency(currency_code) const currency = await currencyRepository.findOne({ where: { code: currency_code.toLowerCase() }, }) @@ -124,14 +138,17 @@ class RegionService extends BaseService { } if (metadata) { - regionObject.metadata = this.setMetadata_(regionObject, metadata) + regionObject.metadata = setMetadata( + { metadata: regionObject.metadata ?? null }, + metadata + ) } for (const [key, value] of Object.entries(validated)) { regionObject[key] = value } - const created = regionRepository.create(regionObject) + const created = regionRepository.create(regionObject) as Region const result = await regionRepository.save(created) await this.eventBus_ @@ -146,12 +163,13 @@ class RegionService extends BaseService { /** * Updates a region - * @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 + * + * @param regionId - the region to update + * @param update - the data to update the region with + * @return the result of the update operation */ - async update(regionId, update) { - return this.atomicPhase_(async (manager) => { + async update(regionId: string, update: UpdateRegionInput): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepository = manager.getCustomRepository( this.regionRepository_ ) @@ -163,11 +181,11 @@ class RegionService extends BaseService { const { metadata, currency_code, ...toValidate } = update - const validated = await this.validateFields_(toValidate, region.id) + const validated = await this.validateFields(toValidate, region.id) if (currency_code) { // will throw if currency is not added to store currencies - await this.validateCurrency_(currency_code) + await this.validateCurrency(currency_code) const currency = await currencyRepository.findOne({ where: { code: currency_code.toLowerCase() }, }) @@ -183,7 +201,7 @@ class RegionService extends BaseService { } if (metadata) { - region.metadata = this.setMetadata_(region, metadata) + region.metadata = setMetadata(region, metadata) } for (const [key, value] of Object.entries(validated)) { @@ -204,13 +222,19 @@ class RegionService extends BaseService { } /** - * Validates fields for creation and updates. If the region already exisits + * Validates fields for creation and updates. If the region already exists * 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 + * + * @param regionData - the region data to validate + * @param id - optional id of the region to check against + * @return the validated region data */ - async validateFields_(region, id = undefined) { + protected async validateFields< + T extends CreateRegionInput | UpdateRegionInput + >( + regionData: Omit, + id?: T extends UpdateRegionInput ? string : undefined + ): Promise> { const ppRepository = this.manager_.getCustomRepository( this.paymentProviderRepository_ ) @@ -221,23 +245,25 @@ class RegionService extends BaseService { this.taxProviderRepository_ ) + const region = { ...regionData } as DeepPartial + if (region.tax_rate) { - this.validateTaxRate_(region.tax_rate) + this.validateTaxRate(region.tax_rate) } - if (region.countries) { + if (regionData.countries) { region.countries = await Promise.all( - region.countries.map((countryCode) => - this.validateCountry_(countryCode, id) + regionData.countries!.map((countryCode) => + this.validateCountry(countryCode, id!) ) ).catch((err) => { throw err }) } - if (region.tax_provider_id) { + if ((regionData as UpdateRegionInput).tax_provider_id) { const tp = await tpRepository.findOne({ - where: { id: region.tax_provider_id }, + where: { id: (regionData as UpdateRegionInput).tax_provider_id }, }) if (!tp) { throw new MedusaError( @@ -247,9 +273,9 @@ class RegionService extends BaseService { } } - if (region.payment_providers) { + if (regionData.payment_providers) { region.payment_providers = await Promise.all( - region.payment_providers.map(async (pId) => { + regionData.payment_providers.map(async (pId) => { const pp = await ppRepository.findOne({ where: { id: pId } }) if (!pp) { throw new MedusaError( @@ -263,9 +289,9 @@ class RegionService extends BaseService { ) } - if (region.fulfillment_providers) { + if (regionData.fulfillment_providers) { region.fulfillment_providers = await Promise.all( - region.fulfillment_providers.map(async (fId) => { + regionData.fulfillment_providers.map(async (fId) => { const fp = await fpRepository.findOne({ where: { id: fId } }) if (!fp) { throw new MedusaError( @@ -284,9 +310,12 @@ class RegionService extends BaseService { /** * 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 + * + * @param taxRate - a number representing the tax rate of the region + * @throws if the tax rate isn't number between 0-100 + * @return void */ - validateTaxRate_(taxRate) { + protected validateTaxRate(taxRate: number): void | never { if (taxRate > 100 || taxRate < 0) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -297,9 +326,14 @@ class RegionService extends BaseService { /** * Validates a currency code. Will throw if the currency code doesn't exist. - * @param {string} currencyCode - an ISO currency code + * + * @param currencyCode - an ISO currency code + * @throws if the provided currency code is invalid + * @return void */ - async validateCurrency_(currencyCode) { + protected async validateCurrency( + currencyCode: Currency["code"] + ): Promise { const store = await this.storeService_ .withTransaction(this.transactionManager_) .retrieve({ relations: ["currencies"] }) @@ -317,10 +351,15 @@ class RegionService extends BaseService { /** * Validates a country code. Will normalize the code before checking for * existence. - * @param {string} code - a 2 digit alphanumeric ISO country code - * @param {string} regionId - the id of the current region to check against + * + * @param code - a 2 digit alphanumeric ISO country code + * @param regionId - the id of the current region to check against + * @return the validated Country */ - async validateCountry_(code, regionId) { + protected async validateCountry( + code: Country["iso_2"], + regionId: string + ): Promise { const countryRepository = this.manager_.getCustomRepository( this.countryRepository_ ) @@ -359,7 +398,17 @@ class RegionService extends BaseService { return country } - async retrieveByCountryCode(code, config = {}) { + /** + * Retrieve a region by country code. + * + * @param code - a 2 digit alphanumeric ISO country code + * @param config - region find config + * @return a Region with country code + */ + async retrieveByCountryCode( + code: Country["iso_2"], + config: FindConfig = {} + ): Promise { const countryRepository = this.manager_.getCustomRepository( this.countryRepository_ ) @@ -389,17 +438,20 @@ class RegionService extends BaseService { /** * Retrieves a region by its id. - * @param {string} regionId - the id of the region to retrieve - * @param {object} config - configuration settings - * @return {Region} the region + * + * @param regionId - the id of the region to retrieve + * @param config - configuration settings + * @return the region */ - async retrieve(regionId, config = {}) { + async retrieve( + regionId: string, + config: FindConfig = {} + ): Promise { const regionRepository = this.manager_.getCustomRepository( this.regionRepository_ ) - const validatedId = this.validateId_(regionId) - const query = this.buildQuery_({ id: validatedId }, config) + const query = buildQuery({ id: regionId }, config) const region = await regionRepository.findOne(query) if (!region) { @@ -408,29 +460,39 @@ class RegionService extends BaseService { `Region with ${regionId} was not found` ) } + return region } /** * Lists all regions based on a query + * * @param {object} selector - query object for find * @param {object} config - configuration settings * @return {Promise} result of the find operation */ - async list(selector = {}, config = { relations: [], skip: 0, take: 10 }) { + async list( + selector: Selector = {}, + config: FindConfig = { + relations: [], + skip: 0, + take: 10, + } + ): Promise { const regionRepo = this.manager_.getCustomRepository(this.regionRepository_) - const query = this.buildQuery_(selector, config) + const query = buildQuery(selector, config) return regionRepo.find(query) } /** * Deletes a region. - * @param {string} regionId - the region to delete - * @return {Promise} the result of the delete operation + * + * @param regionId - the region to delete + * @return the result of the delete operation */ - async delete(regionId) { - return this.atomicPhase_(async (manager) => { + async delete(regionId: string): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepo = manager.getCustomRepository(this.regionRepository_) const countryRepo = manager.getCustomRepository(this.countryRepository_) @@ -455,15 +517,16 @@ class RegionService extends BaseService { /** * 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 + * + * @param regionId - the region to add a country to + * @param code - a 2 digit alphanumeric ISO country code. + * @return the updated Region */ - async addCountry(regionId, code) { - return this.atomicPhase_(async (manager) => { + async addCountry(regionId: string, code: Country["iso_2"]): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepo = manager.getCustomRepository(this.regionRepository_) - const country = await this.validateCountry_(code, regionId) + const country = await this.validateCountry(code, regionId) const region = await this.retrieve(regionId, { relations: ["countries"] }) @@ -491,13 +554,17 @@ class RegionService extends BaseService { } /** - * 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 + * Removes a country from a Region. + * + * @param regionId - the region to remove from + * @param code - a 2 digit alphanumeric ISO country code to remove + * @return the updated Region */ - async removeCountry(regionId, code) { - return this.atomicPhase_(async (manager) => { + async removeCountry( + regionId: string, + code: Country["iso_2"] + ): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepo = manager.getCustomRepository(this.regionRepository_) const region = await this.retrieve(regionId, { relations: ["countries"] }) @@ -521,6 +588,7 @@ class RegionService extends BaseService { id: updated.id, fields: ["countries"], }) + return updated }) } @@ -528,12 +596,16 @@ class RegionService extends BaseService { /** * 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 + * + * @param regionId - the region to add the provider to + * @param providerId - the provider to add to the region + * @return the updated Region */ - async addPaymentProvider(regionId, providerId) { - return this.atomicPhase_(async (manager) => { + async addPaymentProvider( + regionId: string, + providerId: string + ): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepo = manager.getCustomRepository(this.regionRepository_) const ppRepo = manager.getCustomRepository( this.paymentProviderRepository_ @@ -575,12 +647,16 @@ class RegionService extends BaseService { /** * 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 + * + * @param regionId - the region to add the provider to + * @param providerId - the provider to add to the region + * @return the updated Region */ - async addFulfillmentProvider(regionId, providerId) { - return this.atomicPhase_(async (manager) => { + async addFulfillmentProvider( + regionId: string, + providerId: string + ): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepo = manager.getCustomRepository(this.regionRepository_) const fpRepo = manager.getCustomRepository( this.fulfillmentProviderRepository_ @@ -592,7 +668,7 @@ class RegionService extends BaseService { // Check if region already has payment provider if (region.fulfillment_providers.find(({ id }) => id === providerId)) { - return Promise.resolve() + return Promise.resolve(region) } const fp = await fpRepo.findOne({ where: { id: providerId } }) @@ -613,18 +689,23 @@ class RegionService extends BaseService { id: updated.id, fields: ["fulfillment_providers"], }) + return updated }) } /** * 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 + * + * @param regionId - the region to remove the provider from + * @param providerId - the provider to remove from the region + * @return the updated Region */ - async removePaymentProvider(regionId, providerId) { - return this.atomicPhase_(async (manager) => { + async removePaymentProvider( + regionId: string, + providerId: string + ): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepo = manager.getCustomRepository(this.regionRepository_) const region = await this.retrieve(regionId, { @@ -633,7 +714,7 @@ class RegionService extends BaseService { // Check if region already has payment provider if (!region.payment_providers.find(({ id }) => id === providerId)) { - return Promise.resolve() + return Promise.resolve(region) } region.payment_providers = region.payment_providers.filter( @@ -647,18 +728,23 @@ class RegionService extends BaseService { id: updated.id, fields: ["payment_providers"], }) + return updated }) } /** * 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 + * + * @param regionId - the region to remove the provider from + * @param providerId - the provider to remove from the region + * @return the updated Region */ - async removeFulfillmentProvider(regionId, providerId) { - return this.atomicPhase_(async (manager) => { + async removeFulfillmentProvider( + regionId: string, + providerId: string + ): Promise { + return await this.atomicPhase_(async (manager) => { const regionRepo = manager.getCustomRepository(this.regionRepository_) const region = await this.retrieve(regionId, { @@ -667,7 +753,7 @@ class RegionService extends BaseService { // Check if region already has payment provider if (!region.fulfillment_providers.find(({ id }) => id === providerId)) { - return Promise.resolve() + return Promise.resolve(region) } region.fulfillment_providers = region.fulfillment_providers.filter( @@ -681,6 +767,7 @@ class RegionService extends BaseService { id: updated.id, fields: ["fulfillment_providers"], }) + return updated }) } diff --git a/packages/medusa/src/types/region.ts b/packages/medusa/src/types/region.ts new file mode 100644 index 0000000000..89a2803356 --- /dev/null +++ b/packages/medusa/src/types/region.ts @@ -0,0 +1,27 @@ +import { FindConfig } from "./common" +import { Region } from "../models" + +export type UpdateRegionInput = { + name?: string + currency_code?: string + tax_code?: string + tax_rate?: number + gift_cards_taxable?: boolean + automatic_taxes?: boolean + tax_provider_id?: string | null + payment_providers?: string[] + fulfillment_providers?: string[] + countries?: string[] + metadata?: Record +} + +export type CreateRegionInput = { + name: string + currency_code: string + tax_code?: string + tax_rate: number + payment_providers: string[] + fulfillment_providers: string[] + countries: string[] + metadata?: Record +}