refactor(medusa): Migrate StoreService to TS (#1413)

This commit is contained in:
Adrien de Peretti
2022-06-09 20:46:50 +02:00
committed by GitHub
parent 5172b21d09
commit a605622bcb
10 changed files with 333 additions and 289 deletions

View File

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

View File

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

View File

@@ -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<string, unknown>
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<StoreService> {
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<Store> {
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<Store> = {}): Promise<Store> {
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<Currency> {
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<Store> {
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<Store | never> {
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<any> {
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

View File

@@ -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<string, unknown>
}

View File

@@ -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<string, Currency> = {
USD: {
symbol: "$",
name: "US Dollar",