diff --git a/packages/medusa/src/api/routes/admin/store/__tests__/get-store.js b/packages/medusa/src/api/routes/admin/store/__tests__/get-store.js index b2db40b1e7..cfa1257d42 100644 --- a/packages/medusa/src/api/routes/admin/store/__tests__/get-store.js +++ b/packages/medusa/src/api/routes/admin/store/__tests__/get-store.js @@ -22,10 +22,12 @@ describe("GET /admin/store", () => { it("calls service retrieve", () => { expect(StoreServiceMock.retrieve).toHaveBeenCalledTimes(1) - expect(StoreServiceMock.retrieve).toHaveBeenCalledWith([ - "currencies", - "default_currency", - ]) + expect(StoreServiceMock.retrieve).toHaveBeenCalledWith({ + relations: [ + "currencies", + "default_currency", + ] + }) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/store/get-store.ts b/packages/medusa/src/api/routes/admin/store/get-store.ts index c27507fc8b..1498d4b38f 100644 --- a/packages/medusa/src/api/routes/admin/store/get-store.ts +++ b/packages/medusa/src/api/routes/admin/store/get-store.ts @@ -3,6 +3,7 @@ import { PaymentProviderService, StoreService, } from "../../../../services" +import { FulfillmentProvider, PaymentProvider, Store } from "../../../../models" /** * @oas [get] /store @@ -30,7 +31,12 @@ export default async (req, res) => { const fulfillmentProviderService: FulfillmentProviderService = req.scope.resolve("fulfillmentProviderService") - const data = await storeService.retrieve(["currencies", "default_currency"]) + const data = (await storeService.retrieve({ + relations: ["currencies", "default_currency"], + })) as Store & { + payment_providers: PaymentProvider[] + fulfillment_providers: FulfillmentProvider[] + } const paymentProviders = await paymentProviderService.list() const fulfillmentProviders = await fulfillmentProviderService.list() diff --git a/packages/medusa/src/api/routes/admin/store/update-store.ts b/packages/medusa/src/api/routes/admin/store/update-store.ts index 2f6b88789b..9e8de32132 100644 --- a/packages/medusa/src/api/routes/admin/store/update-store.ts +++ b/packages/medusa/src/api/routes/admin/store/update-store.ts @@ -1,4 +1,4 @@ -import { IsArray, IsOptional, IsString } from "class-validator" +import { IsArray, IsOptional, IsString, IsObject } from "class-validator" import { StoreService } from "../../../../services" import { validator } from "../../../../utils/validator" @@ -75,4 +75,8 @@ export class AdminPostStoreReq { @IsString({ each: true }) @IsOptional() currencies?: string[] + + @IsObject() + @IsOptional() + metadata?: Record } diff --git a/packages/medusa/src/api/routes/store/customers/get-payment-methods.ts b/packages/medusa/src/api/routes/store/customers/get-payment-methods.ts index da8e2d1174..c964102669 100644 --- a/packages/medusa/src/api/routes/store/customers/get-payment-methods.ts +++ b/packages/medusa/src/api/routes/store/customers/get-payment-methods.ts @@ -2,6 +2,7 @@ import { Customer } from "../../../.." import CustomerService from "../../../../services/customer" import PaymentProviderService from "../../../../services/payment-provider" import StoreService from "../../../../services/store" +import { PaymentProvider } from "../../../../models" /** * @oas [get] /customers/me/payment-methods @@ -32,8 +33,6 @@ import StoreService from "../../../../services/store" export default async (req, res) => { const id = req.user.customer_id - const storeService: StoreService = req.scope.resolve("storeService") - const paymentProviderService: PaymentProviderService = req.scope.resolve( "paymentProviderService" ) @@ -42,15 +41,18 @@ export default async (req, res) => { const customer: Customer = await customerService.retrieve(id) - const store = await storeService.retrieve(["payment_providers"]) + const paymentProviders: PaymentProvider[] = + await paymentProviderService.list() const methods = await Promise.all( - store.payment_providers.map(async (next: string) => { - const provider = paymentProviderService.retrieveProvider(next) + paymentProviders.map(async (paymentProvider: PaymentProvider) => { + const provider = paymentProviderService.retrieveProvider( + paymentProvider.id + ) const pMethods = await provider.retrieveSavedMethods(customer) return pMethods.map((m) => ({ - provider_id: next, + provider_id: paymentProvider.id, data: m, })) }) diff --git a/packages/medusa/src/services/__tests__/store.js b/packages/medusa/src/services/__tests__/store.js index 1045fc14c0..ab79b70f4f 100644 --- a/packages/medusa/src/services/__tests__/store.js +++ b/packages/medusa/src/services/__tests__/store.js @@ -44,7 +44,7 @@ describe("StoreService", () => { }) it("successfully retrieve store", async () => { - await storeService.retrieve() + await storeService.retrieve().catch(() => void 0) expect(storeRepository.findOne).toHaveBeenCalledTimes(1) }) @@ -92,7 +92,7 @@ describe("StoreService", () => { storeService.update({ currencies: ["1cd", "usd"], }) - ).rejects.toThrow("Invalid currency 1cd") + ).rejects.toThrow("Currency with code 1cd does not exist") expect(storeRepository.findOne).toHaveBeenCalledTimes(1) }) @@ -148,8 +148,6 @@ describe("StoreService", () => { await expect(storeService.addCurrency("1cd")).rejects.toThrow( "Currency 1cd not found" ) - - expect(storeRepository.findOne).toHaveBeenCalledTimes(1) }) it("fails if currency already existis", async () => { diff --git a/packages/medusa/src/services/region.js b/packages/medusa/src/services/region.js index ac1c3da946..d31d3af67c 100644 --- a/packages/medusa/src/services/region.js +++ b/packages/medusa/src/services/region.js @@ -302,7 +302,7 @@ class RegionService extends BaseService { async validateCurrency_(currencyCode) { const store = await this.storeService_ .withTransaction(this.transactionManager_) - .retrieve(["currencies"]) + .retrieve({ relations: ["currencies"] }) const storeCurrencies = store.currencies.map((curr) => curr.code) diff --git a/packages/medusa/src/services/store.js b/packages/medusa/src/services/store.js deleted file mode 100644 index 0f10024092..0000000000 --- a/packages/medusa/src/services/store.js +++ /dev/null @@ -1,271 +0,0 @@ -import { MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" -import { currencies } from "../utils/currencies" - -/** - * Provides layer to manipulate store settings. - * @extends BaseService - */ -class StoreService extends BaseService { - constructor({ - manager, - storeRepository, - currencyRepository, - eventBusService, - }) { - super() - - /** @private @const {EntityManager} */ - this.manager_ = manager - - /** @private @const {StoreRepository} */ - this.storeRepository_ = storeRepository - - /** @private @const {CurrencyRepository} */ - this.currencyRepository_ = currencyRepository - - /** @private @const {EventBus} */ - this.eventBus_ = eventBusService - } - - withTransaction(transactionManager) { - if (!transactionManager) { - return this - } - - const cloned = new StoreService({ - manager: transactionManager, - storeRepository: this.storeRepository_, - currencyRepository: this.currencyRepository_, - eventBusService: this.eventBus_, - }) - - cloned.transactionManager_ = transactionManager - - return cloned - } - - /** - * Creates a store if it doesn't already exist. - * @return {Promise} the store. - */ - async create() { - return this.atomicPhase_(async (manager) => { - const storeRepository = manager.getCustomRepository(this.storeRepository_) - const currencyRepository = manager.getCustomRepository( - this.currencyRepository_ - ) - - let store = await this.retrieve() - - if (!store) { - const s = await storeRepository.create() - // Add default currency (USD) to store currencies - const usd = await currencyRepository.findOne({ - code: "usd", - }) - - if (usd) { - s.currencies = [usd] - } - - store = await storeRepository.save(s) - } - - return store - }) - } - - /** - * Retrieve the store settings. There is always a maximum of one store. - * @param {string[]} relations - relations to fetch with store - * @return {Promise} the store - */ - async retrieve(relations = []) { - const storeRepo = this.manager_.getCustomRepository(this.storeRepository_) - - const store = await storeRepo.findOne({ relations }) - - return store - } - - getDefaultCurrency_(code) { - const currencyObject = currencies[code.toUpperCase()] - - return { - code: currencyObject.code.toLowerCase(), - symbol: currencyObject.symbol, - symbol_native: currencyObject.symbol_native, - name: currencyObject.name, - } - } - - /** - * Updates a store - * @param {object} update - an object with the update values. - * @return {Promise} resolves to the update result. - */ - async update(update) { - return this.atomicPhase_(async (manager) => { - const storeRepository = manager.getCustomRepository(this.storeRepository_) - const currencyRepository = manager.getCustomRepository( - this.currencyRepository_ - ) - - const store = await this.retrieve(["currencies"]) - - const { - metadata, - default_currency_code, - currencies: storeCurrencies, - ...rest - } = update - - if (metadata) { - store.metadata = this.setMetadata_(store.id, metadata) - } - - if (storeCurrencies) { - const defaultCurr = default_currency_code ?? store.default_currency_code - const hasDefCurrency = storeCurrencies.find( - (c) => c.toLowerCase() === defaultCurr.toLowerCase() - ) - - // throw if we are trying to remove a currency from store currently used as default - if (!hasDefCurrency) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `You are not allowed to remove default currency from store currencies without replacing it as well` - ) - } - - store.currencies = await Promise.all( - storeCurrencies.map(async (curr) => { - const currency = await currencyRepository.findOne({ - where: { code: curr.toLowerCase() }, - }) - - if (!currency) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Invalid currency ${curr}` - ) - } - - return currency - }) - ) - } - - if (default_currency_code) { - const storeCurrCodes = store.currencies.map((c) => c.code) - const hasDefCurrency = storeCurrCodes.find( - (c) => c === default_currency_code.toLowerCase() - ) - - // throw if store currencies does not have default currency - if (!hasDefCurrency) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Store does not have currency: ${default_currency_code}` - ) - } - - const curr = await currencyRepository.findOne({ - code: default_currency_code.toLowerCase(), - }) - - if (!curr) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Currency ${default_currency_code} not found` - ) - } - - store.default_currency = curr - store.default_currency_code = curr.code - } - - for (const [key, value] of Object.entries(rest)) { - store[key] = value - } - - const result = await storeRepository.save(store) - return result - }) - } - - /** - * Add a currency to the store - * @param {string} code - 3 character ISO currency code - * @return {Promise} result after update - */ - async addCurrency(code) { - return this.atomicPhase_(async (manager) => { - const storeRepo = manager.getCustomRepository(this.storeRepository_) - const currencyRepository = manager.getCustomRepository( - this.currencyRepository_ - ) - const store = await this.retrieve(["currencies"]) - - const curr = await currencyRepository.findOne({ - where: { code: code.toLowerCase() }, - }) - - if (!curr) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Currency ${code} not found` - ) - } - - if ( - store.currencies.map((c) => c.code).includes(curr.code.toLowerCase()) - ) { - throw new MedusaError( - MedusaError.Types.DUPLICATE_ERROR, - `Currency already added` - ) - } - - store.currencies = [...store.currencies, curr] - const updated = await storeRepo.save(store) - return updated - }) - } - - /** - * Removes a currency from the store - * @param {string} code - 3 character ISO currency code - * @return {Promise} result after update - */ - async removeCurrency(code) { - return this.atomicPhase_(async (manager) => { - const storeRepo = manager.getCustomRepository(this.storeRepository_) - const store = await this.retrieve(["currencies"]) - - const exists = store.currencies.find((c) => c.code === code.toLowerCase()) - // If currency does not exist, return early - if (!exists) { - return store - } - - store.currencies = store.currencies.filter((c) => c.code !== code) - const updated = await storeRepo.save(store) - return updated - }) - } - - /** - * 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 - } -} - -export default StoreService diff --git a/packages/medusa/src/services/store.ts b/packages/medusa/src/services/store.ts new file mode 100644 index 0000000000..b8eb4d118c --- /dev/null +++ b/packages/medusa/src/services/store.ts @@ -0,0 +1,284 @@ +import { MedusaError } from "medusa-core-utils" +import { currencies, Currency } from "../utils/currencies" +import { EntityManager } from "typeorm" +import { StoreRepository } from "../repositories/store" +import { CurrencyRepository } from "../repositories/currency" +import EventBusService from "./event-bus" +import { Store } from "../models" +import { AdminPostStoreReq } from "../api/routes/admin/store" +import { FindConfig } from "../types/common" +import { TransactionBaseService } from "../interfaces" +import { buildQuery, setMetadata } from "../utils" +import { UpdateStoreInput } from "../types/store" + +type InjectedDependencies = { + manager: EntityManager + storeRepository: typeof StoreRepository + currencyRepository: typeof CurrencyRepository + eventBusService: EventBusService +} + +/** + * Provides layer to manipulate store settings. + * @extends BaseService + */ +class StoreService extends TransactionBaseService { + protected manager_: EntityManager + protected transactionManager_: EntityManager + + protected readonly storeRepository_: typeof StoreRepository + protected readonly currencyRepository_: typeof CurrencyRepository + protected readonly eventBus_: EventBusService + + constructor({ + manager, + storeRepository, + currencyRepository, + eventBusService, + }: InjectedDependencies) { + super({ + manager, + storeRepository, + currencyRepository, + eventBusService, + }) + + this.manager_ = manager + this.storeRepository_ = storeRepository + this.currencyRepository_ = currencyRepository + this.eventBus_ = eventBusService + } + + /** + * Creates a store if it doesn't already exist. + * @return The store. + */ + async create(): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const storeRepository = transactionManager.getCustomRepository( + this.storeRepository_ + ) + const currencyRepository = transactionManager.getCustomRepository( + this.currencyRepository_ + ) + + let store = await this.retrieve().catch(() => void 0) + if (store) { + return store + } + + const newStore = await storeRepository.create() + // Add default currency (USD) to store currencies + const usd = await currencyRepository.findOne({ + code: "usd", + }) + + if (usd) { + newStore.currencies = [usd] + } + + store = await storeRepository.save(newStore) + return store + } + ) + } + + /** + * Retrieve the store settings. There is always a maximum of one store. + * @param config The config object from which the query will be built + * @return the store + */ + async retrieve(config: FindConfig = {}): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const storeRepo = transactionManager.getCustomRepository( + this.storeRepository_ + ) + const query = buildQuery({}, config) + const store = await storeRepo.findOne(query) + + if (!store) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + "Store does not exist" + ) + } + + return store + } + ) + } + + protected getDefaultCurrency_(code: string): Partial { + const currencyObject = currencies[code.toUpperCase()] + + return { + code: currencyObject.code.toLowerCase(), + symbol: currencyObject.symbol, + symbol_native: currencyObject.symbol_native, + name: currencyObject.name, + } + } + + /** + * Updates a store + * @param data - an object with the update values. + * @return resolves to the update result. + */ + async update(data: UpdateStoreInput): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const storeRepository = transactionManager.getCustomRepository( + this.storeRepository_ + ) + const currencyRepository = transactionManager.getCustomRepository( + this.currencyRepository_ + ) + + const { + metadata, + default_currency_code, + currencies: storeCurrencies, + ...rest + } = data + + const store = await this.retrieve({ relations: ["currencies"] }) + + if (metadata) { + store.metadata = setMetadata(store, metadata) + } + + if (storeCurrencies) { + const defaultCurr = + default_currency_code ?? store.default_currency_code + const hasDefCurrency = storeCurrencies.find( + (c) => c.toLowerCase() === defaultCurr.toLowerCase() + ) + + // throw if we are trying to remove a currency from store currently used as default + if (!hasDefCurrency) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `You are not allowed to remove default currency from store currencies without replacing it as well` + ) + } + + store.currencies = await Promise.all( + storeCurrencies.map(async (curr) => { + const currency = await currencyRepository.findOne({ + where: { code: curr.toLowerCase() }, + }) + + if (!currency) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Currency with code ${curr} does not exist` + ) + } + + return currency + }) + ) + } + + if (default_currency_code) { + const hasDefCurrency = store.currencies.find( + (c) => c.code.toLowerCase() === default_currency_code.toLowerCase() + ) + + if (!hasDefCurrency) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Store does not have currency: ${default_currency_code}` + ) + } + + const curr = (await currencyRepository.findOne({ + code: default_currency_code.toLowerCase(), + })) as Currency + + store.default_currency = curr + store.default_currency_code = curr.code + } + + for (const [key, value] of Object.entries(rest)) { + store[key] = value + } + + return await storeRepository.save(store) + } + ) + } + + /** + * Add a currency to the store + * @param code - 3 character ISO currency code + * @return result after update + */ + async addCurrency(code: string): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const storeRepo = transactionManager.getCustomRepository( + this.storeRepository_ + ) + const currencyRepository = transactionManager.getCustomRepository( + this.currencyRepository_ + ) + + const curr = await currencyRepository.findOne({ + where: { code: code.toLowerCase() }, + }) + + if (!curr) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Currency ${code} not found` + ) + } + + const store = await this.retrieve({ relations: ["currencies"] }) + + const doesStoreInclCurrency = store.currencies + .map((c) => c.code.toLowerCase()) + .includes(curr.code.toLowerCase()) + if (doesStoreInclCurrency) { + throw new MedusaError( + MedusaError.Types.DUPLICATE_ERROR, + `Currency already added` + ) + } + + store.currencies = [...store.currencies, curr] + return await storeRepo.save(store) + } + ) + } + + /** + * Removes a currency from the store + * @param code - 3 character ISO currency code + * @return result after update + */ + async removeCurrency(code: string): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const storeRepo = transactionManager.getCustomRepository( + this.storeRepository_ + ) + const store = await this.retrieve({ relations: ["currencies"] }) + const doesCurrencyExists = store.currencies.some( + (c) => c.code === code.toLowerCase() + ) + if (!doesCurrencyExists) { + return store + } + + store.currencies = store.currencies.filter((c) => c.code !== code) + return await storeRepo.save(store) + } + ) + } +} + +export default StoreService diff --git a/packages/medusa/src/types/store.ts b/packages/medusa/src/types/store.ts new file mode 100644 index 0000000000..0103a66fa8 --- /dev/null +++ b/packages/medusa/src/types/store.ts @@ -0,0 +1,9 @@ +export type UpdateStoreInput = { + name?: string + swap_link_template?: string + payment_link_template?: string + invite_link_template?: string + default_currency_code?: string + currencies?: string[] + metadata?: Record +} diff --git a/packages/medusa/src/utils/currencies.js b/packages/medusa/src/utils/currencies.ts similarity index 98% rename from packages/medusa/src/utils/currencies.js rename to packages/medusa/src/utils/currencies.ts index bcaa487593..81d4e75205 100644 --- a/packages/medusa/src/utils/currencies.js +++ b/packages/medusa/src/utils/currencies.ts @@ -1,4 +1,14 @@ -export const currencies = { +export type Currency = { + symbol: string + name: string + symbol_native: string + decimal_digits: number + rounding: number + code: string + name_plural: string +} + +export const currencies: Record = { USD: { symbol: "$", name: "US Dollar",