From 1a6c4dc73d5b6f0635b420d71e6c5a39e13df846 Mon Sep 17 00:00:00 2001 From: Buyantogtokh Date: Mon, 9 Dec 2019 13:31:19 +0800 Subject: [PATCH 1/4] move product schema declaration from deals into boards --- src/db/models/definitions/boards.ts | 30 ++++++++++++++ src/db/models/definitions/deals.ts | 62 +++++++++-------------------- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/src/db/models/definitions/boards.ts b/src/db/models/definitions/boards.ts index b459bbcd2..1ecb5ab42 100644 --- a/src/db/models/definitions/boards.ts +++ b/src/db/models/definitions/boards.ts @@ -32,6 +32,19 @@ export interface IItemCommonFields { priority?: string; } +export interface IProductData extends Document { + productId: string; + uom: string; + currency: string; + quantity: number; + unitPrice: number; + taxPercent?: number; + tax?: number; + discountPercent?: number; + discount?: number; + amount?: number; +} + export interface IItemCommonFieldsDocument extends IItemCommonFields, Document { _id: string; } @@ -84,6 +97,23 @@ export interface IOrderInput { order: number; } +export const productDataSchema = new Schema( + { + _id: field({ type: String }), + productId: field({ type: String, label: 'Product' }), + uom: field({ type: String, label: 'Unit of measurement' }), + currency: field({ type: String, label: 'Currency' }), + quantity: field({ type: Number, label: 'Quantity' }), + unitPrice: field({ type: Number, label: 'Unit price' }), + taxPercent: field({ type: Number, label: 'Tax percent' }), + tax: field({ type: Number, label: 'Tax' }), + discountPercent: field({ type: Number, label: 'Discount percent' }), + discount: field({ type: Number, label: 'Discount' }), + amount: field({ type: Number, label: 'Amount' }), + }, + { _id: false }, +); + const attachmentSchema = new Schema( { name: field({ type: String, label: 'Name' }), diff --git a/src/db/models/definitions/deals.ts b/src/db/models/definitions/deals.ts index 54f36bcfc..8154e64a5 100644 --- a/src/db/models/definitions/deals.ts +++ b/src/db/models/definitions/deals.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { commonItemFieldsSchema, IItemCommonFields } from './boards'; +import { commonItemFieldsSchema, IItemCommonFields, IProductData, productDataSchema } from './boards'; import { PRODUCT_TYPES } from './constants'; import { field, schemaWrapper } from './utils'; @@ -35,19 +35,6 @@ export interface IProductCategoryDocument extends IProductCategory, Document { createdAt: Date; } -interface IProductData extends Document { - productId: string; - uom: string; - currency: string; - quantity: number; - unitPrice: number; - taxPercent?: number; - tax?: number; - discountPercent?: number; - discount?: number; - amount?: number; -} - export interface IDeal extends IItemCommonFields { productsData?: IProductData[]; } @@ -61,24 +48,27 @@ export interface IDealDocument extends IDeal, Document { export const productSchema = schemaWrapper( new Schema({ _id: field({ pkey: true }), - name: field({ type: String }), - code: field({ type: String, unique: true }), - categoryId: field({ type: String }), + name: field({ type: String, label: 'Name' }), + code: field({ type: String, unique: true, label: 'Code' }), + categoryId: field({ type: String, label: 'Category' }), type: field({ type: String, enum: PRODUCT_TYPES.ALL, default: PRODUCT_TYPES.PRODUCT, + label: 'Type', }), - tagIds: field({ type: [String], optional: true }), - description: field({ type: String, optional: true }), - sku: field({ type: String, optional: true }), // Stock Keeping Unit - unitPrice: field({ type: Number, optional: true }), + tagIds: field({ type: [String], optional: true, label: 'Tags' }), + description: field({ type: String, optional: true, label: 'Description' }), + sku: field({ type: String, optional: true, label: 'Stock keeping unit' }), + unitPrice: field({ type: Number, optional: true, label: 'Unit price' }), customFieldsData: field({ type: Object, + label: 'Custom fields data', }), createdAt: field({ type: Date, default: new Date(), + label: 'Created at', }), }), ); @@ -86,37 +76,21 @@ export const productSchema = schemaWrapper( export const productCategorySchema = schemaWrapper( new Schema({ _id: field({ pkey: true }), - name: field({ type: String }), - code: field({ type: String, unique: true }), - order: field({ type: String }), - parentId: field({ type: String, optional: true }), - description: field({ type: String, optional: true }), + name: field({ type: String, label: 'Name' }), + code: field({ type: String, unique: true, label: 'Code' }), + order: field({ type: String, label: 'Order' }), + parentId: field({ type: String, optional: true, label: 'Parent category' }), + description: field({ type: String, optional: true, label: 'Description' }), createdAt: field({ type: Date, default: new Date(), + label: 'Created at', }), }), ); -const productDataSchema = new Schema( - { - _id: field({ type: String }), - productId: field({ type: String }), - uom: field({ type: String }), // Units of measurement - currency: field({ type: String }), - quantity: field({ type: Number }), - unitPrice: field({ type: Number }), - taxPercent: field({ type: Number }), - tax: field({ type: Number }), - discountPercent: field({ type: Number }), - discount: field({ type: Number }), - amount: field({ type: Number }), - }, - { _id: false }, -); - export const dealSchema = new Schema({ ...commonItemFieldsSchema, - productsData: field({ type: [productDataSchema] }), + productsData: field({ type: [productDataSchema], label: 'Products' }), }); From 4e195f8c2f4900744c4bb60c2a9ffc8571ae04a3 Mon Sep 17 00:00:00 2001 From: Buyantogtokh Date: Tue, 10 Dec 2019 18:07:32 +0800 Subject: [PATCH 2/4] enable adding of products to tickets --- src/data/resolvers/tickets.ts | 59 ++++++++++++++++++++++++++++ src/data/schema/common.ts | 10 +++-- src/data/schema/ticket.ts | 9 ++++- src/db/factories.ts | 20 ++++++++-- src/db/models/definitions/boards.ts | 2 + src/db/models/definitions/tickets.ts | 4 +- 6 files changed, 95 insertions(+), 9 deletions(-) diff --git a/src/data/resolvers/tickets.ts b/src/data/resolvers/tickets.ts index 78c457c9b..cbb4fb7bc 100644 --- a/src/data/resolvers/tickets.ts +++ b/src/data/resolvers/tickets.ts @@ -2,9 +2,11 @@ import { Companies, Conformities, Customers, + Fields, Notifications, PipelineLabels, Pipelines, + Products, Stages, Users, } from '../../db/models'; @@ -68,4 +70,61 @@ export default { labels(ticket: ITicketDocument) { return PipelineLabels.find({ _id: { $in: ticket.labelIds || [] } }); }, + + async products(ticket: ITicketDocument) { + const products: any = []; + + for (const data of ticket.productsData || []) { + const product = await Products.getProduct({ _id: data.productId }); + + const { customFieldsData } = product; + + if (customFieldsData) { + const customFields = {}; + const fieldIds: string[] = []; + + Object.keys(customFieldsData).forEach(_id => { + fieldIds.push(_id); + }); + + const fields = await Fields.find({ _id: { $in: fieldIds }, contentType: 'product' }); + + for (const field of fields) { + customFields[field._id] = { + text: field.text, + data: customFieldsData[field._id], + }; + } + + product.customFieldsData = customFields; + } + + // Add product object to resulting list + products.push({ + ...data.toJSON(), + product: product.toJSON(), + }); + } + + return products; + }, + + amount(ticket: ITicketDocument) { + const data = ticket.productsData; + const amountsMap = {}; + + (data || []).forEach(product => { + const type = product.currency; + + if (type) { + if (!amountsMap[type]) { + amountsMap[type] = 0; + } + + amountsMap[type] += product.amount || 0; + } + }); + + return amountsMap; + }, }; diff --git a/src/data/schema/common.ts b/src/data/schema/common.ts index 8f3d259bc..4953c7b10 100644 --- a/src/data/schema/common.ts +++ b/src/data/schema/common.ts @@ -24,14 +24,14 @@ export const conformityQueryFields = ` `; export const commonTypes = ` + userId: String name: String! order: Int createdAt: Date hasNotified: Boolean assignedUserIds: [String] - assignedUsers: [User] + watchedUserIds: [String] labelIds: [String] - labels: [PipelineLabel] closeDate: Date description: String modifiedAt: Date @@ -40,9 +40,13 @@ export const commonTypes = ` isComplete: Boolean, isWatched: Boolean, stageId: String + priority: String + initialStageId: String + + labels: [PipelineLabel] + assignedUsers: [User] stage: Stage pipeline: Pipeline boardId: String - priority: String attachments: [Attachment] `; diff --git a/src/data/schema/ticket.ts b/src/data/schema/ticket.ts index 9ccc33eb9..9ddce7203 100644 --- a/src/data/schema/ticket.ts +++ b/src/data/schema/ticket.ts @@ -4,9 +4,13 @@ export const types = ` type Ticket { _id: String! source: String + ${commonTypes} + productsData: JSON + + products: JSON + amount: JSON companies: [Company] customers: [Customer] - ${commonTypes} } `; @@ -40,7 +44,8 @@ const commonParams = ` priority: String, source: String, reminderMinute: Int, - isComplete: Boolean + isComplete: Boolean, + productsData: JSON `; export const mutations = ` diff --git a/src/db/factories.ts b/src/db/factories.ts index 992572598..e78b330dc 100644 --- a/src/db/factories.ts +++ b/src/db/factories.ts @@ -902,7 +902,7 @@ export const dealFactory = async (params: IDealFactoryInput = {}) => { amount: faker.random.objectElement(), ...(!params.noCloseDate ? { closeDate: params.closeDate || new Date() } : {}), description: faker.random.word(), - productsDate: params.productsData, + productsData: params.productsData, assignedUserIds: params.assignedUserIds || [faker.random.word()], userId: params.userId || faker.random.word(), watchedUserIds: params.watchedUserIds, @@ -954,6 +954,13 @@ interface ITicketFactoryInput { source?: string; watchedUserIds?: string[]; labelIds?: string[]; + productsData?: any; + initialStageId?: string; + userId?: string; + order?: number; + reminderMinute?: number; + isComplete?: boolean; + modifiedBy?: string; } export const ticketFactory = async (params: ITicketFactoryInput = {}) => { @@ -968,10 +975,17 @@ export const ticketFactory = async (params: ITicketFactoryInput = {}) => { ...(!params.noCloseDate ? { closeDate: params.closeDate || new Date() } : {}), description: faker.random.word(), assignedUserIds: params.assignedUserIds, - priority: params.priority, - source: params.source, + priority: params.priority || faker.random.word(), + source: params.source || faker.random.word(), watchedUserIds: params.watchedUserIds, labelIds: params.labelIds || [], + productsData: params.productsData || [], + initialStageId: params.initialStageId || stage._id, + userId: params.userId || faker.random.uuid(), + order: params.order || faker.random.number(), + reminderMinute: params.reminderMinute || faker.random.number(), + isComplete: params.isComplete || faker.random.boolean(), + modifiedBy: params.modifiedBy || faker.random.uuid(), }); return ticket.save(); diff --git a/src/db/models/definitions/boards.ts b/src/db/models/definitions/boards.ts index 1ecb5ab42..bbcd63d9c 100644 --- a/src/db/models/definitions/boards.ts +++ b/src/db/models/definitions/boards.ts @@ -30,6 +30,8 @@ export interface IItemCommonFields { order?: number; searchText?: string; priority?: string; + reminderMinute?: number; + isComplete?: boolean; } export interface IProductData extends Document { diff --git a/src/db/models/definitions/tickets.ts b/src/db/models/definitions/tickets.ts index f82a92daf..0661be2ca 100644 --- a/src/db/models/definitions/tickets.ts +++ b/src/db/models/definitions/tickets.ts @@ -1,9 +1,10 @@ import { Document, Schema } from 'mongoose'; -import { commonItemFieldsSchema, IItemCommonFields } from './boards'; +import { commonItemFieldsSchema, IItemCommonFields, IProductData, productDataSchema } from './boards'; import { field } from './utils'; export interface ITicket extends IItemCommonFields { source?: string; + productsData?: IProductData[]; } export interface ITicketDocument extends ITicket, Document { @@ -15,4 +16,5 @@ export const ticketSchema = new Schema({ ...commonItemFieldsSchema, source: field({ type: String, label: 'Source' }), + productsData: field({ type: [productDataSchema], label: 'Products' }), }); From 5f72fb3e71538ab113a7fc535baa7b83b5ce54e3 Mon Sep 17 00:00:00 2001 From: Buyantogtokh Date: Tue, 10 Dec 2019 18:07:59 +0800 Subject: [PATCH 3/4] improve ticket queries test --- src/__tests__/ticketQueries.test.ts | 94 +++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 11 deletions(-) diff --git a/src/__tests__/ticketQueries.test.ts b/src/__tests__/ticketQueries.test.ts index 51db93d8d..f835fd3c7 100644 --- a/src/__tests__/ticketQueries.test.ts +++ b/src/__tests__/ticketQueries.test.ts @@ -5,23 +5,39 @@ import { conformityFactory, customerFactory, pipelineFactory, + pipelineLabelFactory, + productFactory, stageFactory, ticketFactory, userFactory, } from '../db/factories'; -import { Tickets } from '../db/models'; +import { Boards, Pipelines, Stages, Tickets } from '../db/models'; import { BOARD_TYPES } from '../db/models/definitions/constants'; import './setup.ts'; describe('ticketQueries', () => { - const commonTicketTypes = ` + const commonTicketFields = ` _id + userId + createdAt + order name - stageId - assignedUserIds closeDate + reminderMinute + isComplete description + assignedUserIds + watchedUserIds + labelIds + stageId + initialStageId + modifiedAt + modifiedBy + priority + productsData + source + companies { _id } customers { _id } assignedUsers { _id } @@ -31,6 +47,8 @@ describe('ticketQueries', () => { isWatched hasNotified labels { _id } + products + amount `; const qryTicketFilter = ` @@ -52,7 +70,7 @@ describe('ticketQueries', () => { source: $source closeDateType: $closeDateType ) { - ${commonTicketTypes} + ${commonTicketFields} } } `; @@ -60,13 +78,16 @@ describe('ticketQueries', () => { const qryDetail = ` query ticketDetail($_id: String!) { ticketDetail(_id: $_id) { - ${commonTicketTypes} + ${commonTicketFields} } } `; afterEach(async () => { // Clearing test data + await Boards.deleteMany({}); + await Pipelines.deleteMany({}); + await Stages.deleteMany({}); await Tickets.deleteMany({}); }); @@ -134,7 +155,6 @@ describe('ticketQueries', () => { const board = await boardFactory({ type: BOARD_TYPES.TICKET }); const pipeline = await pipelineFactory({ boardId: board._id, type: BOARD_TYPES.TICKET }); const stage = await stageFactory({ pipelineId: pipeline._id, type: BOARD_TYPES.TICKET }); - const args = { stageId: stage._id }; await ticketFactory(args); @@ -144,7 +164,7 @@ describe('ticketQueries', () => { const qryList = ` query tickets($stageId: String!) { tickets(stageId: $stageId) { - ${commonTicketTypes} + ${commonTicketFields} } } `; @@ -155,13 +175,65 @@ describe('ticketQueries', () => { }); test('Ticket detail', async () => { - const ticket = await ticketFactory(); + const customFieldsData = { field1: 'field1' }; + const product1 = await productFactory({ customFieldsData }); + const product2 = await productFactory({ customFieldsData }); + const user1 = await userFactory(); + const user2 = await userFactory(); + const label1 = await pipelineLabelFactory(); + const label2 = await pipelineLabelFactory(); + const board = await boardFactory({ type: BOARD_TYPES.TICKET }); + const pipeline = await pipelineFactory({ boardId: board._id, type: BOARD_TYPES.TICKET }); + const stage = await stageFactory({ pipelineId: pipeline._id, type: BOARD_TYPES.TICKET }); - const args = { _id: ticket._id }; + const productsData = [ + { + productId: product1._id, + currency: 'USD', + amount: 200, + }, + { + productId: product2._id, + currency: 'MNT', + amount: 300, + }, + ]; + + const ticket = await ticketFactory({ + productsData, + assignedUserIds: [user1._id], + watchedUserIds: [user2._id], + labelIds: [label1._id, label2._id], + stageId: stage._id, + }); - const response = await graphqlRequest(qryDetail, 'ticketDetail', args); + const response = await graphqlRequest(qryDetail, 'ticketDetail', { _id: ticket._id }, { user: user2 }); expect(response._id).toBe(ticket._id); + expect(response.userId).toBe(ticket.userId); + expect(response.order).toBe(ticket.order); + expect(response.name).toBe(ticket.name); + expect(response.reminderMinute).toBe(ticket.reminderMinute); + expect(response.isComplete).toBe(ticket.isComplete); + expect(response.description).toBe(ticket.description); + expect(response.assignedUserIds.length).toBe((ticket.assignedUserIds || []).length); + expect(response.watchedUserIds.length).toBe((ticket.watchedUserIds || []).length); + expect(response.labelIds.length).toBe((ticket.labelIds || []).length); + expect(response.stageId).toBe(ticket.stageId); + expect(response.initialStageId).toBe(ticket.initialStageId); + expect(response.modifiedBy).toBe(ticket.modifiedBy); + expect(response.priority).toBe(ticket.priority); + expect(response.source).toBe(ticket.source); + expect(response.productsData.length).toBe(2); + // resolvers + expect(response.labels.length).toBe(2); + expect(response.amount).toMatchObject({ USD: 200, MNT: 300 }); + expect(response.stage._id).toBe(stage._id); + expect(response.boardId).toBe(board._id); + expect(response.assignedUsers.length).toBe(1); + expect(response.pipeline._id).toBe(pipeline._id); + expect(response.isWatched).toBe(true); + expect(response.products.length).toBe(2); }); test('Ticket detail with watchedUserIds', async () => { From 72bfa5efe2fc7642d68267edafac0ff92af788db Mon Sep 17 00:00:00 2001 From: Buyantogtokh Date: Wed, 11 Dec 2019 13:03:15 +0800 Subject: [PATCH 4/4] improve ticket query test coverage --- src/__tests__/ticketQueries.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/__tests__/ticketQueries.test.ts b/src/__tests__/ticketQueries.test.ts index f835fd3c7..2451115ed 100644 --- a/src/__tests__/ticketQueries.test.ts +++ b/src/__tests__/ticketQueries.test.ts @@ -4,6 +4,7 @@ import { companyFactory, conformityFactory, customerFactory, + fieldFactory, pipelineFactory, pipelineLabelFactory, productFactory, @@ -175,7 +176,8 @@ describe('ticketQueries', () => { }); test('Ticket detail', async () => { - const customFieldsData = { field1: 'field1' }; + const field = await fieldFactory({ contentType: 'product', text: 'text' }); + const customFieldsData = { [field._id]: 'field1' }; const product1 = await productFactory({ customFieldsData }); const product2 = await productFactory({ customFieldsData }); const user1 = await userFactory();