Feat/improve overview of gift cards (#297)

* modified list-gift-card

* added search with relations to gift cards to allow for proper lookup

* removed new line

* created skeleton for testing

* began impl. tests

* added more test

* finished tests

* reverted failed changes

* updated tests to accomodate re-introduced additions

* removed VSCode settings file

* updated according to comments

* corrected use of findOneWithRelations for gift-card

* corrected parsing to be postgresql instead of an if-statement for gift-card

* changed bits and pieces
This commit is contained in:
Sebastian Mateos Nicolajsen
2021-07-08 09:59:59 +02:00
committed by GitHub
parent 0c96813e2f
commit 92caff845e
6 changed files with 385 additions and 8 deletions

View File

@@ -33,7 +33,10 @@ export const defaultFields = [
"metadata",
]
export const defaultRelations = ["region"]
export const defaultRelations = [
"region",
"order",
]
export const allowedFields = [
"id",

View File

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

View File

@@ -1,4 +1,5 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultFields, defaultRelations } from "./"
/**
* @oas [post] /gift-cards/{id}

View File

@@ -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<GiftCard> {}
export class GiftCardRepository extends Repository<GiftCard> {
public async findWithRelations(
relations: Array<keyof GiftCard> = [],
idsOrOptionsWithoutRelations: Omit<
FindManyOptions<GiftCard>,
"relations"
> = {}
): Promise<GiftCard[]> {
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<keyof GiftCard> = [],
optionsWithoutRelations: Omit<FindManyOptions<GiftCard>, "relations"> = {}
): Promise<GiftCard> {
// Limit 1
optionsWithoutRelations.take = 1
const result = await this.findWithRelations(
relations,
optionsWithoutRelations
)
return result[0]
}
}

View File

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

View File

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