From fb0272cc3e988a84eaf6016a7ff38cf3d2908c06 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 13 Jun 2024 14:28:51 +0200 Subject: [PATCH 01/10] feat: Add cap to schema --- package.json | 2 +- src/reference/openapi.yml | 8 ++++++++ src/schema.ts | 2 ++ yarn.lock | 8 ++++---- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a3e91fb66..25b27a80c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "author": "", "license": "ISC", "dependencies": { - "@appquality/tryber-database": "^0.40.3", + "@appquality/tryber-database": "^0.41.1", "@appquality/wp-auth": "^1.0.7", "@googlemaps/google-maps-services-js": "^3.3.7", "@sendgrid/mail": "^7.6.0", diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 4dd911bd2..d5e42e213 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -9986,6 +9986,10 @@ paths: type: string size: type: integer + cap: + type: integer + x-stoplight: + id: yzv1ppm6f5kq7 countries: type: array minItems: 1 @@ -11269,6 +11273,10 @@ components: type: string size: type: integer + cap: + type: integer + x-stoplight: + id: dv3yqv5k8gpv6 countries: type: array items: diff --git a/src/schema.ts b/src/schema.ts index 31bc68357..7e5f77e5b 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -927,6 +927,7 @@ export interface components { target?: { notes?: string; size?: number; + cap?: number; }; countries?: components["schemas"]["CountryCode"][]; languages?: number[]; @@ -4203,6 +4204,7 @@ export interface operations { target?: { notes?: string; size?: number; + cap?: number; }; countries?: components["schemas"]["CountryCode"][]; languages?: { diff --git a/yarn.lock b/yarn.lock index 15f5a854d..1cc4f3577 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,10 +28,10 @@ "@babel/parser" "^7.22.5" "@babel/traverse" "^7.22.5" -"@appquality/tryber-database@^0.40.3": - version "0.40.3" - resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.40.3.tgz#94bd841393ddfe269cb7d2f73e3cd401071f3a41" - integrity sha512-xseQiNLwef5AK11DwOyblVPDd0oeyzMXT+MLzv5y/t+tGe1NonI5yYZEF7UJAnmkCFRneZPRTBe4P++svVE2bA== +"@appquality/tryber-database@^0.41.1": + version "0.41.1" + resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.41.1.tgz#525e3e61f5ecbab0d634b97dcb0fed35b97dd4b7" + integrity sha512-KJaPGhJF6UKCnb5jWooZg68cjk9w0w/mFo4VkKz/x0m4CTI3eWEk85Sp7gN7lAh8P15CA46ZepFYfxY8il+eQg== dependencies: better-sqlite3 "^8.1.0" knex "^2.5.1" From c3018a5841a2209f004bf64ea94a74e21de4a7b6 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 13 Jun 2024 14:29:04 +0200 Subject: [PATCH 02/10] feat: Return cap in get dossier --- src/routes/dossiers/campaignId/_get/index.spec.ts | 11 +++++++++++ src/routes/dossiers/campaignId/_get/index.ts | 8 +++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/routes/dossiers/campaignId/_get/index.spec.ts b/src/routes/dossiers/campaignId/_get/index.spec.ts index f779f37ea..e4412a331 100644 --- a/src/routes/dossiers/campaignId/_get/index.spec.ts +++ b/src/routes/dossiers/campaignId/_get/index.spec.ts @@ -298,6 +298,7 @@ describe("Route GET /dossiers/:id", () => { updated_by: 100, product_type_id: 1, notes: "Notes", + cap: 100, }); await tryber.tables.CampaignDossierDataCountries.do().insert([ { @@ -529,5 +530,15 @@ describe("Route GET /dossiers/:id", () => { expect(response.status).toBe(200); expect(response.body).toHaveProperty("id", 1); }); + + it("Should return cap", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("target"); + expect(response.body.target).toHaveProperty("cap", 100); + }); }); }); diff --git a/src/routes/dossiers/campaignId/_get/index.ts b/src/routes/dossiers/campaignId/_get/index.ts index c3474e466..761d8a53f 100644 --- a/src/routes/dossiers/campaignId/_get/index.ts +++ b/src/routes/dossiers/campaignId/_get/index.ts @@ -136,6 +136,7 @@ export default class RouteItem extends UserRoute<{ "target_devices", "product_type_id", "notes", + "cap", tryber.ref("name").withSchema("product_types").as("product_type_name") ) .leftJoin( @@ -280,7 +281,9 @@ export default class RouteItem extends UserRoute<{ ...(this.campaign.out_of_scope && { outOfScope: this.campaign.out_of_scope, }), - ...((this.campaign.target_audience || this.campaign.target_size) && { + ...((this.campaign.target_audience || + this.campaign.target_size || + this.campaign.cap) && { target: { ...(this.campaign.target_audience && { notes: this.campaign.target_audience, @@ -288,6 +291,9 @@ export default class RouteItem extends UserRoute<{ ...(this.campaign.target_size && { size: this.campaign.target_size, }), + ...(this.campaign.cap && { + cap: this.campaign.cap, + }), }, }), ...(this.campaign.target_devices && { From 9dbe82d87864b36b1aae3a5648894ace22cf87a1 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 13 Jun 2024 14:32:12 +0200 Subject: [PATCH 03/10] feat: Allow creating dossier with cap --- src/routes/dossiers/_post/creation.spec.ts | 24 ++++++++++++++++++++++ src/routes/dossiers/_post/index.ts | 3 +++ 2 files changed, 27 insertions(+) diff --git a/src/routes/dossiers/_post/creation.spec.ts b/src/routes/dossiers/_post/creation.spec.ts index fbf87febb..0b8b57824 100644 --- a/src/routes/dossiers/_post/creation.spec.ts +++ b/src/routes/dossiers/_post/creation.spec.ts @@ -708,4 +708,28 @@ describe("Route POST /dossiers", () => { expect(getResponse.status).toBe(200); expect(getResponse.body).toHaveProperty("notes", "Notes"); }); + it("Should save the cap in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + target: { + cap: 100, + }, + }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const getResponse = await request(app) + .get(`/dossiers/${id}`) + .set("authorization", "Bearer admin"); + + expect(getResponse.status).toBe(200); + expect(getResponse.body).toHaveProperty("target"); + expect(getResponse.body.target).toHaveProperty("cap", 100); + }); }); diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index e25296e8a..0b30cfaf9 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -304,6 +304,9 @@ export default class RouteItem extends UserRoute<{ created_by: this.getTesterId(), updated_by: this.getTesterId(), notes: this.getBody().notes, + ...(this.getBody().target?.cap + ? { cap: this.getBody().target?.cap } + : {}), }) .returning("id"); From 4d1d6c9e93062f8ae1c7d92a3e4d7b2aaac8362d Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 13 Jun 2024 14:34:11 +0200 Subject: [PATCH 04/10] feat: Allow updating target cap --- src/routes/dossiers/campaignId/_put/index.ts | 3 +++ .../dossiers/campaignId/_put/update.spec.ts | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/routes/dossiers/campaignId/_put/index.ts b/src/routes/dossiers/campaignId/_put/index.ts index 07e840032..f04722626 100644 --- a/src/routes/dossiers/campaignId/_put/index.ts +++ b/src/routes/dossiers/campaignId/_put/index.ts @@ -169,6 +169,9 @@ export default class RouteItem extends UserRoute<{ ...(this.getBody().target?.size && { target_size: this.getBody().target?.size, }), + ...(this.getBody().target?.cap && { + cap: this.getBody().target?.cap, + }), product_type_id: this.getBody().productType, target_devices: this.getBody().deviceRequirements, notes: this.getBody().notes, diff --git a/src/routes/dossiers/campaignId/_put/update.spec.ts b/src/routes/dossiers/campaignId/_put/update.spec.ts index 8f2c68d90..5ada7cd39 100644 --- a/src/routes/dossiers/campaignId/_put/update.spec.ts +++ b/src/routes/dossiers/campaignId/_put/update.spec.ts @@ -673,6 +673,24 @@ describe("Route POST /dossiers", () => { expect(responseGet.status).toBe(200); expect(responseGet.body).toHaveProperty("notes", "Notes"); }); + it("Should update the cap in the dossier data", async () => { + await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + target: { + cap: 10, + }, + }); + + const responseGet = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + expect(responseGet.status).toBe(200); + expect(responseGet.body).toHaveProperty("target"); + expect(responseGet.body.target).toHaveProperty("cap", 10); + }); }); describe("Role handling", () => { From c715460a3a0380900bbfd98d1363f47259a61723 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Fri, 14 Jun 2024 13:03:57 +0200 Subject: [PATCH 05/10] rework: Refactor get campaigns --- src/routes/users/me/campaigns/_get/index.ts | 239 +++++++++----------- 1 file changed, 106 insertions(+), 133 deletions(-) diff --git a/src/routes/users/me/campaigns/_get/index.ts b/src/routes/users/me/campaigns/_get/index.ts index df8468720..ba9fcd91b 100644 --- a/src/routes/users/me/campaigns/_get/index.ts +++ b/src/routes/users/me/campaigns/_get/index.ts @@ -1,5 +1,3 @@ -import * as db from "@src/features/db"; -import Campaigns from "@src/features/db/class/Campaigns"; import UserRoute from "@src/features/routes/UserRoute"; import { tryber } from "@src/features/database"; @@ -9,18 +7,6 @@ import resolvePermalinks from "../../../../../features/wp/resolvePermalinks"; type TranslatablePage = StoplightComponents["schemas"]["TranslatablePage"]; -type CampaignType = { - id: number; - title: string; - page_preview_id: number; - page_manual_id: number; - start_date: string; - end_date: string; - close_date: string; - campaign_type?: string; - campaign_type_id: number; -}; - class RouteItem extends UserRoute<{ response: StoplightOperations["get-users-me-campaigns"]["responses"]["200"]["content"]["application/json"]; query: StoplightOperations["get-users-me-campaigns"]["parameters"]["query"]; @@ -34,58 +20,45 @@ class RouteItem extends UserRoute<{ private order: NonNullable["order"] | undefined; private start: NonNullable["start"]> = 0; - private limit: NonNullable["limit"]> = 10; - private hasLimit: boolean = false; - - private db: { - campaigns: Campaigns; - }; + private limit: NonNullable["limit"]; constructor(configuration: RouteClassConfiguration) { super(configuration); const query = this.getQuery(); + this.setOrderBy(); + this.setOrder(); + if (query.start) this.start = parseInt(query.start as unknown as string); + if (query.limit) this.limit = parseInt(query.limit as unknown as string); this.filterBy = query.filterBy || {}; - if ( - query.orderBy && - ["start_date", "end_date", "close_date"].includes(query.orderBy) - ) { - this.orderBy = query.orderBy as RouteItem["orderBy"]; - } - if (query.order && ["ASC", "DESC"].includes(query.order)) { - this.order = query.order; - } - if (query.start) { - this.start = parseInt(query.start as unknown as string); - } - if (query.limit) { - this.hasLimit = true; - this.limit = parseInt(query.limit as unknown as string); - } + } - this.db = { - campaigns: new Campaigns(["*"]), - }; + private setOrderBy() { + const { orderBy } = this.getQuery(); + if (!orderBy) return; + if (orderBy === "start_date") this.orderBy = "start_date"; + if (orderBy === "end_date") this.orderBy = "end_date"; + if (orderBy === "close_date") this.orderBy = "close_date"; + } + + private setOrder() { + const { order } = this.getQuery(); + if (!order) return; + if (order === "ASC") this.order = "ASC"; + if (order === "DESC") this.order = "DESC"; } protected async prepare() { try { - let campaigns = await this.getCampaigns(); - - if (!this.hasLimit) { - this.setSuccess(200, { - results: campaigns, - size: campaigns.length, - start: this.start, - }); - } - let total = campaigns.length || 0; - campaigns = campaigns.slice(this.start, this.limit + this.start); + const campaigns = await this.getCampaigns(); + const total = campaigns.length || 0; + const results = this.limit + ? campaigns.slice(this.start, this.limit + this.start) + : campaigns; this.setSuccess(200, { - results: campaigns, - size: campaigns.length, - limit: this.limit, - total: total, + results, + size: results.length, + ...(this.limit ? { limit: this.limit, total } : {}), start: this.start, }); } catch (e) { @@ -95,15 +68,27 @@ class RouteItem extends UserRoute<{ } private async getCampaigns() { - const query = await this.getCampaignsQuery(); + const results = await this.getCampaignsQuery(); - const results = await query; if (!results.length) { throw Error("no data found"); } - const enhancedCampaigns = (await this.enhanceCampaigns(results)).map( - (cp) => ({ + const items = await this.enhanceWithLinkedPages( + await this.enhanceWithCampaignType( + await this.enhanceCampaignsWithApplication(results) + ) + ); + + return items + .filter((campaign) => { + if (this.filterByAccepted()) return campaign.accepted; + else + return ( + !campaign.accepted && this.campaignHasAllPreviewPublished(campaign) + ); + }) + .map((cp) => ({ id: cp.id, name: cp.title, dates: { @@ -117,17 +102,7 @@ class RouteItem extends UserRoute<{ manual_link: cp.manual_link, preview_link: cp.preview_link, applied: cp.applied == 1, - }) - ); - - if (!this.filterByAccepted()) { - return enhancedCampaigns.filter( - (item: { preview_link: TranslatablePage }) => - this.campaignHasAllPreviewPublished(item) - ); - } - - return enhancedCampaigns; + })); } private async getCampaignsQuery() { @@ -194,34 +169,46 @@ class RouteItem extends UserRoute<{ return query; } - private async enhanceCampaigns(campaigns: CampaignType[]): Promise< - (CampaignType & { - preview_link: TranslatablePage; - manual_link: TranslatablePage; - campaign_type?: string; - applied?: 0 | 1; - })[] - > { - const applications = await this.getCampaignApplications(campaigns); + private async enhanceCampaignsWithApplication( + campaigns: (T & { id: number })[] + ) { + const applications = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("campaign_id", "accepted") + .whereIn( + "campaign_id", + campaigns.map((c) => c.id) + ) + .andWhere("user_id", this.getWordpressId()); - if (this.filterByAccepted()) { - campaigns = campaigns.filter((r) => { - const application = applications.find((a) => a.campaign_id == r.id); - return application ? application.accepted === 1 : false; - }); - } else { - campaigns = campaigns.filter((r) => { - const application = applications.find((a) => a.campaign_id == r.id); - return application ? application.accepted === 0 : true; - }); - } + return campaigns.map((campaign) => { + const application = applications.find( + (a) => a.campaign_id == campaign.id + ); - const linkedPages = await this.getLinkedPages(campaigns); - const types = await this.getListOfCampaignTypes(campaigns); + return { + ...campaign, + applied: applications.find((a) => a.campaign_id == campaign.id) + ? (1 as const) + : (0 as const), + accepted: application ? application.accepted === 1 : false, + }; + }); + } - let results = campaigns.map((campaign) => { - const type = types.find((t) => t.id == campaign.campaign_type_id); + private async enhanceWithLinkedPages( + campaigns: (T & { + page_manual_id: number; + page_preview_id: number; + })[] + ) { + const pageIds = campaigns.reduce( + (accumulator: number[], r) => + [r.page_preview_id, r.page_manual_id].concat(accumulator), + [] + ); + const linkedPages = await resolvePermalinks(pageIds); + return campaigns.map((campaign) => { return { ...campaign, preview_link: linkedPages[campaign.page_preview_id] @@ -230,34 +217,29 @@ class RouteItem extends UserRoute<{ manual_link: linkedPages[campaign.page_manual_id] ? linkedPages[campaign.page_manual_id] : {}, - campaign_type: type ? type.name : undefined, - applied: applications.find((a) => a.campaign_id == campaign.id) - ? (1 as 1) - : (0 as 0), }; }); - return results; } - private async getListOfCampaignTypes( - campaigns: CampaignType[] - ): Promise<{ id: number; name: string }[]> { + private async enhanceWithCampaignType( + campaigns: (T & { campaign_type_id: number })[] + ) { if (!campaigns.length) return []; - return await db.query( - `SELECT id,name FROM wp_appq_campaign_type WHERE id IN (${campaigns - .map((c) => db.format("?", [c.campaign_type_id])) - .join(",")})` - ); - } + const types = await tryber.tables.WpAppqCampaignType.do() + .select("id", "name") + .whereIn( + "id", + campaigns.map((c) => c.campaign_type_id) + ); - private async getLinkedPages(rows: CampaignType[]) { - const pageIds = rows.reduce( - (accumulator: number[], r) => - [r.page_preview_id, r.page_manual_id].concat(accumulator), - [] - ); - const pageLinks = await resolvePermalinks(pageIds); - return pageLinks; + return campaigns.map((campaign) => { + const type = types.find((t) => t.id == campaign.campaign_type_id); + + return { + ...campaign, + campaign_type: type ? type.name : undefined, + }; + }); } private campaignHasAllPreviewPublished(campaign: { @@ -283,24 +265,11 @@ class RouteItem extends UserRoute<{ return true; } - private getCampaignApplications( - campaigns: CampaignType[] - ): Promise<{ campaign_id: number; accepted: number }[]> { - return db.query( - `SELECT campaign_id,accepted - FROM wp_crowd_appq_has_candidate - WHERE campaign_id IN (${campaigns - .map((c) => db.format("?", [c.id])) - .join(",")}) AND user_id = ${this.getWordpressId()}` - ); - } - - private async getPageAccess(): Promise { - return ( - await db.query( - `SELECT view_id FROM wp_appq_lc_access WHERE tester_id = ${this.getTesterId()}` - ) - ).map((row: { view_id: number }) => db.format("?", [row.view_id])); + private async getPageAccess() { + return await tryber.tables.WpAppqLcAccess.do() + .select("view_id") + .where("tester_id", this.getTesterId()) + .then((rows) => rows.map((row) => row.view_id)); } private filterByAccepted() { @@ -308,21 +277,25 @@ class RouteItem extends UserRoute<{ if (parseInt(this.filterBy.accepted) === 1) return true; return false; } + private filterByCompleted() { if (typeof this.filterBy?.completed === "undefined") return false; if (parseInt(this.filterBy.completed) === 1) return true; return false; } + private filterByRunning() { if (typeof this.filterBy?.completed === "undefined") return false; if (parseInt(this.filterBy.completed) === 0) return true; return false; } + private filterByClosed() { if (typeof this.filterBy?.statusId === "undefined") return false; if (parseInt(this.filterBy.statusId) === 2) return true; return false; } + private filterByOpen() { if (typeof this.filterBy?.statusId === "undefined") return false; if (parseInt(this.filterBy.statusId) === 1) return true; From 045004033bb6bfed339694ca94c661b07ab5e68e Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Fri, 14 Jun 2024 15:25:59 +0200 Subject: [PATCH 06/10] feat: Allow target check --- package.json | 1 + .../me/campaigns/_get/UserTargetChecker.ts | 50 ++++++ src/routes/users/me/campaigns/_get/index.ts | 79 ++++++++- .../users/me/campaigns/_get/target.spec.ts | 167 ++++++++++++++++++ yarn.lock | 12 ++ 5 files changed, 305 insertions(+), 4 deletions(-) create mode 100644 src/routes/users/me/campaigns/_get/UserTargetChecker.ts create mode 100644 src/routes/users/me/campaigns/_get/target.spec.ts diff --git a/package.json b/package.json index 25b27a80c..8ba2dbfa3 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "express": "^4.18.1", "express-fileupload": "^1.4.0", "glob": "^8.0.3", + "i18n-iso-countries": "^7.11.2", "jsonwebtoken": "^8.5.1", "morgan": "^1.10.0", "multer-s3": "^2.10.0", diff --git a/src/routes/users/me/campaigns/_get/UserTargetChecker.ts b/src/routes/users/me/campaigns/_get/UserTargetChecker.ts new file mode 100644 index 000000000..3c0d415d1 --- /dev/null +++ b/src/routes/users/me/campaigns/_get/UserTargetChecker.ts @@ -0,0 +1,50 @@ +import { tryber } from "@src/features/database"; +import countryList from "i18n-iso-countries"; + +export class UserTargetChecker { + private testerId: number; + + private userLanguages: number[] = []; + private userCountry: string = ""; + + constructor({ testerId }: { testerId: number }) { + this.testerId = testerId; + countryList.registerLocale(require("i18n-iso-countries/langs/en.json")); + } + + async init() { + await this.initUserLanguages(); + } + + private async initUserLanguages() { + this.userLanguages = await tryber.tables.WpAppqProfileHasLang.do() + .select("language_id") + .where("profile_id", this.testerId) + .then((res) => res.map((r) => r.language_id)); + } + + private async initUserCountries() { + const country = await tryber.tables.WpAppqEvdProfile.do() + .select("country") + .where("id", this.testerId) + .then((res) => res[0].country); + + const countryCode = countryList.getAlpha2Codes()[country]; + this.userCountry = countryCode; + } + + inTarget(targetRules: { languages?: number[]; countries?: string[] }) { + if (Object.keys(targetRules).length === 0) return true; + const { languages, countries } = targetRules; + + if (languages && !languages.some((l) => this.userLanguages.includes(l))) { + return false; + } + + if (countries && !countries.includes(this.userCountry)) { + return false; + } + + return true; + } +} diff --git a/src/routes/users/me/campaigns/_get/index.ts b/src/routes/users/me/campaigns/_get/index.ts index ba9fcd91b..66e95f75d 100644 --- a/src/routes/users/me/campaigns/_get/index.ts +++ b/src/routes/users/me/campaigns/_get/index.ts @@ -2,6 +2,7 @@ import UserRoute from "@src/features/routes/UserRoute"; import { tryber } from "@src/features/database"; import resolvePermalinks from "../../../../../features/wp/resolvePermalinks"; +import { UserTargetChecker } from "./UserTargetChecker"; /** OPENAPI-CLASS: get-users-me-campaigns */ @@ -74,12 +75,20 @@ class RouteItem extends UserRoute<{ throw Error("no data found"); } - const items = await this.enhanceWithLinkedPages( - await this.enhanceWithCampaignType( - await this.enhanceCampaignsWithApplication(results) + const items = await this.filterByTargetRules( + await this.enhanceWithTargetRules( + await this.enhanceWithLinkedPages( + await this.enhanceWithCampaignType( + await this.enhanceCampaignsWithApplication(results) + ) + ) ) ); + if (!items.length) { + throw Error("no data found"); + } + return items .filter((campaign) => { if (this.filterByAccepted()) return campaign.accepted; @@ -116,6 +125,10 @@ class RouteItem extends UserRoute<{ "end_date", "close_date", "campaign_type_id", + tryber + .ref("is_public") + .withSchema("wp_appq_evd_campaign") + .as("visibility_type"), tryber .ref("name") .withSchema("wp_appq_campaign_type") @@ -142,7 +155,7 @@ class RouteItem extends UserRoute<{ const pageAccess = await this.getPageAccess(); query.where((q) => { - q.whereIn("is_public", [1, 2]); + q.whereIn("is_public", [1, 2, 4]); if (pageAccess.length) { q.orWhereIn("page_preview_id", pageAccess); } @@ -242,6 +255,64 @@ class RouteItem extends UserRoute<{ }); } + private async enhanceWithTargetRules( + campaigns: (T & { id: number; visibility_type: number })[] + ) { + const campaignsWithTarget = campaigns.filter( + (c) => c.visibility_type === 4 + ); + if (!campaignsWithTarget.length) return campaigns; + + const allowedLanguages = + await tryber.tables.CampaignDossierDataLanguages.do() + .select("campaign_id", "language_id") + .join( + "campaign_dossier_data", + "campaign_dossier_data.id", + "campaign_dossier_data_languages.campaign_dossier_data_id" + ) + .whereIn( + "campaign_dossier_data.campaign_id", + campaignsWithTarget.map((c) => c.id) + ); + + return campaigns.map((campaign) => { + if (campaign.visibility_type !== 4) return campaign; + + const languages = allowedLanguages + .filter((l) => l.campaign_id === campaign.id) + .map((l) => l.language_id); + + return { + ...campaign, + targetRules: { + ...(languages.length ? { languages } : {}), + }, + }; + }); + } + + private async filterByTargetRules( + campaigns: (T & { + targetRules?: { + languages?: number[]; + countries?: string[]; + }; + })[] + ) { + const userTargetChecker = new UserTargetChecker({ + testerId: this.getTesterId(), + }); + await userTargetChecker.init(); + return campaigns.filter((campaign) => { + if (!campaign.targetRules) { + return true; + } else { + return userTargetChecker.inTarget(campaign.targetRules); + } + }); + } + private campaignHasAllPreviewPublished(campaign: { preview_link: TranslatablePage; }) { diff --git a/src/routes/users/me/campaigns/_get/target.spec.ts b/src/routes/users/me/campaigns/_get/target.spec.ts new file mode 100644 index 000000000..25ecbf606 --- /dev/null +++ b/src/routes/users/me/campaigns/_get/target.spec.ts @@ -0,0 +1,167 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import resolvePermalinks from "@src/features/wp/resolvePermalinks"; +import request from "supertest"; + +jest.mock("@src/features/wp/resolvePermalinks"); + +const sevenDaysFromNow = new Date().setDate(new Date().getDate() + 7); +const endDate = new Date(sevenDaysFromNow).toISOString().split("T")[0]; +const fourteenDaysFromNow = new Date().setDate(new Date().getDate() + 14); +const closeDate = new Date(fourteenDaysFromNow).toISOString().split("T")[0]; + +describe("GET /users/me/campaigns - target", () => { + beforeAll(async () => { + (resolvePermalinks as jest.Mock).mockImplementation(() => { + return { + 1: { en: "en/test1", it: "it/test1", es: "es/test1" }, + 2: { en: "en/test2", it: "it/test2", es: "es/test2" }, + }; + }); + await tryber.tables.WpAppqCampaignType.do().insert({ + id: 1, + name: "Type", + category_id: 1, + }); + await tryber.seeds().campaign_statuses(); + await tryber.tables.WpAppqEvdCampaign.do().insert({ + start_date: new Date().toISOString().split("T")[0], + end_date: endDate, + close_date: closeDate, + campaign_type_id: 1, + page_preview_id: 1, + page_manual_id: 2, + os: "1", + platform_id: 1, + pm_id: 1, + customer_id: 1, + project_id: 1, + customer_title: "Customer", + id: 1, + title: "Public campaign", + is_public: 4, + phase_id: 20, + }); + + await tryber.tables.CampaignDossierData.do().insert({ + id: 1, + campaign_id: 1, + created_by: 1, + updated_by: 1, + }); + + await tryber.tables.CampaignDossierDataLanguages.do().insert({ + campaign_dossier_data_id: 1, + language_id: 1, + }); + + await tryber.tables.CampaignDossierDataCountries.do().insert({ + campaign_dossier_data_id: 1, + country_code: "IT", + }); + + await tryber.tables.WpAppqLang.do().insert([ + { + id: 1, + display_name: "English", + lang_code: "en", + }, + { + id: 2, + display_name: "Italian", + lang_code: "it", + }, + ]); + }); + + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.CampaignPhase.do().delete(); + jest.resetAllMocks(); + }); + + describe("Tester in target", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdProfile.do().insert({ + id: 1, + wp_user_id: 1, + email: "", + education_id: 1, + employment_id: 1, + country: "Italy", + }); + await tryber.tables.WpAppqProfileHasLang.do().insert({ + profile_id: 1, + language_id: 1, + }); + }); + afterAll(async () => { + await tryber.tables.WpAppqEvdProfile.do().delete(); + await tryber.tables.WpAppqProfileHasLang.do().delete(); + }); + it("Should show the campaign", async () => { + const response = await request(app) + .get("/users/me/campaigns") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("results"); + expect(Array.isArray(response.body.results)).toBe(true); + expect(response.body.results.length).toBe(1); + }); + }); + describe("Tester partially in target", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdProfile.do().insert({ + id: 1, + wp_user_id: 1, + email: "", + education_id: 1, + employment_id: 1, + country: "Italy", + }); + await tryber.tables.WpAppqProfileHasLang.do().insert({ + profile_id: 1, + language_id: 2, + }); + }); + afterAll(async () => { + await tryber.tables.WpAppqEvdProfile.do().delete(); + await tryber.tables.WpAppqProfileHasLang.do().delete(); + }); + it("Should not show the campaign", async () => { + const response = await request(app) + .get("/users/me/campaigns") + .set("Authorization", "Bearer tester"); + + expect(response.status).toBe(404); + }); + }); + describe("Tester not in target", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdProfile.do().insert({ + id: 1, + wp_user_id: 1, + email: "", + education_id: 1, + employment_id: 1, + country: "France", + }); + await tryber.tables.WpAppqProfileHasLang.do().insert({ + profile_id: 1, + language_id: 2, + }); + }); + afterAll(async () => { + await tryber.tables.WpAppqEvdProfile.do().delete(); + await tryber.tables.WpAppqProfileHasLang.do().delete(); + }); + it("Should show the campaign", async () => { + const response = await request(app) + .get("/users/me/campaigns") + .set("Authorization", "Bearer tester"); + + expect(response.status).toBe(404); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 1cc4f3577..c646e6a81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2446,6 +2446,11 @@ dezalgo@1.0.3: asap "^2.0.0" wrappy "1" +diacritics@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1" + integrity sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA== + diff-sequences@^27.4.0: version "27.4.0" resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.4.0.tgz" @@ -3196,6 +3201,13 @@ husky@^7.0.4: resolved "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz" integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== +i18n-iso-countries@^7.11.2: + version "7.11.2" + resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-7.11.2.tgz#9132d33e0fd2726f0cac2f39febf55870f12c2d3" + integrity sha512-aquYZvUqNW968dFDezDpnz8/b0qRosO3A1XBXlVAdZREABcMKU+zdu7+ckLeWrCdF6YYPVkwsdktPaZOIHdIAA== + dependencies: + diacritics "1.3.0" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" From 55b5bdb1d69b85a21104b171fb71354eeac1ec87 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Mon, 17 Jun 2024 11:17:50 +0200 Subject: [PATCH 07/10] feat: Add spots count to user campaigns --- src/reference/openapi.yml | 17 +++- .../users/me/campaigns/_get/cap.spec.ts | 93 +++++++++++++++++++ src/routes/users/me/campaigns/_get/index.ts | 75 ++++++++++++++- src/schema.ts | 4 + 4 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 src/routes/users/me/campaigns/_get/cap.spec.ts diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index d5e42e213..6d4f8c70d 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -10536,7 +10536,6 @@ components: id: type: integer CampaignOptional: - description: '' type: object x-examples: example-1: @@ -10678,14 +10677,26 @@ components: $ref: '#/components/schemas/TranslatablePage' bugform_link: oneOf: - - properties: {} - type: boolean + - type: boolean description: Exists only when the campaign bugform is deactivated. It is FALSE - $ref: '#/components/schemas/TranslatablePage' description: If bugform is deactivated is a boolean else contains URLs to bugforms for each languages applied: type: boolean description: True if you applied on this Campaign + visibility: + type: object + x-stoplight: + id: z9vo6su8stzvn + properties: + freeSpots: + type: integer + x-stoplight: + id: qy6n5oa4quk52 + totalSpots: + type: integer + x-stoplight: + id: 1jumwi4cvp91d CampaignRequired: description: '' type: object diff --git a/src/routes/users/me/campaigns/_get/cap.spec.ts b/src/routes/users/me/campaigns/_get/cap.spec.ts new file mode 100644 index 000000000..3a3deff5f --- /dev/null +++ b/src/routes/users/me/campaigns/_get/cap.spec.ts @@ -0,0 +1,93 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import resolvePermalinks from "@src/features/wp/resolvePermalinks"; +import request from "supertest"; + +jest.mock("@src/features/wp/resolvePermalinks"); + +const sevenDaysFromNow = new Date().setDate(new Date().getDate() + 7); +const endDate = new Date(sevenDaysFromNow).toISOString().split("T")[0]; +const fourteenDaysFromNow = new Date().setDate(new Date().getDate() + 14); +const closeDate = new Date(fourteenDaysFromNow).toISOString().split("T")[0]; + +describe("GET /users/me/campaigns - cap", () => { + beforeAll(async () => { + (resolvePermalinks as jest.Mock).mockImplementation(() => { + return { + 1: { en: "en/test1", it: "it/test1", es: "es/test1" }, + 2: { en: "en/test2", it: "it/test2", es: "es/test2" }, + }; + }); + await tryber.tables.WpAppqCampaignType.do().insert({ + id: 1, + name: "Type", + category_id: 1, + }); + await tryber.seeds().campaign_statuses(); + await tryber.tables.WpAppqEvdCampaign.do().insert({ + start_date: new Date().toISOString().split("T")[0], + end_date: endDate, + close_date: closeDate, + campaign_type_id: 1, + page_preview_id: 1, + page_manual_id: 2, + os: "1", + platform_id: 1, + pm_id: 1, + customer_id: 1, + project_id: 1, + customer_title: "Customer", + id: 1, + title: "Public campaign", + is_public: 4, + phase_id: 20, + }); + + await tryber.tables.CampaignDossierData.do().insert({ + id: 1, + campaign_id: 1, + created_by: 1, + updated_by: 1, + cap: 10, + }); + + await tryber.tables.WpCrowdAppqHasCandidate.do().insert([ + { campaign_id: 1, user_id: 10, accepted: -1 }, + { campaign_id: 1, user_id: 20, accepted: 0 }, + ]); + }); + + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.CampaignPhase.do().delete(); + jest.resetAllMocks(); + }); + + it("Should show free spots on the campaign with target", async () => { + const response = await request(app) + .get("/users/me/campaigns") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("results"); + expect(Array.isArray(response.body.results)).toBe(true); + expect(response.body.results.length).toBe(1); + expect(response.body.results[0]).toHaveProperty("visibility"); + expect(response.body.results[0].visibility).toHaveProperty("freeSpots", 9); + }); + + it("Should show total spots on the campaign with target", async () => { + const response = await request(app) + .get("/users/me/campaigns") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("results"); + expect(Array.isArray(response.body.results)).toBe(true); + expect(response.body.results.length).toBe(1); + expect(response.body.results[0]).toHaveProperty("visibility"); + expect(response.body.results[0].visibility).toHaveProperty( + "totalSpots", + 10 + ); + }); +}); diff --git a/src/routes/users/me/campaigns/_get/index.ts b/src/routes/users/me/campaigns/_get/index.ts index 66e95f75d..bf939bdd6 100644 --- a/src/routes/users/me/campaigns/_get/index.ts +++ b/src/routes/users/me/campaigns/_get/index.ts @@ -76,10 +76,12 @@ class RouteItem extends UserRoute<{ } const items = await this.filterByTargetRules( - await this.enhanceWithTargetRules( - await this.enhanceWithLinkedPages( - await this.enhanceWithCampaignType( - await this.enhanceCampaignsWithApplication(results) + await this.enhanceWithFreeSpots( + await this.enhanceWithTargetRules( + await this.enhanceWithLinkedPages( + await this.enhanceWithCampaignType( + await this.enhanceCampaignsWithApplication(results) + ) ) ) ) @@ -111,6 +113,14 @@ class RouteItem extends UserRoute<{ manual_link: cp.manual_link, preview_link: cp.preview_link, applied: cp.applied == 1, + ...(cp.freeSpots && cp.totalSpots + ? { + visibility: { + freeSpots: cp.freeSpots, + totalSpots: cp.totalSpots, + }, + } + : {}), })); } @@ -292,6 +302,63 @@ class RouteItem extends UserRoute<{ }); } + private async enhanceWithFreeSpots( + campaigns: (T & { id: number; visibility_type: number })[] + ) { + const campaignsWithTarget = campaigns.filter( + (c) => c.visibility_type === 4 + ); + if (!campaignsWithTarget.length) + return campaigns.map((c) => ({ + ...c, + freeSpots: undefined, + totalSpots: undefined, + })); + + const applicationSpots = await tryber.tables.CampaignDossierData.do() + .select("campaign_id", "cap") + .whereIn( + "campaign_id", + campaignsWithTarget.map((c) => c.id) + ); + + const validApplications = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("campaign_id") + .count({ + count: "user_id", + }) + .whereNot("accepted", -1) + .whereIn( + "campaign_id", + campaignsWithTarget.map((c) => c.id) + ) + .then((res) => + res.map((r) => ({ + campaign_id: r.campaign_id, + count: typeof r.count === "number" ? r.count : 0, + })) + ); + + return campaigns.map((campaign) => { + const applicationSpot = applicationSpots.find( + (c) => c.campaign_id === campaign.id + ); + const validApplicationsCount = validApplications.find( + (c) => c.campaign_id === campaign.id + ); + return { + ...campaign, + ...(applicationSpot + ? { + freeSpots: + applicationSpot.cap - (validApplicationsCount?.count || 0), + totalSpots: applicationSpot.cap, + } + : {}), + }; + }); + } + private async filterByTargetRules( campaigns: (T & { targetRules?: { diff --git a/src/schema.ts b/src/schema.ts index 7e5f77e5b..072d20b16 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -698,6 +698,10 @@ export interface components { bugform_link?: boolean | components["schemas"]["TranslatablePage"]; /** @description True if you applied on this Campaign */ applied?: boolean; + visibility?: { + freeSpots?: number; + totalSpots?: number; + }; }; CampaignRequired: { name: string; From 196ab4fb323252d4f5422b785c1fb4b971d1a01a Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 25 Jun 2024 15:20:20 +0200 Subject: [PATCH 08/10] feat: Use desired number of testers for cap --- src/routes/dossiers/_post/index.ts | 6 +- .../dossiers/campaignId/_get/cap.spec.ts | 92 +++++++++++++++++++ .../dossiers/campaignId/_get/index.spec.ts | 12 +-- src/routes/dossiers/campaignId/_get/index.ts | 5 +- src/routes/dossiers/campaignId/_put/index.ts | 6 +- .../users/me/campaigns/_get/cap.spec.ts | 2 +- src/routes/users/me/campaigns/_get/index.ts | 14 ++- 7 files changed, 114 insertions(+), 23 deletions(-) create mode 100644 src/routes/dossiers/campaignId/_get/cap.spec.ts diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index 0b30cfaf9..346eb9b48 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -261,6 +261,9 @@ export default class RouteItem extends UserRoute<{ os: os.join(","), form_factor: form_factor.join(","), base_bug_internal_id: "UG", + ...(this.getBody().target?.cap + ? { desired_number_of_testers: this.getBody().target?.cap } + : {}), ...(campaignToDuplicate ? { desired_number_of_testers: @@ -304,9 +307,6 @@ export default class RouteItem extends UserRoute<{ created_by: this.getTesterId(), updated_by: this.getTesterId(), notes: this.getBody().notes, - ...(this.getBody().target?.cap - ? { cap: this.getBody().target?.cap } - : {}), }) .returning("id"); diff --git a/src/routes/dossiers/campaignId/_get/cap.spec.ts b/src/routes/dossiers/campaignId/_get/cap.spec.ts new file mode 100644 index 000000000..e65ace5df --- /dev/null +++ b/src/routes/dossiers/campaignId/_get/cap.spec.ts @@ -0,0 +1,92 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("Route GET /dossiers/:id", () => { + beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { + id: 1, + name: "Active", + type_id: 1, + }, + ]); + await tryber.tables.WpAppqCustomer.do().insert({ + id: 1, + company: "Test Company", + pm_id: 1, + }); + await tryber.tables.WpAppqProject.do().insert({ + id: 1, + display_name: "Test Project", + customer_id: 1, + edited_by: 1, + }); + + await tryber.tables.WpAppqEvdProfile.do().insert([ + { + name: "Test", + surname: "CSM", + email: "", + education_id: 1, + employment_id: 1, + id: 1, + wp_user_id: 1, + }, + ]); + + await tryber.tables.WpAppqCampaignType.do().insert({ + id: 1, + name: "Test Type", + description: "Test Description", + category_id: 1, + }); + + await tryber.tables.WpAppqEvdPlatform.do().insert([ + { + id: 1, + name: "Test Device", + form_factor: 0, + architecture: 1, + }, + ]); + + await tryber.tables.WpAppqEvdCampaign.do().insert({ + id: 1, + project_id: 1, + campaign_type_id: 1, + title: "Test Campaign", + customer_title: "Test Customer Campaign", + start_date: "2019-08-24T14:15:22Z", + end_date: "2019-08-24T14:15:22Z", + close_date: "2019-08-27T14:15:22Z", + platform_id: 0, + os: "1", + page_manual_id: 0, + page_preview_id: 0, + pm_id: 1, + customer_id: 0, + desired_number_of_testers: 100, + }); + }); + + afterAll(async () => { + await tryber.tables.WpAppqCustomer.do().delete(); + await tryber.tables.WpAppqProject.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.WpAppqEvdPlatform.do().delete(); + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqEvdProfile.do().delete(); + await tryber.tables.CampaignPhase.do().delete(); + }); + + it("Should return cap", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("target"); + expect(response.body.target).toHaveProperty("cap", 100); + }); +}); diff --git a/src/routes/dossiers/campaignId/_get/index.spec.ts b/src/routes/dossiers/campaignId/_get/index.spec.ts index e4412a331..1d225916d 100644 --- a/src/routes/dossiers/campaignId/_get/index.spec.ts +++ b/src/routes/dossiers/campaignId/_get/index.spec.ts @@ -76,6 +76,7 @@ describe("Route GET /dossiers/:id", () => { page_preview_id: 0, pm_id: 1, customer_id: 0, + desired_number_of_testers: 100, }); await tryber.tables.CustomRoles.do().insert([ @@ -298,7 +299,6 @@ describe("Route GET /dossiers/:id", () => { updated_by: 100, product_type_id: 1, notes: "Notes", - cap: 100, }); await tryber.tables.CampaignDossierDataCountries.do().insert([ { @@ -530,15 +530,5 @@ describe("Route GET /dossiers/:id", () => { expect(response.status).toBe(200); expect(response.body).toHaveProperty("id", 1); }); - - it("Should return cap", async () => { - const response = await request(app) - .get("/dossiers/1") - .set("authorization", "Bearer admin"); - - expect(response.status).toBe(200); - expect(response.body).toHaveProperty("target"); - expect(response.body.target).toHaveProperty("cap", 100); - }); }); }); diff --git a/src/routes/dossiers/campaignId/_get/index.ts b/src/routes/dossiers/campaignId/_get/index.ts index 761d8a53f..bebb88169 100644 --- a/src/routes/dossiers/campaignId/_get/index.ts +++ b/src/routes/dossiers/campaignId/_get/index.ts @@ -38,6 +38,10 @@ export default class RouteItem extends UserRoute<{ "project_id", "campaign_type_id", "os", + tryber + .ref("desired_number_of_testers") + .withSchema("wp_appq_evd_campaign") + .as("cap"), tryber .ref("display_name") .withSchema("wp_appq_project") @@ -136,7 +140,6 @@ export default class RouteItem extends UserRoute<{ "target_devices", "product_type_id", "notes", - "cap", tryber.ref("name").withSchema("product_types").as("product_type_name") ) .leftJoin( diff --git a/src/routes/dossiers/campaignId/_put/index.ts b/src/routes/dossiers/campaignId/_put/index.ts index f04722626..d4b42f62d 100644 --- a/src/routes/dossiers/campaignId/_put/index.ts +++ b/src/routes/dossiers/campaignId/_put/index.ts @@ -128,6 +128,9 @@ export default class RouteItem extends UserRoute<{ customer_title: this.getBody().title.customer, os: os.join(","), form_factor: form_factor.join(","), + ...(this.getBody().target?.cap && { + desired_number_of_testers: this.getBody().target?.cap, + }), }) .where({ id: this.campaignId, @@ -169,9 +172,6 @@ export default class RouteItem extends UserRoute<{ ...(this.getBody().target?.size && { target_size: this.getBody().target?.size, }), - ...(this.getBody().target?.cap && { - cap: this.getBody().target?.cap, - }), product_type_id: this.getBody().productType, target_devices: this.getBody().deviceRequirements, notes: this.getBody().notes, diff --git a/src/routes/users/me/campaigns/_get/cap.spec.ts b/src/routes/users/me/campaigns/_get/cap.spec.ts index 3a3deff5f..47722774b 100644 --- a/src/routes/users/me/campaigns/_get/cap.spec.ts +++ b/src/routes/users/me/campaigns/_get/cap.spec.ts @@ -41,6 +41,7 @@ describe("GET /users/me/campaigns - cap", () => { title: "Public campaign", is_public: 4, phase_id: 20, + desired_number_of_testers: 10, }); await tryber.tables.CampaignDossierData.do().insert({ @@ -48,7 +49,6 @@ describe("GET /users/me/campaigns - cap", () => { campaign_id: 1, created_by: 1, updated_by: 1, - cap: 10, }); await tryber.tables.WpCrowdAppqHasCandidate.do().insert([ diff --git a/src/routes/users/me/campaigns/_get/index.ts b/src/routes/users/me/campaigns/_get/index.ts index bf939bdd6..9c3f4d346 100644 --- a/src/routes/users/me/campaigns/_get/index.ts +++ b/src/routes/users/me/campaigns/_get/index.ts @@ -315,10 +315,16 @@ class RouteItem extends UserRoute<{ totalSpots: undefined, })); - const applicationSpots = await tryber.tables.CampaignDossierData.do() - .select("campaign_id", "cap") + const applicationSpots = await tryber.tables.WpAppqEvdCampaign.do() + .select( + "id", + tryber + .ref("desired_number_of_testers") + .withSchema("wp_appq_evd_campaign") + .as("cap") + ) .whereIn( - "campaign_id", + "id", campaignsWithTarget.map((c) => c.id) ); @@ -341,7 +347,7 @@ class RouteItem extends UserRoute<{ return campaigns.map((campaign) => { const applicationSpot = applicationSpots.find( - (c) => c.campaign_id === campaign.id + (c) => c.id === campaign.id ); const validApplicationsCount = validApplications.find( (c) => c.campaign_id === campaign.id From 7f07ecc9cb5721ef249b6a3da87fc805a23f7105 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Fri, 28 Jun 2024 11:51:07 +0200 Subject: [PATCH 09/10] feat: Do not get cap from duplicated cp --- src/routes/dossiers/_post/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index 346eb9b48..699b1ec66 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -266,8 +266,6 @@ export default class RouteItem extends UserRoute<{ : {}), ...(campaignToDuplicate ? { - desired_number_of_testers: - campaignToDuplicate.desired_number_of_testers, min_allowed_media: campaignToDuplicate.min_allowed_media, cust_bug_vis: campaignToDuplicate.cust_bug_vis, campaign_type: campaignToDuplicate.campaign_type, From 862b0ed968640404a1e59d0831cf506c116bb9da Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 2 Jul 2024 12:50:27 +0200 Subject: [PATCH 10/10] fix: Allow saving target/cap 0 --- src/routes/dossiers/_post/creation.spec.ts | 41 +++++++++++++++++++ src/routes/dossiers/_post/index.ts | 5 +-- src/routes/dossiers/campaignId/_get/index.ts | 8 ++-- src/routes/dossiers/campaignId/_put/index.ts | 5 ++- .../dossiers/campaignId/_put/update.spec.ts | 37 ++++++++++++++++- 5 files changed, 86 insertions(+), 10 deletions(-) diff --git a/src/routes/dossiers/_post/creation.spec.ts b/src/routes/dossiers/_post/creation.spec.ts index 0b8b57824..83a8ea8de 100644 --- a/src/routes/dossiers/_post/creation.spec.ts +++ b/src/routes/dossiers/_post/creation.spec.ts @@ -564,6 +564,23 @@ describe("Route POST /dossiers", () => { expect(dossierData).toHaveLength(1); expect(dossierData[0]).toHaveProperty("target_size", 10); }); + it("Should save target size 0 in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, target: { size: 0 } }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("target_size", 0); + }); it("Should save the tester id in the dossier data", async () => { const response = await request(app) @@ -732,4 +749,28 @@ describe("Route POST /dossiers", () => { expect(getResponse.body).toHaveProperty("target"); expect(getResponse.body.target).toHaveProperty("cap", 100); }); + it("Should save the cap 0 in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + target: { + cap: 0, + }, + }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const getResponse = await request(app) + .get(`/dossiers/${id}`) + .set("authorization", "Bearer admin"); + + expect(getResponse.status).toBe(200); + expect(getResponse.body).toHaveProperty("target"); + expect(getResponse.body.target).toHaveProperty("cap", 0); + }); }); diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index 699b1ec66..175518eec 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -173,7 +173,6 @@ export default class RouteItem extends UserRoute<{ const campaign = await tryber.tables.WpAppqEvdCampaign.do() .select( "id", - "desired_number_of_testers", "min_allowed_media", "cust_bug_vis", "campaign_type", @@ -261,7 +260,7 @@ export default class RouteItem extends UserRoute<{ os: os.join(","), form_factor: form_factor.join(","), base_bug_internal_id: "UG", - ...(this.getBody().target?.cap + ...(typeof this.getBody().target?.cap !== "undefined" ? { desired_number_of_testers: this.getBody().target?.cap } : {}), ...(campaignToDuplicate @@ -297,7 +296,7 @@ export default class RouteItem extends UserRoute<{ goal: this.getBody().goal, out_of_scope: this.getBody().outOfScope, target_audience: this.getBody().target?.notes, - ...(this.getBody().target?.size && { + ...(typeof this.getBody().target?.size !== "undefined" && { target_size: this.getBody().target?.size, }), product_type_id: this.getBody().productType, diff --git a/src/routes/dossiers/campaignId/_get/index.ts b/src/routes/dossiers/campaignId/_get/index.ts index bebb88169..9ab2876a4 100644 --- a/src/routes/dossiers/campaignId/_get/index.ts +++ b/src/routes/dossiers/campaignId/_get/index.ts @@ -285,16 +285,16 @@ export default class RouteItem extends UserRoute<{ outOfScope: this.campaign.out_of_scope, }), ...((this.campaign.target_audience || - this.campaign.target_size || - this.campaign.cap) && { + typeof this.campaign.target_size !== "undefined" || + typeof this.campaign.cap !== "undefined") && { target: { ...(this.campaign.target_audience && { notes: this.campaign.target_audience, }), - ...(this.campaign.target_size && { + ...(typeof this.campaign.target_size !== "undefined" && { size: this.campaign.target_size, }), - ...(this.campaign.cap && { + ...(typeof this.campaign.cap !== "undefined" && { cap: this.campaign.cap, }), }, diff --git a/src/routes/dossiers/campaignId/_put/index.ts b/src/routes/dossiers/campaignId/_put/index.ts index d4b42f62d..eb5a0abcb 100644 --- a/src/routes/dossiers/campaignId/_put/index.ts +++ b/src/routes/dossiers/campaignId/_put/index.ts @@ -116,6 +116,7 @@ export default class RouteItem extends UserRoute<{ private async updateCampaign() { const { os, form_factor } = await this.getDevices(); + console.log(typeof this.getBody().target?.cap); await tryber.tables.WpAppqEvdCampaign.do() .update({ title: this.getBody().title.tester, @@ -128,7 +129,7 @@ export default class RouteItem extends UserRoute<{ customer_title: this.getBody().title.customer, os: os.join(","), form_factor: form_factor.join(","), - ...(this.getBody().target?.cap && { + ...(typeof this.getBody().target?.cap !== "undefined" && { desired_number_of_testers: this.getBody().target?.cap, }), }) @@ -169,7 +170,7 @@ export default class RouteItem extends UserRoute<{ goal: this.getBody().goal, out_of_scope: this.getBody().outOfScope, target_audience: this.getBody().target?.notes, - ...(this.getBody().target?.size && { + ...(typeof this.getBody().target?.size !== "undefined" && { target_size: this.getBody().target?.size, }), product_type_id: this.getBody().productType, diff --git a/src/routes/dossiers/campaignId/_put/update.spec.ts b/src/routes/dossiers/campaignId/_put/update.spec.ts index 5ada7cd39..c9b55f1ec 100644 --- a/src/routes/dossiers/campaignId/_put/update.spec.ts +++ b/src/routes/dossiers/campaignId/_put/update.spec.ts @@ -416,7 +416,7 @@ describe("Route POST /dossiers", () => { goal: "Original goal", out_of_scope: "Original out of scope", target_audience: "Original target audience", - target_size: 0, + target_size: 10, target_devices: "Original target devices", product_type_id: 2, created_by: 100, @@ -561,6 +561,23 @@ describe("Route POST /dossiers", () => { expect(dossierData).toHaveLength(1); expect(dossierData[0]).toHaveProperty("target_size", 10); }); + it("Should save target size 0 in the dossier data", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, target: { size: 0 } }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + + const id = 1; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("target_size", 0); + }); it("Should save the tester id in the dossier data", async () => { const response = await request(app) @@ -691,6 +708,24 @@ describe("Route POST /dossiers", () => { expect(responseGet.body).toHaveProperty("target"); expect(responseGet.body.target).toHaveProperty("cap", 10); }); + it("Should update the cap to 0 in the dossier data", async () => { + await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + target: { + cap: 0, + }, + }); + + const responseGet = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + expect(responseGet.status).toBe(200); + expect(responseGet.body).toHaveProperty("target"); + expect(responseGet.body.target).toHaveProperty("cap", 0); + }); }); describe("Role handling", () => {