diff --git a/packages/medusa/src/api/routes/admin/gift-cards/create-gift-card.ts b/packages/medusa/src/api/routes/admin/gift-cards/create-gift-card.ts index 6676b32e27..e8b16f167a 100644 --- a/packages/medusa/src/api/routes/admin/gift-cards/create-gift-card.ts +++ b/packages/medusa/src/api/routes/admin/gift-cards/create-gift-card.ts @@ -77,10 +77,9 @@ export class AdminPostGiftCardsReq { @IsBoolean() is_disabled?: boolean - @IsOptional() @IsString() - region_id?: string + region_id: string @IsOptional() - metadata?: object + metadata?: Record } diff --git a/packages/medusa/src/api/routes/admin/gift-cards/index.ts b/packages/medusa/src/api/routes/admin/gift-cards/index.ts index 53a4048a14..63619b5725 100644 --- a/packages/medusa/src/api/routes/admin/gift-cards/index.ts +++ b/packages/medusa/src/api/routes/admin/gift-cards/index.ts @@ -2,14 +2,23 @@ import { Router } from "express" import "reflect-metadata" import { GiftCard } from "../../../.." import { DeleteResponse, PaginatedResponse } from "../../../../types/common" -import middlewares from "../../../middlewares" +import middlewares, { transformQuery } from "../../../middlewares" +import { AdminGetGiftCardsParams } from "./list-gift-cards" const route = Router() export default (app) => { app.use("/gift-cards", route) - route.get("/", middlewares.wrap(require("./list-gift-cards").default)) + route.get( + "/", + transformQuery(AdminGetGiftCardsParams, { + defaultFields: defaultAdminGiftCardFields, + defaultRelations: defaultAdminGiftCardRelations, + isList: true, + }), + middlewares.wrap(require("./list-gift-cards").default) + ) route.post("/", middlewares.wrap(require("./create-gift-card").default)) @@ -22,7 +31,7 @@ export default (app) => { return app } -export const defaultAdminGiftCardFields = [ +export const defaultAdminGiftCardFields: (keyof GiftCard)[] = [ "id", "code", "value", diff --git a/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.ts b/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.ts index 45ba1a5ec2..8530be5a4f 100644 --- a/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.ts +++ b/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.ts @@ -1,5 +1,6 @@ import { Type } from "class-transformer" import { IsInt, IsOptional, IsString } from "class-validator" +import { pickBy } from "lodash" import { defaultAdminGiftCardFields, defaultAdminGiftCardRelations } from "." import { GiftCardService } from "../../../../services" import { validator } from "../../../../utils/validator" @@ -27,21 +28,12 @@ import { validator } from "../../../../utils/validator" export default async (req, res) => { const validated = await validator(AdminGetGiftCardsParams, req.query) - const selector = {} - - if (validated.q && typeof validated.q !== "undefined") { - selector["q"] = validated.q - } - const giftCardService: GiftCardService = req.scope.resolve("giftCardService") - const giftCards = await giftCardService.list(selector, { - select: defaultAdminGiftCardFields, - relations: defaultAdminGiftCardRelations, - order: { created_at: "DESC" }, - limit: validated.limit, - skip: validated.offset, - }) + const giftCards = await giftCardService.list( + pickBy(req.filterableFields, (val) => typeof val !== "undefined"), + req.listConfig + ) res.status(200).json({ gift_cards: giftCards, diff --git a/packages/medusa/src/api/routes/admin/gift-cards/update-gift-card.ts b/packages/medusa/src/api/routes/admin/gift-cards/update-gift-card.ts index f42540576a..7579943934 100644 --- a/packages/medusa/src/api/routes/admin/gift-cards/update-gift-card.ts +++ b/packages/medusa/src/api/routes/admin/gift-cards/update-gift-card.ts @@ -83,5 +83,5 @@ export class AdminPostGiftCardsGiftCardReq { region_id?: string @IsOptional() - metadata?: object + metadata?: Record } diff --git a/packages/medusa/src/api/routes/store/gift-cards/index.ts b/packages/medusa/src/api/routes/store/gift-cards/index.ts index 7f633e909e..d414565b9b 100644 --- a/packages/medusa/src/api/routes/store/gift-cards/index.ts +++ b/packages/medusa/src/api/routes/store/gift-cards/index.ts @@ -14,7 +14,12 @@ export default (app) => { export const defaultStoreGiftCardRelations = ["region"] -export const defaultStoreGiftCardFields = ["id", "code", "value", "balance"] +export const defaultStoreGiftCardFields: (keyof GiftCard)[] = [ + "id", + "code", + "value", + "balance", +] export const allowedStoreGiftCardRelations = ["region"] diff --git a/packages/medusa/src/repositories/gift-card.ts b/packages/medusa/src/repositories/gift-card.ts index f3ebcf9afa..9953a43dec 100644 --- a/packages/medusa/src/repositories/gift-card.ts +++ b/packages/medusa/src/repositories/gift-card.ts @@ -1,15 +1,20 @@ -import { flatten, groupBy, map, merge } from "lodash" -import { EntityRepository, FindManyOptions, Repository } from "typeorm" +import { flatten, groupBy, merge } from "lodash" +import { + Brackets, + EntityRepository, + FindManyOptions, + Repository, +} from "typeorm" import { GiftCard } from "../models/gift-card" +import { ExtendedFindConfig, QuerySelector, Writable } from "../types/common" @EntityRepository(GiftCard) export class GiftCardRepository extends Repository { public async findWithRelations( - relations: Array = [], - idsOrOptionsWithoutRelations: Omit< - FindManyOptions, - "relations" - > = {} + relations: (keyof GiftCard | string)[] = [], + idsOrOptionsWithoutRelations: + | Omit, "relations"> + | string[] = {} ): Promise { let entities if (Array.isArray(idsOrOptionsWithoutRelations)) { @@ -40,11 +45,47 @@ export class GiftCardRepository extends Repository { const entitiesAndRelations = entitiesIdsWithRelations.concat(entities) const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id") - return map(entitiesAndRelationsById, entityAndRelations => - merge({}, ...entityAndRelations) + return Object.values(entitiesAndRelationsById).map((v) => merge({}, ...v)) + } + + protected async queryGiftCards( + q: string, + where: Partial>>, + rels: (keyof GiftCard | string)[] + ): Promise { + const raw = await this.createQueryBuilder("gift_card") + .leftJoinAndSelect("gift_card.order", "order") + .select(["gift_card.id"]) + .where(where) + .andWhere( + new Brackets((qb) => { + return qb + .where(`gift_card.code ILIKE :q`, { q: `%${q}%` }) + .orWhere(`display_id::varchar(255) ILIKE :dId`, { dId: `${q}` }) + }) + ) + .getMany() + + return this.findWithRelations( + rels, + raw.map((i) => i.id) ) } + public async listGiftCards( + query: ExtendedFindConfig>, + rels: (keyof GiftCard | string)[] = [], + q?: string + ): Promise { + if (q) { + const where = query.where + delete where.id + + return await this.queryGiftCards(q, where, rels) + } + return this.findWithRelations(rels, query) + } + public async findOneWithRelations( relations: Array = [], optionsWithoutRelations: Omit, "relations"> = {} diff --git a/packages/medusa/src/services/__tests__/gift-card.js b/packages/medusa/src/services/__tests__/gift-card.js index 792edec5e3..e144fd4bac 100644 --- a/packages/medusa/src/services/__tests__/gift-card.js +++ b/packages/medusa/src/services/__tests__/gift-card.js @@ -5,23 +5,23 @@ import GiftCardService from "../gift-card" describe("GiftCardService", () => { const eventBusService = { emit: jest.fn(), - withTransaction: function() { + withTransaction: function () { return this }, } describe("create", () => { const giftCardRepo = MockRepository({ - create: s => { + create: (s) => { return Promise.resolve(s) }, - save: s => { + save: (s) => { return Promise.resolve(s) }, }) const regionService = { - withTransaction: function() { + withTransaction: function () { return this }, retrieve: () => { @@ -59,18 +59,6 @@ describe("GiftCardService", () => { code: expect.any(String), }) }) - - it("fails to create giftcard if no region is provided", async () => { - const card = { - ...giftCard, - } - - card.region_id = undefined - - await expect(giftCardService.create(card)).rejects.toThrow( - "Gift card is missing region_id" - ) - }) }) describe("retrieve", () => { @@ -152,16 +140,16 @@ describe("GiftCardService", () => { } const giftCardRepo = MockRepository({ - findOneWithRelations: s => { + findOneWithRelations: (s) => { return Promise.resolve(giftCard) }, - save: s => { + save: (s) => { return Promise.resolve(s) }, }) const regionService = { - withTransaction: function() { + withTransaction: function () { return this }, retrieve: () => { @@ -198,7 +186,7 @@ describe("GiftCardService", () => { it.each([[-100], [6000]])( "fails to update balance with illegal input '%s'", - async input => { + async (input) => { await expect( giftCardService.update(IdMap.getId("giftcard-id"), { balance: input, @@ -215,7 +203,7 @@ describe("GiftCardService", () => { } const giftCardRepo = MockRepository({ - findOne: s => { + findOne: (s) => { switch (s.where.id) { case IdMap.getId("gift-card"): return Promise.resolve(giftCard) @@ -223,7 +211,7 @@ describe("GiftCardService", () => { return Promise.resolve() } }, - softRemove: s => { + softRemove: (s) => { return Promise.resolve() }, }) diff --git a/packages/medusa/src/services/gift-card.js b/packages/medusa/src/services/gift-card.js deleted file mode 100644 index ab2da18541..0000000000 --- a/packages/medusa/src/services/gift-card.js +++ /dev/null @@ -1,305 +0,0 @@ -import { MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" -import randomize from "randomatic" -import { Brackets } from "typeorm" - -/** - * Provides layer to manipulate gift cards. - * @extends BaseService - */ -class GiftCardService extends BaseService { - static Events = { - CREATED: "gift_card.created", - } - - constructor({ - manager, - giftCardRepository, - giftCardTransactionRepository, - regionService, - eventBusService, - }) { - super() - - /** @private @const {EntityManager} */ - this.manager_ = manager - - /** @private @const {GiftCardRepository} */ - this.giftCardRepository_ = giftCardRepository - - /** @private @const {GiftCardRepository} */ - this.giftCardTransactionRepo_ = giftCardTransactionRepository - - /** @private @const {RegionService} */ - this.regionService_ = regionService - - /** @private @const {EventBus} */ - this.eventBus_ = eventBusService - } - - withTransaction(transactionManager) { - if (!transactionManager) { - return this - } - - const cloned = new GiftCardService({ - manager: transactionManager, - giftCardRepository: this.giftCardRepository_, - giftCardTransactionRepository: this.giftCardTransactionRepo_, - regionService: this.regionService_, - eventBusService: this.eventBus_, - }) - - cloned.transactionManager_ = transactionManager - - return cloned - } - - /** - * Generates a 16 character gift card code - * @return {string} the generated gift card code - */ - generateCode_() { - const code = [ - randomize("A0", 4), - randomize("A0", 4), - randomize("A0", 4), - randomize("A0", 4), - ].join("-") - - return code - } - - /** - * @param {Object} selector - the query object for find - * @param {Object} config - the configuration used to find the objects. contains relations, skip, and take. - * @return {Promise} the result of the find operation - */ - async list(selector = {}, config = { relations: [], skip: 0, take: 10 }) { - const giftCardRepo = this.manager_.getCustomRepository( - this.giftCardRepository_ - ) - - let q - if ("q" in selector) { - q = selector.q - delete selector.q - } - - const query = this.buildQuery_(selector, config) - - const rels = query.relations - delete query.relations - - if (q) { - const where = query.where - delete where.id - - const raw = await giftCardRepo - .createQueryBuilder("gift_card") - .leftJoinAndSelect("gift_card.order", "order") - .select(["gift_card.id"]) - .where(where) - .andWhere( - new Brackets((qb) => { - return qb - .where(`gift_card.code ILIKE :q`, { q: `%${q}%` }) - .orWhere(`display_id::varchar(255) ILIKE :dId`, { dId: `${q}` }) - }) - ) - .getMany() - - return giftCardRepo.findWithRelations( - rels, - raw.map((i) => i.id) - ) - } - return giftCardRepo.findWithRelations(rels, query) - } - - async createTransaction(data) { - return this.atomicPhase_(async (manager) => { - const gctRepo = manager.getCustomRepository(this.giftCardTransactionRepo_) - const created = gctRepo.create(data) - const saved = await gctRepo.save(created) - return saved.id - }) - } - - /** - * Creates a gift card with provided data given that the data is validated. - * @param {GiftCard} giftCard - the gift card data to create - * @return {Promise} the result of the create operation - */ - async create(giftCard) { - return this.atomicPhase_(async (manager) => { - const giftCardRepo = manager.getCustomRepository(this.giftCardRepository_) - - if (!giftCard.region_id) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Gift card is missing region_id` - ) - } - - // Will throw if region does not exist - const region = await this.regionService_.retrieve(giftCard.region_id) - - const code = this.generateCode_() - - const toCreate = { - code, - region_id: region.id, - ...giftCard, - } - - const created = await giftCardRepo.create(toCreate) - const result = await giftCardRepo.save(created) - - await this.eventBus_ - .withTransaction(manager) - .emit(GiftCardService.Events.CREATED, { - id: result.id, - }) - - return result - }) - } - - /** - * Gets a gift card by id. - * @param {string} giftCardId - id of gift card to retrieve - * @param {object} config - optional values to include with gift card query - * @return {Promise} the gift card - */ - async retrieve(giftCardId, config = {}) { - const giftCardRepo = this.manager_.getCustomRepository( - this.giftCardRepository_ - ) - - const validatedId = this.validateId_(giftCardId) - - const query = { - where: { id: validatedId }, - } - - if (config.select) { - query.select = config.select - } - - if (config.relations) { - query.relations = config.relations - } - - const rels = query.relations - delete query.relations - - const giftCard = await giftCardRepo.findOneWithRelations(rels, query) - - if (!giftCard) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Gift card with ${giftCardId} was not found` - ) - } - - return giftCard - } - - async retrieveByCode(code, config = {}) { - const giftCardRepo = this.manager_.getCustomRepository( - this.giftCardRepository_ - ) - - const query = { - where: { code }, - } - - if (config.select) { - query.select = config.select - } - - if (config.relations) { - query.relations = config.relations - } - - const rels = query.relations - delete query.relations - - const giftCard = await giftCardRepo.findOneWithRelations(rels, query) - - if (!giftCard) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Gift card with ${code} was not found` - ) - } - - return giftCard - } - - /** - * Updates a giftCard. - * @param {string} giftCardId - giftCard id of giftCard to update - * @param {GiftCard} update - the data to update the giftCard with - * @return {Promise} the result of the update operation - */ - async update(giftCardId, update) { - return this.atomicPhase_(async (manager) => { - const giftCardRepo = manager.getCustomRepository(this.giftCardRepository_) - - const giftCard = await this.retrieve(giftCardId) - - const { region_id, metadata, balance, ...rest } = update - - if (region_id && region_id !== giftCard.region_id) { - const region = await this.regionService_.retrieve(region_id) - giftCard.region_id = region.id - } - - if (metadata) { - giftCard.metadata = await this.setMetadata_(giftCard.id, metadata) - } - - if (typeof balance !== "undefined") { - if (balance < 0 || giftCard.value < balance) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "new balance is invalid" - ) - } - giftCard.balance = balance - } - - for (const [key, value] of Object.entries(rest)) { - giftCard[key] = value - } - - const updated = await giftCardRepo.save(giftCard) - return updated - }) - } - - /** - * Deletes a gift card idempotently - * @param {string} giftCardId - id of gift card to delete - * @return {Promise} the result of the delete operation - */ - async delete(giftCardId) { - return this.atomicPhase_(async (manager) => { - const giftCardRepo = manager.getCustomRepository(this.giftCardRepository_) - - const giftCard = await giftCardRepo.findOne({ where: { id: giftCardId } }) - - if (!giftCard) { - return Promise.resolve() - } - - await giftCardRepo.softRemove(giftCard) - - return Promise.resolve() - }) - } -} - -export default GiftCardService diff --git a/packages/medusa/src/services/gift-card.ts b/packages/medusa/src/services/gift-card.ts new file mode 100644 index 0000000000..542bf7deb9 --- /dev/null +++ b/packages/medusa/src/services/gift-card.ts @@ -0,0 +1,273 @@ +import { MedusaError } from "medusa-core-utils" +import randomize from "randomatic" +import { Brackets, EntityManager, FindOneOptions } from "typeorm" +import { EventBusService } from "." +import { TransactionBaseService } from "../interfaces" +import { GiftCard } from "../models" +import { GiftCardRepository } from "../repositories/gift-card" +import { GiftCardTransactionRepository } from "../repositories/gift-card-transaction" +import { + ExtendedFindConfig, + FindConfig, + QuerySelector, + Selector, +} from "../types/common" +import { + CreateGiftCardInput, + CreateGiftCardTransactionInput, + UpdateGiftCardInput, +} from "../types/gift-card" +import { buildQuery, setMetadata } from "../utils" +import RegionService from "./region" + +type InjectedDependencies = { + manager: EntityManager + giftCardRepository: typeof GiftCardRepository + giftCardTransactionRepository: typeof GiftCardTransactionRepository + regionService: RegionService + eventBusService: EventBusService +} +/** + * Provides layer to manipulate gift cards. + */ +class GiftCardService extends TransactionBaseService { + protected readonly giftCardRepository_: typeof GiftCardRepository + protected readonly giftCardTransactionRepo_: typeof GiftCardTransactionRepository + protected readonly regionService_: RegionService + protected readonly eventBus_: EventBusService + + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + static Events = { + CREATED: "gift_card.created", + } + + constructor({ + manager, + giftCardRepository, + giftCardTransactionRepository, + regionService, + eventBusService, + }: InjectedDependencies) { + // eslint-disable-next-line prefer-rest-params + super(arguments[0]) + + this.manager_ = manager + + this.giftCardRepository_ = giftCardRepository + this.giftCardTransactionRepo_ = giftCardTransactionRepository + this.regionService_ = regionService + this.eventBus_ = eventBusService + } + + /** + * Generates a 16 character gift card code + * @return the generated gift card code + */ + static generateCode(): string { + const code = [ + randomize("A0", 4), + randomize("A0", 4), + randomize("A0", 4), + randomize("A0", 4), + ].join("-") + + return code + } + + /** + * @param selector - the query object for find + * @param config - the configuration used to find the objects. contains relations, skip, and take. + * @return the result of the find operation + */ + async list( + selector: QuerySelector = {}, + config: FindConfig = { relations: [], skip: 0, take: 10 } + ): Promise { + return await this.atomicPhase_(async (manager) => { + const giftCardRepo = manager.getCustomRepository(this.giftCardRepository_) + + let q + if ("q" in selector) { + q = selector.q + delete selector.q + } + + const query: ExtendedFindConfig< + GiftCard, + QuerySelector + > = buildQuery, GiftCard>(selector, config) + + const rels = query.relations + delete query.relations + + return await giftCardRepo.listGiftCards(query, rels, q) + }) + } + + async createTransaction( + data: CreateGiftCardTransactionInput + ): Promise { + return await this.atomicPhase_(async (manager) => { + const gctRepo = manager.getCustomRepository(this.giftCardTransactionRepo_) + const created = gctRepo.create(data) + const saved = await gctRepo.save(created) + return saved.id + }) + } + + /** + * Creates a gift card with provided data given that the data is validated. + * @param giftCard - the gift card data to create + * @return the result of the create operation + */ + async create(giftCard: CreateGiftCardInput): Promise { + return await this.atomicPhase_(async (manager) => { + const giftCardRepo = manager.getCustomRepository(this.giftCardRepository_) + + // Will throw if region does not exist + const region = await this.regionService_ + .withTransaction(manager) + .retrieve(giftCard.region_id) + + const code = GiftCardService.generateCode() + + const toCreate = { + code, + ...giftCard, + region_id: region.id, + } + + const created = giftCardRepo.create(toCreate) + const result = await giftCardRepo.save(created) + + await this.eventBus_ + .withTransaction(manager) + .emit(GiftCardService.Events.CREATED, { + id: result.id, + }) + + return result + }) + } + + protected async retrieve_( + selector: Selector, + config: FindConfig = {} + ): Promise { + return await this.atomicPhase_(async (manager) => { + const giftCardRepo = manager.getCustomRepository(this.giftCardRepository_) + + const { relations, ...query } = buildQuery(selector, config) + + const giftCard = await giftCardRepo.findOneWithRelations( + relations as (keyof GiftCard)[], + query + ) + + if (!giftCard) { + const selectorConstraints = Object.entries(selector) + .map((key, value) => `${key}: ${value}`) + .join(", ") + + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Gift card with ${selectorConstraints} was not found` + ) + } + + return giftCard + }) + } + + /** + * Gets a gift card by id. + * @param giftCardId - id of gift card to retrieve + * @param config - optional values to include with gift card query + * @return the gift card + */ + async retrieve( + giftCardId: string, + config: FindConfig = {} + ): Promise { + return await this.atomicPhase_(async () => { + return await this.retrieve_({ id: giftCardId }, config) + }) + } + + async retrieveByCode( + code: string, + config: FindConfig = {} + ): Promise { + return await this.atomicPhase_(async () => { + return await this.retrieve_({ code }, config) + }) + } + + /** + * Updates a giftCard. + * @param giftCardId - giftCard id of giftCard to update + * @param update - the data to update the giftCard with + * @return the result of the update operation + */ + async update( + giftCardId: string, + update: UpdateGiftCardInput + ): Promise { + return await this.atomicPhase_(async (manager) => { + const giftCardRepo = manager.getCustomRepository(this.giftCardRepository_) + + const giftCard = await this.retrieve(giftCardId) + + const { region_id, metadata, balance, ...rest } = update + + if (region_id && region_id !== giftCard.region_id) { + const region = await this.regionService_.retrieve(region_id) + giftCard.region_id = region.id + } + + if (metadata) { + giftCard.metadata = setMetadata(giftCard, metadata) + } + + if (typeof balance !== "undefined") { + if (balance < 0 || giftCard.value < balance) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "new balance is invalid" + ) + } + + giftCard.balance = balance + } + + for (const [key, value] of Object.entries(rest)) { + giftCard[key] = value + } + + return await giftCardRepo.save(giftCard) + }) + } + + /** + * Deletes a gift card idempotently + * @param giftCardId - id of gift card to delete + * @return the result of the delete operation + */ + async delete(giftCardId: string): Promise { + return await this.atomicPhase_(async (manager) => { + const giftCardRepo = manager.getCustomRepository(this.giftCardRepository_) + + const giftCard = await giftCardRepo.findOne({ where: { id: giftCardId } }) + + if (!giftCard) { + return + } + + return await giftCardRepo.softRemove(giftCard) + }) + } +} + +export default GiftCardService diff --git a/packages/medusa/src/types/common.ts b/packages/medusa/src/types/common.ts index e3b96f5adc..040105e02e 100644 --- a/packages/medusa/src/types/common.ts +++ b/packages/medusa/src/types/common.ts @@ -46,6 +46,8 @@ export type ExtendedFindConfig< relations?: string[] } +export type QuerySelector = Selector & { q?: string } + export type Selector = { [key in keyof TEntity]?: | TEntity[key] diff --git a/packages/medusa/src/types/gift-card.ts b/packages/medusa/src/types/gift-card.ts new file mode 100644 index 0000000000..017cb78b78 --- /dev/null +++ b/packages/medusa/src/types/gift-card.ts @@ -0,0 +1,23 @@ +export type CreateGiftCardInput = { + value?: number + balance?: number + ends_at?: Date + is_disabled?: boolean + region_id: string + metadata?: Record +} + +export type UpdateGiftCardInput = { + balance?: number + ends_at?: Date + is_disabled?: boolean + region_id?: string + metadata?: Record +} + +export type CreateGiftCardTransactionInput = { + gift_card_id: string + order_id: string + amount: number + created_at: Date +}