diff --git a/packages/medusa/src/api/routes/admin/gift-cards/index.js b/packages/medusa/src/api/routes/admin/gift-cards/index.js index ea3a04f5ee..bb15df5aac 100644 --- a/packages/medusa/src/api/routes/admin/gift-cards/index.js +++ b/packages/medusa/src/api/routes/admin/gift-cards/index.js @@ -33,7 +33,10 @@ export const defaultFields = [ "metadata", ] -export const defaultRelations = ["region"] +export const defaultRelations = [ + "region", + "order", +] export const allowedFields = [ "id", diff --git a/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.js b/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.js index 10791e6235..28dba7d592 100644 --- a/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.js +++ b/packages/medusa/src/api/routes/admin/gift-cards/list-gift-cards.js @@ -1,3 +1,4 @@ +import { MedusaError, Validator } from "medusa-core-utils" import { defaultFields, defaultRelations } from "./" /** @@ -21,14 +22,23 @@ import { defaultFields, defaultRelations } from "./" */ export default async (req, res) => { try { + const limit = parseInt(req.query.limit) || 50 + const offset = parseInt(req.query.offset) || 0 + const selector = {} + if ("q" in req.query) { + selector.q = req.query.q + } + const giftCardService = req.scope.resolve("giftCardService") const giftCards = await giftCardService.list(selector, { select: defaultFields, relations: defaultRelations, order: { created_at: "DESC" }, + limit: limit, + skip: offset, }) res.status(200).json({ gift_cards: giftCards }) diff --git a/packages/medusa/src/api/routes/admin/gift-cards/update-gift-card.js b/packages/medusa/src/api/routes/admin/gift-cards/update-gift-card.js index 6c90578dd2..eb28ec062d 100644 --- a/packages/medusa/src/api/routes/admin/gift-cards/update-gift-card.js +++ b/packages/medusa/src/api/routes/admin/gift-cards/update-gift-card.js @@ -1,4 +1,5 @@ import { MedusaError, Validator } from "medusa-core-utils" +import { defaultFields, defaultRelations } from "./" /** * @oas [post] /gift-cards/{id} diff --git a/packages/medusa/src/repositories/gift-card.ts b/packages/medusa/src/repositories/gift-card.ts index 578dc8ed58..f3ebcf9afa 100644 --- a/packages/medusa/src/repositories/gift-card.ts +++ b/packages/medusa/src/repositories/gift-card.ts @@ -1,5 +1,61 @@ -import { EntityRepository, Repository } from "typeorm" +import { flatten, groupBy, map, merge } from "lodash" +import { EntityRepository, FindManyOptions, Repository } from "typeorm" import { GiftCard } from "../models/gift-card" @EntityRepository(GiftCard) -export class GiftCardRepository extends Repository {} +export class GiftCardRepository extends Repository { + public async findWithRelations( + relations: Array = [], + idsOrOptionsWithoutRelations: Omit< + FindManyOptions, + "relations" + > = {} + ): Promise { + let entities + if (Array.isArray(idsOrOptionsWithoutRelations)) { + entities = await this.findByIds(idsOrOptionsWithoutRelations) + } else { + entities = await this.find(idsOrOptionsWithoutRelations) + } + const entitiesIds = entities.map(({ id }) => id) + + const groupedRelations = {} + for (const rel of relations) { + const [topLevel] = rel.split(".") + if (groupedRelations[topLevel]) { + groupedRelations[topLevel].push(rel) + } else { + groupedRelations[topLevel] = [rel] + } + } + + const entitiesIdsWithRelations = await Promise.all( + Object.entries(groupedRelations).map(([_, rels]) => { + return this.findByIds(entitiesIds, { + select: ["id"], + relations: rels as string[], + }) + }) + ).then(flatten) + const entitiesAndRelations = entitiesIdsWithRelations.concat(entities) + + const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id") + return map(entitiesAndRelationsById, entityAndRelations => + merge({}, ...entityAndRelations) + ) + } + + public async findOneWithRelations( + relations: Array = [], + optionsWithoutRelations: Omit, "relations"> = {} + ): Promise { + // Limit 1 + optionsWithoutRelations.take = 1 + + const result = await this.findWithRelations( + relations, + optionsWithoutRelations + ) + return result[0] + } +} diff --git a/packages/medusa/src/services/__tests__/gift-card.js b/packages/medusa/src/services/__tests__/gift-card.js new file mode 100644 index 0000000000..792edec5e3 --- /dev/null +++ b/packages/medusa/src/services/__tests__/gift-card.js @@ -0,0 +1,256 @@ +import { IdMap, MockManager, MockRepository } from "medusa-test-utils" + +import GiftCardService from "../gift-card" + +describe("GiftCardService", () => { + const eventBusService = { + emit: jest.fn(), + withTransaction: function() { + return this + }, + } + + describe("create", () => { + const giftCardRepo = MockRepository({ + create: s => { + return Promise.resolve(s) + }, + save: s => { + return Promise.resolve(s) + }, + }) + + const regionService = { + withTransaction: function() { + return this + }, + retrieve: () => { + return Promise.resolve({ + id: IdMap.getId("region-id"), + }) + }, + } + + const giftCardService = new GiftCardService({ + manager: MockManager, + giftCardRepository: giftCardRepo, + regionService: regionService, + eventBusService: eventBusService, + }) + + const giftCard = { + region_id: IdMap.getId("region-id"), + order_id: IdMap.getId("order-id"), + is_disabled: true, + } + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("correctly creates a giftcard", async () => { + await giftCardService.create(giftCard) + + expect(giftCardRepo.create).toHaveBeenCalledTimes(1) + expect(giftCardRepo.create).toHaveBeenCalledWith({ + region_id: IdMap.getId("region-id"), + order_id: IdMap.getId("order-id"), + is_disabled: true, + 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", () => { + const giftCardRepo = MockRepository({ + findOneWithRelations: () => { + return Promise.resolve({}) + }, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + const giftCardService = new GiftCardService({ + manager: MockManager, + giftCardRepository: giftCardRepo, + }) + + it("it calls order model functions", async () => { + await giftCardService.retrieve(IdMap.getId("gift-card"), { + relations: ["region"], + select: ["id"], + }) + + expect(giftCardRepo.findOneWithRelations).toHaveBeenCalledTimes(1) + expect(giftCardRepo.findOneWithRelations).toHaveBeenCalledWith( + ["region"], + { + where: { + id: IdMap.getId("gift-card"), + }, + select: ["id"], + } + ) + }) + }) + + describe("retrieveByCode", () => { + const giftCardRepo = MockRepository({ + findOneWithRelations: () => { + return Promise.resolve({}) + }, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + const giftCardService = new GiftCardService({ + manager: MockManager, + giftCardRepository: giftCardRepo, + }) + + it("it calls order model functions", async () => { + await giftCardService.retrieveByCode("1234-1234-1234-1234", { + relations: ["region"], + select: ["id"], + }) + + expect(giftCardRepo.findOneWithRelations).toHaveBeenCalledTimes(1) + expect(giftCardRepo.findOneWithRelations).toHaveBeenCalledWith( + ["region"], + { + where: { + code: "1234-1234-1234-1234", + }, + select: ["id"], + } + ) + }) + }) + + describe("update", () => { + const giftCard = { + region_id: IdMap.getId("region-id"), + order_id: IdMap.getId("order-id"), + is_disabled: true, + value: 5000, + } + + const giftCardRepo = MockRepository({ + findOneWithRelations: s => { + return Promise.resolve(giftCard) + }, + save: s => { + return Promise.resolve(s) + }, + }) + + const regionService = { + withTransaction: function() { + return this + }, + retrieve: () => { + return Promise.resolve({ + id: IdMap.getId("other-region"), + }) + }, + } + + const giftCardService = new GiftCardService({ + manager: MockManager, + giftCardRepository: giftCardRepo, + regionService: regionService, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("calls order model functions", async () => { + await giftCardService.update(IdMap.getId("giftcard-id"), { + is_disabled: false, + region_id: IdMap.getId("other-region"), + }) + + expect(giftCardRepo.save).toHaveBeenCalledTimes(1) + expect(giftCardRepo.save).toHaveBeenCalledWith({ + region_id: IdMap.getId("other-region"), + order_id: IdMap.getId("order-id"), + is_disabled: false, + value: 5000, + }) + }) + + it.each([[-100], [6000]])( + "fails to update balance with illegal input '%s'", + async input => { + await expect( + giftCardService.update(IdMap.getId("giftcard-id"), { + balance: input, + }) + ).rejects.toThrow("new balance is invalid") + } + ) + }) + + describe("delete", () => { + const giftCard = { + region_id: IdMap.getId("region-id"), + order_id: IdMap.getId("order-id"), + } + + const giftCardRepo = MockRepository({ + findOne: s => { + switch (s.where.id) { + case IdMap.getId("gift-card"): + return Promise.resolve(giftCard) + default: + return Promise.resolve() + } + }, + softRemove: s => { + return Promise.resolve() + }, + }) + + const giftCardService = new GiftCardService({ + manager: MockManager, + giftCardRepository: giftCardRepo, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("successfully deletes existing gift-card", async () => { + await giftCardService.delete(IdMap.getId("gift-card")) + + expect(giftCardRepo.softRemove).toHaveBeenCalledTimes(1) + expect(giftCardRepo.softRemove).toHaveBeenCalledWith({ + region_id: IdMap.getId("region-id"), + order_id: IdMap.getId("order-id"), + }) + }) + + it("returns if no gift-card found", async () => { + await giftCardService.delete(IdMap.getId("other")) + + expect(giftCardRepo.softRemove).toHaveBeenCalledTimes(0) + }) + }) +}) diff --git a/packages/medusa/src/services/gift-card.js b/packages/medusa/src/services/gift-card.js index ecd62cf045..173a3d1aee 100644 --- a/packages/medusa/src/services/gift-card.js +++ b/packages/medusa/src/services/gift-card.js @@ -1,7 +1,8 @@ import _ from "lodash" import randomize from "randomatic" import { BaseService } from "medusa-interfaces" -import { Validator, MedusaError } from "medusa-core-utils" +import { Brackets } from "typeorm" +import { MedusaError } from "medusa-core-utils" /** * Provides layer to manipulate gift cards. @@ -72,6 +73,7 @@ class GiftCardService extends BaseService { /** * @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 }) { @@ -79,8 +81,41 @@ class GiftCardService extends BaseService { this.giftCardRepository_ ) + let q + if ("q" in selector) { + q = selector.q + delete selector.q + } + const query = this.buildQuery_(selector, config) - return giftCardRepo.find(query) + + 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) { @@ -156,7 +191,10 @@ class GiftCardService extends BaseService { query.relations = config.relations } - const giftCard = await giftCardRepo.findOne(query) + const rels = query.relations + delete query.relations + + const giftCard = await giftCardRepo.findOneWithRelations(rels, query) if (!giftCard) { throw new MedusaError( @@ -185,7 +223,10 @@ class GiftCardService extends BaseService { query.relations = config.relations } - const giftCard = await giftCardRepo.findOne(query) + const rels = query.relations + delete query.relations + + const giftCard = await giftCardRepo.findOneWithRelations(rels, query) if (!giftCard) { throw new MedusaError( @@ -209,7 +250,7 @@ class GiftCardService extends BaseService { const giftCard = await this.retrieve(giftCardId) - const { region_id, metadata, ...rest } = update + const { region_id, metadata, balance, ...rest } = update if (region_id && region_id !== giftCard.region_id) { const region = await this.regionService_.retrieve(region_id) @@ -220,6 +261,16 @@ class GiftCardService extends BaseService { giftCard.metadata = await this.setMetadata_(giftCard.id, metadata) } + if (balance) { + 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 }