diff --git a/.gitignore b/.gitignore index 14c39f3af..750c476ca 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ coverage/ keys/* !/**/.gitkeep.idea .idea -.vscode/*.log \ No newline at end of file +.vscode/*.log +.aider* diff --git a/package.json b/package.json index 8ba2dbfa3..32809d21f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "author": "", "license": "ISC", "dependencies": { - "@appquality/tryber-database": "^0.41.1", + "@appquality/tryber-database": "^0.41.9", "@appquality/wp-auth": "^1.0.7", "@googlemaps/google-maps-services-js": "^3.3.7", "@sendgrid/mail": "^7.6.0", diff --git a/src/features/db/class/PreselectionFormFields.ts b/src/features/db/class/PreselectionFormFields.ts index c075ac168..82dcf8c69 100644 --- a/src/features/db/class/PreselectionFormFields.ts +++ b/src/features/db/class/PreselectionFormFields.ts @@ -14,6 +14,7 @@ type PreselectionFormFieldsType = { | "address" | `cuf_${number}`; options: string; + invalid_options: string; question: string; priority: number; }; @@ -26,6 +27,7 @@ class PreselectionFormFieldsObject { options: PreselectionFormFieldsType["options"]; question: PreselectionFormFieldsType["question"]; priority: PreselectionFormFieldsType["priority"]; + invalid_options: PreselectionFormFieldsType["invalid_options"]; constructor(item: PreselectionFormFieldsType) { this.id = item.id; @@ -35,6 +37,7 @@ class PreselectionFormFieldsObject { this.options = item.options; this.question = item.question; this.priority = item.priority; + this.invalid_options = item.invalid_options; } public getOptions(): string[] | number[] { @@ -63,6 +66,7 @@ class Table extends Database<{ "options", "question", "priority", + "invalid_options", ], }); } diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 1f72109f5..970b3c769 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -1248,6 +1248,14 @@ paths: type: string value: type: string + status: + type: string + x-stoplight: + id: 3f1wy22lgngzn + enum: + - candidate + - excluded + - selected required: - id - name @@ -1702,6 +1710,7 @@ paths: - onlyAccepted - onlyCandidates - all + - candidatesAndExcluded in: query name: show description: Show accepted/candidates or both @@ -6241,6 +6250,7 @@ paths: - start_date - end_date - close_date + - visibility in: query name: orderBy description: The field for item order @@ -6925,9 +6935,15 @@ paths: type: array items: allOf: - - $ref: '#/components/schemas/PreselectionFormQuestion' - - type: object - properties: + - properties: + question: + type: string + x-stoplight: + id: ir6xgrpexauf5 + short_name: + type: string + x-stoplight: + id: bl9s0k131fjp4 value: oneOf: - type: integer @@ -6953,7 +6969,48 @@ paths: id: type: number required: + - question - id + - x-stoplight: + id: 76ya1n62vrxoj + oneOf: + - properties: + type: + $ref: '#/components/schemas/PreselectionQuestionSimple' + required: + - type + - x-stoplight: + id: age6qie67x6dt + properties: + type: + $ref: '#/components/schemas/PreselectionQuestionMultiple' + options: + type: array + x-stoplight: + id: 3eqfw8hp0j217 + items: + x-stoplight: + id: u67gsrnn8bhvb + type: string + required: + - type + - options + - x-stoplight: + id: v6nzvavd1bbnh + properties: + type: + $ref: '#/components/schemas/PreselectionQuestionCuf' + options: + type: array + x-stoplight: + id: ilyb6ci3tao5c + items: + x-stoplight: + id: 511phg4idvt3g + type: number + required: + - type + type: object examples: example-1: value: @@ -10287,6 +10344,12 @@ components: type: integer totalSpots: type: integer + type: + type: string + enum: + - available + - unavailable + - candidate CampaignRequired: description: '' type: object @@ -10493,8 +10556,7 @@ components: PreselectionFormQuestion: title: PreselectionFormQuestion allOf: - - type: object - properties: + - properties: question: type: string short_name: @@ -10502,50 +10564,60 @@ components: required: - question - oneOf: - - type: object - properties: + - properties: type: - type: string - enum: - - text + $ref: '#/components/schemas/PreselectionQuestionSimple' required: - type - - type: object - properties: + - properties: type: - type: string - enum: - - multiselect - - select - - radio + $ref: '#/components/schemas/PreselectionQuestionMultiple' options: type: array + x-stoplight: + id: xdbut0hjezieh items: - type: string + x-stoplight: + id: cln0chyj7s107 + type: object + properties: + value: + type: string + x-stoplight: + id: ifhnncm4ap0l8 + isInvalid: + type: boolean + x-stoplight: + id: 6imsh5mbep30d + required: + - value required: - type - - options - - type: object - properties: + - properties: type: - type: string - pattern: '^cuf_[0-9]*$' + $ref: '#/components/schemas/PreselectionQuestionCuf' options: type: array + x-stoplight: + id: 98p0edyoy9t49 items: - type: integer - required: - - type - - type: object - properties: - type: - type: string - enum: - - gender - - phone_number - - address + x-stoplight: + id: lri0oems0d8by + type: object + properties: + value: + type: number + x-stoplight: + id: hrwvvg2r2jm4v + isInvalid: + type: boolean + x-stoplight: + id: wiocd6n2pq0z5 + required: + - value required: - type + type: object Project: title: Project type: object @@ -10898,6 +10970,31 @@ components: - title - startDate - deviceList + PreselectionQuestionSimple: + title: PreselectionQuestionSimple + x-stoplight: + id: huxuwdbvi0pep + type: string + enum: + - gender + - text + - phone_number + - address + PreselectionQuestionMultiple: + title: PreselectionQuestionMultiple + x-stoplight: + id: op7khbr8grgql + type: string + enum: + - multiselect + - select + - radio + PreselectionQuestionCuf: + title: PreselectionQuestionCuf + x-stoplight: + id: 5diemuprwjolx + type: string + pattern: '^cuf_[0-9]*$' securitySchemes: JWT: type: http diff --git a/src/routes/campaigns/campaignId/candidates/_get/CandidateDevices.ts b/src/routes/campaigns/campaignId/candidates/_get/CandidateDevices.ts index 7bd45652c..3f9ec3324 100644 --- a/src/routes/campaigns/campaignId/candidates/_get/CandidateDevices.ts +++ b/src/routes/campaigns/campaignId/candidates/_get/CandidateDevices.ts @@ -5,7 +5,11 @@ class CandidateDevices implements CandidateData { private campaignId: number; private candidateIds: number[]; private filters?: { os?: string[] }; - private show: "onlyAccepted" | "onlyCandidates" | "all" = "all"; + private show: + | "onlyAccepted" + | "onlyCandidates" + | "all" + | "candidatesAndExcluded" = "all"; private _devices: | { @@ -36,7 +40,7 @@ class CandidateDevices implements CandidateData { campaignId: number; candidateIds: number[]; filters?: { os?: string[] }; - show: "onlyAccepted" | "onlyCandidates" | "all"; + show: "onlyAccepted" | "onlyCandidates" | "all" | "candidatesAndExcluded"; }) { this.candidateIds = candidateIds; this.filters = filters; diff --git a/src/routes/campaigns/campaignId/candidates/_get/Candidates.ts b/src/routes/campaigns/campaignId/candidates/_get/Candidates.ts index 0757f9760..df3afaafe 100644 --- a/src/routes/campaigns/campaignId/candidates/_get/Candidates.ts +++ b/src/routes/campaigns/campaignId/candidates/_get/Candidates.ts @@ -2,13 +2,17 @@ import { tryber } from "@src/features/database"; class Candidates { private campaign_id: number; - private show: "onlyAccepted" | "onlyCandidates" | "all" = "all"; + private show: + | "onlyAccepted" + | "onlyCandidates" + | "all" + | "candidatesAndExcluded" = "all"; constructor({ campaign_id, show, }: { campaign_id: number; - show: "onlyAccepted" | "onlyCandidates" | "all"; + show: "onlyAccepted" | "onlyCandidates" | "all" | "candidatesAndExcluded"; }) { this.campaign_id = campaign_id; this.show = show; @@ -29,7 +33,8 @@ class Candidates { .select( tryber.ref("id").withSchema("wp_appq_evd_profile"), "name", - "surname" + "surname", + "accepted" ) .where("campaign_id", this.campaign_id) .where("name", "<>", "Deleted User") @@ -40,9 +45,19 @@ class Candidates { query.where("accepted", 1); } else if (this.show === "onlyCandidates") { query.where("accepted", 0); + } else if (this.show === "candidatesAndExcluded") { + query.whereIn("accepted", [0, -1]); } - return await query; + return (await query).map((candidate) => ({ + ...candidate, + status: + candidate.accepted === 1 + ? ("selected" as const) + : candidate.accepted === 0 + ? ("candidate" as const) + : ("excluded" as const), + })); } } diff --git a/src/routes/campaigns/campaignId/candidates/_get/index.ts b/src/routes/campaigns/campaignId/candidates/_get/index.ts index a4d3a1c53..6394d764e 100644 --- a/src/routes/campaigns/campaignId/candidates/_get/index.ts +++ b/src/routes/campaigns/campaignId/candidates/_get/index.ts @@ -286,6 +286,7 @@ export default class RouteItem extends UserRoute<{ age: candidate.age, questions: candidate.questions, levels: candidate.levels, + status: candidate.status, }; }), size: candidates.length, diff --git a/src/routes/campaigns/campaignId/candidates/_get/show.spec.ts b/src/routes/campaigns/campaignId/candidates/_get/show.spec.ts index c76b425e7..87b9f4b37 100644 --- a/src/routes/campaigns/campaignId/candidates/_get/show.spec.ts +++ b/src/routes/campaigns/campaignId/candidates/_get/show.spec.ts @@ -25,23 +25,25 @@ describe("GET /campaigns/:campaignId/candidates - show ", () => { surname: "pluto", education_id: 1, employment_id: 1, + birth_date: "1999-01-01", + total_exp_pts: 90, + sex: 0, }; await tryber.tables.WpAppqEvdProfile.do().insert([ { ...profile, id: 1, wp_user_id: 1, - birth_date: "2000-01-01", - total_exp_pts: 100, - sex: 1, }, { ...profile, id: 2, wp_user_id: 2, - birth_date: "1999-01-01", - total_exp_pts: 90, - sex: 0, + }, + { + ...profile, + id: 3, + wp_user_id: 3, }, ]); @@ -55,6 +57,11 @@ describe("GET /campaigns/:campaignId/candidates - show ", () => { user_id: 1, accepted: 0, }, + { + ...candidate, + user_id: 3, + accepted: -1, + }, ]); await tryber.tables.WpCrowdAppqHasCandidate.do().insert([ { @@ -90,6 +97,11 @@ describe("GET /campaigns/:campaignId/candidates - show ", () => { id: 3, id_profile: 2, }, + { + ...device, + id: 4, + id_profile: 3, + }, ]); await tryber.tables.WpAppqEvdPlatform.do().insert({ @@ -145,7 +157,7 @@ describe("GET /campaigns/:campaignId/candidates - show ", () => { .get("/campaigns/1/candidates?show=all") .set("authorization", `Bearer tester olp {"appq_tester_selection":true}`); expect(response.body).toHaveProperty("results"); - expect(response.body.results).toHaveLength(2); + expect(response.body.results).toHaveLength(3); expect(response.body.results).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -154,6 +166,33 @@ describe("GET /campaigns/:campaignId/candidates - show ", () => { expect.objectContaining({ id: 2, }), + expect.objectContaining({ + id: 3, + }), + ]) + ); + }); + + it("Should return acceptance status", async () => { + const response = await request(app) + .get("/campaigns/1/candidates?show=all") + .set("authorization", `Bearer tester olp {"appq_tester_selection":true}`); + expect(response.body).toHaveProperty("results"); + expect(response.body.results).toHaveLength(3); + expect(response.body.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 1, + status: "candidate", + }), + expect.objectContaining({ + id: 2, + status: "selected", + }), + expect.objectContaining({ + id: 3, + status: "excluded", + }), ]) ); }); @@ -168,4 +207,21 @@ describe("GET /campaigns/:campaignId/candidates - show ", () => { expect(response.body.results[0].devices).toHaveLength(1); expect(response.body.results[0].devices[0]).toHaveProperty("id", 3); }); + it("Should show candidates and excluded if candidatesAndExcluded", async () => { + const response = await request(app) + .get("/campaigns/1/candidates?show=candidatesAndExcluded") + .set("authorization", `Bearer tester olp {"appq_tester_selection":true}`); + expect(response.body).toHaveProperty("results"); + expect(response.body.results).toHaveLength(2); + expect(response.body.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 1, + }), + expect.objectContaining({ + id: 3, + }), + ]) + ); + }); }); diff --git a/src/routes/campaigns/forms/FieldCreator.ts b/src/routes/campaigns/forms/FieldCreator.ts index 6307607d8..d055c7c3c 100644 --- a/src/routes/campaigns/forms/FieldCreator.ts +++ b/src/routes/campaigns/forms/FieldCreator.ts @@ -1,5 +1,5 @@ -import * as db from "@src/features/db"; import { tryber } from "@src/features/database"; +import * as db from "@src/features/db"; export default class FieldCreator { private formId: number; @@ -7,6 +7,7 @@ export default class FieldCreator { private question: string; private short_name: string | undefined; private options: string | undefined; + private invalid_options: string | undefined; private id: number | undefined; private priority: number; @@ -23,6 +24,7 @@ export default class FieldCreator { short_name, type, options, + invalid_options, id, priority, }: { @@ -30,7 +32,8 @@ export default class FieldCreator { question: string; short_name?: string; type: string; - options?: string[] | number[]; + options?: (string | number)[]; + invalid_options?: (string | number)[]; id?: number; priority: number; }) { @@ -42,6 +45,9 @@ export default class FieldCreator { this.question = question; this.short_name = short_name; this.options = options ? JSON.stringify(options) : undefined; + this.invalid_options = options + ? JSON.stringify(invalid_options) + : undefined; this.id = id; this.priority = priority; } @@ -68,6 +74,9 @@ export default class FieldCreator { form_id: this.formId, priority: this.priority, ...(this.options ? { options: this.options } : {}), + ...(this.invalid_options + ? { invalid_options: this.invalid_options } + : {}), ...(this.short_name ? { short_name: this.short_name } : {}), ...(this.id ? { id: this.id } : {}), }) diff --git a/src/routes/campaigns/forms/_post/index.spec.ts b/src/routes/campaigns/forms/_post/index.spec.ts index 9047aed3f..833ef5b20 100644 --- a/src/routes/campaigns/forms/_post/index.spec.ts +++ b/src/routes/campaigns/forms/_post/index.spec.ts @@ -416,7 +416,7 @@ async function checkValidFieldWithOptions({ { question, type, - options, + options: options.map((value) => ({ value })), }, ], }; diff --git a/src/routes/campaigns/forms/_post/index.ts b/src/routes/campaigns/forms/_post/index.ts index 78d48cc99..bf6cba786 100644 --- a/src/routes/campaigns/forms/_post/index.ts +++ b/src/routes/campaigns/forms/_post/index.ts @@ -2,8 +2,6 @@ import OpenapiError from "@src/features/OpenapiError"; import { tryber } from "@src/features/database"; -import Campaigns from "@src/features/db/class/Campaigns"; -import PreselectionForms from "@src/features/db/class/PreselectionForms"; import UserRoute from "@src/features/routes/UserRoute"; import FieldCreator from "../FieldCreator"; @@ -12,17 +10,9 @@ export default class RouteItem extends UserRoute<{ body: StoplightOperations["post-campaigns-forms"]["requestBody"]["content"]["application/json"]; }> { private campaignId: number | undefined; - private db: { - forms: PreselectionForms; - campaigns: Campaigns; - }; constructor(options: RouteItem["configuration"]) { super(options); - this.db = { - forms: new PreselectionForms(), - campaigns: new Campaigns(), - }; const body = this.getBody(); if (body.campaign) { this.campaignId = body.campaign; @@ -57,7 +47,7 @@ export default class RouteItem extends UserRoute<{ const fields = await this.createFields(form.id); this.setSuccess(201, { ...form, - fields, + fields: fields, }); } catch (e) { const error = e as OpenapiError; @@ -138,16 +128,36 @@ export default class RouteItem extends UserRoute<{ const results = []; let i = 1; for (const field of body.fields) { + const options = + "options" in field && field.options + ? (field.options as { isInvalid: boolean; value: string | number }[]) + : undefined; const item = new FieldCreator({ formId: formId, question: field.question, short_name: field.short_name, type: field.type, - options: field.hasOwnProperty("options") ? field.options : undefined, + options: + "options" in field && field.options + ? field.options.map((o) => o.value) + : undefined, + invalid_options: options + ? options + .filter( + (o): o is { isInvalid: true; value: number } => o.isInvalid + ) + .map((o) => o.value) + : undefined, priority: i++, }); try { - results.push(await item.create()); + const result = await item.create(); + results.push({ + ...result, + options: result.options + ? result.options.map((o: number | string) => ({ value: o })) + : undefined, + }); } catch (e) { throw { status_code: 406, diff --git a/src/routes/campaigns/forms/_post/screenout.spec.ts b/src/routes/campaigns/forms/_post/screenout.spec.ts new file mode 100644 index 000000000..e22cb60f0 --- /dev/null +++ b/src/routes/campaigns/forms/_post/screenout.spec.ts @@ -0,0 +1,231 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +const basicCampaign = { + title: "Test Campaign", + customer_title: "Test Campaign", + min_allowed_media: 1, + campaign_type: 0, + bug_lang: 0, + base_bug_internal_id: "I", + start_date: "2020-01-01", + end_date: "2020-01-01", + close_date: "2020-01-01", + campaign_type_id: 1, + os: "", + pm_id: 1, + is_public: 0, + page_manual_id: 0, + page_preview_id: 0, + status_id: 1, + platform_id: 1, + customer_id: 1, + project_id: 1, +}; + +const basicUserField = { + type: "select", + name: "A select field", + slug: "a-select-field", + custom_user_field_group_id: 10, + extras: "", + placeholder: "", +}; + +describe("POST /campaigns/forms/ - screenout", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert({ + ...basicCampaign, + id: 1, + }); + + await tryber.tables.WpAppqCustomUserFieldGroups.do().insert({ + id: 10, + name: "CUF group", + description: "CUF group description", + }); + + await tryber.tables.WpAppqCustomUserField.do().insert({ + ...basicUserField, + id: 1, + }); + + await tryber.tables.WpAppqCustomUserField.do().insert({ + ...basicUserField, + id: 2, + }); + }); + + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqCustomUserField.do().delete(); + }); + + afterEach(async () => { + await tryber.tables.WpAppqCampaignPreselectionForm.do().delete(); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().delete(); + }); + + it("Should save screenout options for select", async () => { + const response = await request(app) + .post("/campaigns/forms/") + .send({ + name: "My form", + fields: [ + { + question: "Yes or no", + type: "select", + priority: 1, + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], + }, + ], + creationDate: "2024-02-23 00:00:00", + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(201); + const { id } = response.body; + + const get = await request(app) + .get(`/campaigns/forms/${id}`) + .set( + "Authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Yes or no", + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], + }); + }); + it("Should save screenout options for multiselect", async () => { + const response = await request(app) + .post("/campaigns/forms/") + .send({ + name: "My form", + fields: [ + { + question: "Select one", + type: "multiselect", + priority: 1, + options: [ + { value: "Blue", isInvalid: true }, + { value: "Red", isInvalid: true }, + { value: "Yellow" }, + ], + }, + ], + creationDate: "2024-02-23 00:00:00", + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(201); + const { id } = response.body; + + const get = await request(app) + .get(`/campaigns/forms/${id}`) + .set( + "Authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Select one", + options: [ + { value: "Blue", isInvalid: true }, + { value: "Red", isInvalid: true }, + { value: "Yellow" }, + ], + }); + }); + it("Should save screenout options for radio", async () => { + const response = await request(app) + .post("/campaigns/forms/") + .send({ + name: "My form", + fields: [ + { + question: "Yes or no", + type: "radio", + priority: 1, + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], + }, + ], + creationDate: "2024-02-23 00:00:00", + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(201); + const { id } = response.body; + + const get = await request(app) + .get(`/campaigns/forms/${id}`) + .set( + "Authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Yes or no", + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], + }); + }); + it("Should save screenout options for cuf", async () => { + const response = await request(app) + .post("/campaigns/forms/") + .send({ + name: "My form", + fields: [ + { + question: "Electricity", + type: "cuf_1", + priority: 1, + options: [ + { value: 1, isInvalid: true }, + { value: 2, isInvalid: true }, + { value: 3 }, + ], + }, + ], + creationDate: "2024-02-23 00:00:00", + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + console.log(response.body); + expect(response.status).toBe(201); + const { id } = response.body; + + const get = await request(app) + .get(`/campaigns/forms/${id}`) + .set( + "Authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Electricity", + options: [ + { value: 1, isInvalid: true }, + { value: 2, isInvalid: true }, + { value: 3 }, + ], + }); + }); +}); diff --git a/src/routes/campaigns/forms/formId/_get/index.spec.ts b/src/routes/campaigns/forms/formId/_get/index.spec.ts index dc675fc34..5a35aaad3 100644 --- a/src/routes/campaigns/forms/formId/_get/index.spec.ts +++ b/src/routes/campaigns/forms/formId/_get/index.spec.ts @@ -1,8 +1,8 @@ -import app from "@src/app"; -import request from "supertest"; +import Campaign from "@src/__mocks__/mockedDb/campaign"; import PreselectionForm from "@src/__mocks__/mockedDb/preselectionForm"; import PreselectionFormFields from "@src/__mocks__/mockedDb/preselectionFormFields"; -import Campaign from "@src/__mocks__/mockedDb/campaign"; +import app from "@src/app"; +import request from "supertest"; describe("GET /campaigns/forms/{formId}", () => { beforeAll(() => { @@ -132,23 +132,28 @@ describe("GET /campaigns/forms/{formId}", () => { ); expect(response.body).toHaveProperty("fields"); expect(response.body.fields).toEqual([ - { id: 8, type: "cuf_1", options: [1, 2, 3], question: "Cuf question" }, + { + id: 8, + type: "cuf_1", + options: [{ value: 1 }, { value: 2 }, { value: 3 }], + question: "Cuf question", + }, { id: 2, type: "select", - options: ["Option 1", "Option 2"], + options: [{ value: "Option 1" }, { value: "Option 2" }], question: "Select question", }, { id: 3, type: "multiselect", - options: ["Option 3", "Option 4"], + options: [{ value: "Option 3" }, { value: "Option 4" }], question: "Multiselect question", }, { id: 4, type: "radio", - options: ["Yes", "No"], + options: [{ value: "Yes" }, { value: "No" }], question: "Radio question", }, { id: 5, type: "gender", question: "Gender question" }, diff --git a/src/routes/campaigns/forms/formId/_get/index.ts b/src/routes/campaigns/forms/formId/_get/index.ts index dfb09c20c..199761130 100644 --- a/src/routes/campaigns/forms/formId/_get/index.ts +++ b/src/routes/campaigns/forms/formId/_get/index.ts @@ -1,8 +1,8 @@ /** OPENAPI-CLASS: get-campaigns-forms-formId */ -import UserRoute from "@src/features/routes/UserRoute"; -import * as db from "@src/features/db"; import { tryber } from "@src/features/database"; +import * as db from "@src/features/db"; +import UserRoute from "@src/features/routes/UserRoute"; export default class RouteItem extends UserRoute<{ response: StoplightOperations["get-campaigns-forms-formId"]["responses"]["200"]["content"]["application/json"]; @@ -89,18 +89,36 @@ export default class RouteItem extends UserRoute<{ private async getFormFields() { const results = await tryber.tables.WpAppqCampaignPreselectionFormFields.do() - .select("id", "type", "question", "short_name", "options") + .select( + "id", + "type", + "question", + "short_name", + "options", + "invalid_options" + ) .where("form_id", this.getId()) .orderBy("priority", "asc"); return results.map((item) => { + const options = + isFieldTypeWithOptions(item.type) && item.options + ? parseOptions(item.options) + : undefined; + return { - ...item, + id: item.id, + type: item.type, + question: item.question, short_name: item.short_name ? item.short_name : undefined, - options: - isFieldTypeWithOptions(item.type) && item.options - ? parseOptions(item.options) - : undefined, + options: options + ? options.map((value: string | number) => { + const isInvalid = ( + item.invalid_options ? parseOptions(item.invalid_options) : [] + ).includes(value); + return { value, ...(isInvalid ? { isInvalid } : {}) }; + }) + : undefined, }; }); diff --git a/src/routes/campaigns/forms/formId/_get/screenout.spec.ts b/src/routes/campaigns/forms/formId/_get/screenout.spec.ts new file mode 100644 index 000000000..7bf5ad835 --- /dev/null +++ b/src/routes/campaigns/forms/formId/_get/screenout.spec.ts @@ -0,0 +1,148 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("GET /campaigns/forms/{formId} - screenout data", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert({ + id: 1, + title: "My campaign", + platform_id: 1, + start_date: "2021-01-01", + end_date: "2021-01-01", + page_manual_id: 1, + page_preview_id: 1, + customer_id: 1, + pm_id: 1, + project_id: 1, + customer_title: "My campaign", + }); + await tryber.tables.WpAppqCampaignPreselectionForm.do().insert({ + id: 1, + campaign_id: 1, + name: "The form", + }); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert([ + { + id: 2, + form_id: 1, + question: "Select question", + type: "select", + options: JSON.stringify(["Option 1", "Option 2"]), + priority: 2, + invalid_options: JSON.stringify(["Option 1"]), + }, + { + id: 3, + form_id: 1, + question: "Multiselect question", + type: "multiselect", + options: JSON.stringify(["Option 3", "Option 4"]), + priority: 3, + invalid_options: JSON.stringify(["Option 3"]), + }, + { + id: 4, + form_id: 1, + question: "Radio question", + type: "radio", + options: JSON.stringify(["Yes", "No"]), + priority: 4, + invalid_options: JSON.stringify(["No"]), + }, + { + id: 8, + form_id: 1, + question: "Cuf question", + type: "cuf_1", + options: JSON.stringify([1, 2, 3]), + priority: 1, + invalid_options: JSON.stringify([1]), + }, + ]); + }); + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqCampaignPreselectionForm.do().delete(); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().delete(); + }); + + it("Should return the invalid questions for select", async () => { + const response = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("fields"); + expect(response.body.fields).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 2, + options: [ + { value: "Option 1", isInvalid: true }, + { value: "Option 2" }, + ], + }), + ]) + ); + }); + it("Should return the invalid questions for multiselect", async () => { + const response = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("fields"); + expect(response.body.fields).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 3, + options: [ + { value: "Option 3", isInvalid: true }, + { value: "Option 4" }, + ], + }), + ]) + ); + }); + it("Should return the invalid questions for radio", async () => { + const response = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("fields"); + expect(response.body.fields).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 4, + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], + }), + ]) + ); + }); + it("Should return the invalid questions for cuf", async () => { + const response = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("fields"); + expect(response.body.fields).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 8, + options: [{ value: 1, isInvalid: true }, { value: 2 }, { value: 3 }], + }), + ]) + ); + }); +}); diff --git a/src/routes/campaigns/forms/formId/_put/index.spec.ts b/src/routes/campaigns/forms/formId/_put/index.spec.ts index ca269bf6f..eedb91207 100644 --- a/src/routes/campaigns/forms/formId/_put/index.spec.ts +++ b/src/routes/campaigns/forms/formId/_put/index.spec.ts @@ -1,8 +1,8 @@ -import app from "@src/app"; -import request from "supertest"; -import { tryber } from "@src/features/database"; import PreselectionForm from "@src/__mocks__/mockedDb/preselectionForm"; import PreselectionFormFields from "@src/__mocks__/mockedDb/preselectionFormFields"; +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; const basicCampaign = { title: "Test Campaign", @@ -47,13 +47,13 @@ const sampleBodyWithFields = { { id: 3, type: "cuf_2", - options: [1, 2], + options: [{ value: 1 }, { value: 2 }], question: "My select cuf question", }, { id: 4, type: "select", - options: ["option1", "option2"], + options: [{ value: "option1" }, { value: "option2" }], question: "My select question", }, ], diff --git a/src/routes/campaigns/forms/formId/_put/index.ts b/src/routes/campaigns/forms/formId/_put/index.ts index eb91dbd85..6714b2977 100644 --- a/src/routes/campaigns/forms/formId/_put/index.ts +++ b/src/routes/campaigns/forms/formId/_put/index.ts @@ -1,8 +1,8 @@ /** OPENAPI-CLASS: put-campaigns-forms-formId */ -import UserRoute from "@src/features/routes/UserRoute"; import OpenapiError from "@src/features/OpenapiError"; -import FieldCreator from "../../FieldCreator"; import { tryber } from "@src/features/database"; +import UserRoute from "@src/features/routes/UserRoute"; +import FieldCreator from "../../FieldCreator"; export default class RouteItem extends UserRoute<{ response: StoplightOperations["put-campaigns-forms-formId"]["responses"]["200"]["content"]["application/json"]; @@ -120,9 +120,21 @@ export default class RouteItem extends UserRoute<{ await this.clearFields(); let i = 1; for (const field of fields) { + const options = + "options" in field && field.options + ? (field.options as { isInvalid: boolean; value: string | number }[]) + : undefined; const fieldCreator = new FieldCreator({ ...field, formId: this.getId(), + + options: + "options" in field && field.options + ? field.options.map((o) => o.value) + : undefined, + invalid_options: options + ? options.filter((o) => o.isInvalid).map((o) => o.value) + : undefined, priority: i++, }); await fieldCreator.create(); diff --git a/src/routes/campaigns/forms/formId/_put/screenout.spec.ts b/src/routes/campaigns/forms/formId/_put/screenout.spec.ts new file mode 100644 index 000000000..8f2193124 --- /dev/null +++ b/src/routes/campaigns/forms/formId/_put/screenout.spec.ts @@ -0,0 +1,301 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +const basicCampaign = { + title: "Test Campaign", + customer_title: "Test Campaign", + min_allowed_media: 1, + campaign_type: 0, + bug_lang: 0, + base_bug_internal_id: "I", + start_date: "2020-01-01", + end_date: "2020-01-01", + close_date: "2020-01-01", + campaign_type_id: 1, + os: "", + pm_id: 1, + is_public: 0, + page_manual_id: 0, + page_preview_id: 0, + status_id: 1, + platform_id: 1, + customer_id: 1, + project_id: 1, +}; + +const sampleBody = { + name: "My form", + fields: [], +}; +describe("PUT /campaigns/forms/ - screenout", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert({ + ...basicCampaign, + id: 1, + }); + + await tryber.tables.WpAppqEvdCampaign.do().insert({ + ...basicCampaign, + id: 2, + }); + + await tryber.tables.WpAppqEvdCampaign.do().insert({ + ...basicCampaign, + id: 3, + }); + + await tryber.tables.WpAppqCustomUserFieldGroups.do().insert({ + id: 10, + name: "CUF group", + description: "CUF group description", + }); + + await tryber.tables.WpAppqCustomUserField.do().insert({ + id: 1, + type: "text", + name: "A text field", + slug: "a-text-field", + custom_user_field_group_id: 10, + placeholder: "write something", + extras: "", + }); + + await tryber.tables.WpAppqCustomUserField.do().insert({ + id: 2, + type: "multiselect", + name: "A multiselect field", + slug: "a-multiselect-field", + custom_user_field_group_id: 10, + placeholder: "select something", + extras: "", + }); + }); + + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqCustomUserField.do().delete(); + }); + + beforeEach(async () => { + await tryber.tables.WpAppqCampaignPreselectionForm.do().insert([ + { + id: 1, + name: "My empty form", + creation_date: "2020-01-01 23:59:59", + }, + { + id: 2, + name: "My empty form linked to campaign", + campaign_id: 1, + creation_date: "2020-01-01 23:59:59", + }, + { + id: 3, + name: "My form", + creation_date: "2020-01-01 23:59:59", + }, + { + id: 4, + name: "My empty form linked to campaign with access granted", + campaign_id: 2, + creation_date: "2020-01-01 23:59:59", + }, + ]); + + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 1, + form_id: 3, + type: "text", + question: "My text question", + short_name: "My short_name question", + priority: 3, + }); + + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 2, + form_id: 3, + question: "My text cuf question", + short_name: "My short_name cuf question", + type: "cuf_1", + priority: 2, + }); + + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 3, + form_id: 3, + type: "cuf_2", + question: "My cuf_2 cuf question", + short_name: "My short_name cuf_2 cuf question", + options: JSON.stringify([1, 2]), + priority: 4, + }); + + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 4, + form_id: 3, + type: "select", + question: "My select question", + short_name: "My short_name select question", + options: JSON.stringify(["option1", "option2"]), + priority: 1, + }); + }); + + afterEach(async () => { + await tryber.tables.WpAppqCampaignPreselectionForm.do().delete(); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().delete(); + }); + + it("Should save screenout options for select", async () => { + const response = await request(app) + .put("/campaigns/forms/1") + .send({ + ...sampleBody, + fields: [ + { + question: "Yes or no", + type: "select", + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], + }, + ], + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + + const get = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + 'Bearer tester capability ["manage_preselection_forms"]' + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Yes or no", + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], + }); + }); + + it("Should save screenout options for multiselect", async () => { + const response = await request(app) + .put("/campaigns/forms/1") + .send({ + ...sampleBody, + fields: [ + { + question: "Select one", + type: "multiselect", + options: [ + { value: "Red", isInvalid: true }, + { value: "Blue", isInvalid: true }, + { value: "Yellow" }, + ], + }, + ], + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + + const get = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + 'Bearer tester capability ["manage_preselection_forms"]' + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Select one", + options: [ + { value: "Red", isInvalid: true }, + { value: "Blue", isInvalid: true }, + { value: "Yellow" }, + ], + }); + }); + + it("Should save screenout options for radio", async () => { + const response = await request(app) + .put("/campaigns/forms/1") + .send({ + ...sampleBody, + fields: [ + { + question: "Yes or no", + type: "radio", + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], + }, + ], + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + + const get = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + 'Bearer tester capability ["manage_preselection_forms"]' + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Yes or no", + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], + }); + }); + + it("Should save screenout options for cuf", async () => { + const response = await request(app) + .put("/campaigns/forms/1") + .send({ + ...sampleBody, + fields: [ + { + question: "Electricity", + type: "cuf_1", + options: [ + { value: 1, isInvalid: true }, + { value: 2, isInvalid: true }, + { value: 3 }, + ], + }, + ], + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + + const get = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + 'Bearer tester capability ["manage_preselection_forms"]' + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Electricity", + options: [ + { value: 1, isInvalid: true }, + { value: 2, isInvalid: true }, + { value: 3 }, + ], + }); + }); +}); diff --git a/src/routes/users/me/campaigns/_get/UserTargetChecker.ts b/src/routes/users/me/campaigns/_get/UserTargetChecker.ts index e27c7fe21..e11e5e896 100644 --- a/src/routes/users/me/campaigns/_get/UserTargetChecker.ts +++ b/src/routes/users/me/campaigns/_get/UserTargetChecker.ts @@ -38,11 +38,19 @@ export class UserTargetChecker { if (Object.keys(targetRules).length === 0) return true; const { languages, countries } = targetRules; - if (languages && !languages.some((l) => this.userLanguages.includes(l))) { + if ( + languages && + languages.length && + !languages.some((l) => this.userLanguages.includes(l)) + ) { return false; } - if (countries && !countries.includes(this.userCountry)) { + if ( + countries && + countries.length && + !countries.includes(this.userCountry) + ) { return false; } diff --git a/src/routes/users/me/campaigns/_get/index.ts b/src/routes/users/me/campaigns/_get/index.ts index 264eb9e4e..bd5b997d7 100644 --- a/src/routes/users/me/campaigns/_get/index.ts +++ b/src/routes/users/me/campaigns/_get/index.ts @@ -17,7 +17,12 @@ class RouteItem extends UserRoute<{ completed?: "0" | "1"; statusId?: "1" | "2"; } = {}; - private orderBy: "start_date" | "end_date" | "close_date" | undefined; + private orderBy: + | "start_date" + | "end_date" + | "close_date" + | "visibility" + | undefined; private order: NonNullable["order"] | undefined; private start: NonNullable["start"]> = 0; @@ -33,12 +38,24 @@ class RouteItem extends UserRoute<{ this.filterBy = query.filterBy || {}; } + private getVisibility(campaign: { + applied: boolean; + start_date: string; + freeSpots?: number; + }): "candidate" | "unavailable" | "available" { + if (campaign.applied) return "candidate"; + if (new Date(campaign.start_date) <= new Date() || campaign.freeSpots === 0) + return "unavailable"; + return "available"; + } + 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"; + if (orderBy === "visibility") this.orderBy = "visibility"; } private setOrder() { @@ -69,9 +86,9 @@ class RouteItem extends UserRoute<{ } private async getCampaigns() { - const results = await this.getCampaignsQuery(); + const campaigns = await this.getCampaignsQuery(); - if (!results.length) { + if (!campaigns.length) { throw Error("no data found"); } @@ -80,7 +97,7 @@ class RouteItem extends UserRoute<{ await this.enhanceWithTargetRules( await this.enhanceWithLinkedPages( await this.enhanceWithCampaignType( - await this.enhanceCampaignsWithApplication(results) + await this.enhanceCampaignsWithApplication(campaigns) ) ) ) @@ -91,7 +108,7 @@ class RouteItem extends UserRoute<{ throw Error("no data found"); } - return items + const results = items .filter((campaign) => { if (this.filterByAccepted()) return campaign.accepted; else @@ -113,15 +130,75 @@ 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, - }, - } - : {}), + visibility: { + type: this.getVisibility({ + applied: cp.applied == 1, + start_date: cp.start_date, + freeSpots: cp.freeSpots, + }), + ...(cp.freeSpots !== undefined ? { freeSpots: cp.freeSpots } : {}), + ...(cp.totalSpots !== undefined ? { totalSpots: cp.totalSpots } : {}), + }, })); + + if (this.orderBy === "visibility") { + results.sort((a, b) => { + const { + type: typeA, + freeSpots: freeSpotsA, + totalSpots: totalSpotsA, + } = a.visibility; + const { + type: typeB, + freeSpots: freeSpotsB, + totalSpots: totalSpotsB, + } = b.visibility; + const startA = new Date(a.dates.start); + const startB = new Date(b.dates.start); + + // Helper function to calculate ratio + const ratio = (freeSpots?: number, totalSpots?: number) => + freeSpots !== undefined && totalSpots !== undefined + ? freeSpots / totalSpots + : Infinity; + + if (typeA === "available" && typeB === "available") { + const hasSpotsA = + freeSpotsA !== undefined && totalSpotsA !== undefined; + const hasSpotsB = + freeSpotsB !== undefined && totalSpotsB !== undefined; + + if (hasSpotsA && hasSpotsB) { + // Both have spots: sort by ratio + return ( + ratio(freeSpotsA, totalSpotsA) - ratio(freeSpotsB, totalSpotsB) + ); + } else if (hasSpotsA && !hasSpotsB) { + // A has spots, B doesn't + return -1; + } else if (!hasSpotsA && hasSpotsB) { + // B has spots, A doesn't + return 1; + } else { + // Neither have spots: sort by start date + return startA.getTime() - startB.getTime(); + } + } else if (typeA === "available" && typeB !== "available") { + return -1; // "available" campaigns come first + } else if (typeA !== "available" && typeB === "available") { + return 1; // "available" campaigns come first + } else if (typeA === "candidate" && typeB === "unavailable") { + return -1; // "candidate" comes before "unavailable" + } else if (typeA === "unavailable" && typeB === "candidate") { + return 1; // "candidate" comes before "unavailable" + } else { + // Both are "unavailable" or "candidate": sort by start date + return startA.getTime() - startB.getTime(); + } + }); + } + + return results; } private async getCampaignsQuery() { @@ -184,10 +261,15 @@ class RouteItem extends UserRoute<{ query.where("status_id", 1); } - query.orderBy( - this.orderBy || "wp_appq_evd_campaign.id", - this.orderBy ? this.order || "DESC" : "ASC" - ); + if (this.orderBy) { + if (this.orderBy === "visibility") { + query.orderBy("start_date", this.order || "DESC"); + } else { + query.orderBy(this.orderBy, this.order || "DESC"); + } + } else { + query.orderBy("wp_appq_evd_campaign.id", "ASC"); + } return query; } @@ -372,7 +454,7 @@ class RouteItem extends UserRoute<{ ); return { ...campaign, - ...(applicationSpot + ...(applicationSpot && applicationSpot.cap >= 0 ? { freeSpots: applicationSpot.cap - (validApplicationsCount?.count || 0), diff --git a/src/routes/users/me/campaigns/_get/visibility.spec.ts b/src/routes/users/me/campaigns/_get/visibility.spec.ts new file mode 100644 index 000000000..0e1aba634 --- /dev/null +++ b/src/routes/users/me/campaigns/_get/visibility.spec.ts @@ -0,0 +1,151 @@ +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"); +describe("GET /users/me/campaigns - visibility", () => { + 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.seeds().campaign_statuses(); + const profile = { + name: "jhon", + surname: "doe", + email: "jhon.doe@tryber.me", + employment_id: 1, + education_id: 1, + }; + await tryber.tables.WpAppqEvdProfile.do().insert([ + { + ...profile, + id: 1, + wp_user_id: 1, + }, + { + ...profile, + id: 2, + wp_user_id: 2, + }, + ]); + await tryber.tables.WpUsers.do().insert([ + { + ID: 1, + user_login: "tester", + }, + ]); + const basicCampaignObject = { + start_date: new Date().toISOString().split("T")[0], + end_date: new Date().toISOString().split("T")[0], + close_date: new Date().toISOString().split("T")[0], + campaign_type_id: 1, + page_preview_id: 1, + page_manual_id: 2, + os: "1", + is_public: 1, + status_id: 1 as 1, + platform_id: 1, + customer_id: 1, + pm_id: 1, + project_id: 1, + customer_title: "Customer title", + phase_id: 20, + }; + await tryber.tables.WpAppqCampaignType.do().insert({ + id: 1, + name: "Type", + category_id: 1, + }); + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { + ...basicCampaignObject, + id: 1, + title: "Campaign applied", + }, + { + ...basicCampaignObject, + id: 2, + title: "Campaign past start date", + start_date: new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], + }, + { + ...basicCampaignObject, + id: 3, + title: "Campaign no free spots", + desired_number_of_testers: 1, + is_public: 4, + }, + { + ...basicCampaignObject, + id: 4, + title: "Campaign future start date", + start_date: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], + }, + ]); + + await tryber.tables.WpCrowdAppqHasCandidate.do().insert([ + { + campaign_id: 1, + user_id: 1, + accepted: 0, + }, + { + campaign_id: 3, + user_id: 2, + accepted: 0, + }, + ]); + }); + + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.WpUsers.do().delete(); + await tryber.tables.WpAppqEvdProfile.do().delete(); + }); + + it("should return 'candidate' if the user is already applied", async () => { + const response = await request(app) + .get("/users/me/campaigns") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + console.log(response.body); + const campaign = response.body.results.find((c: any) => c.id === 1); + expect(campaign.visibility).toHaveProperty("type", "candidate"); + }); + + it("should return 'unavailable' if the start date is in the future", async () => { + const response = await request(app) + .get("/users/me/campaigns") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const campaign = response.body.results.find((c: any) => c.id === 2); + expect(campaign.visibility).toHaveProperty("type", "unavailable"); + }); + + it("should return 'unavailable' if there are no free spots", async () => { + const response = await request(app) + .get("/users/me/campaigns") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const campaign = response.body.results.find((c: any) => c.id === 3); + expect(campaign.visibility).toHaveProperty("type", "unavailable"); + }); + + it("should return 'available' if the user is not applied, the start date is not in the future, and there are free spots", async () => { + const response = await request(app) + .get("/users/me/campaigns") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const campaign = response.body.results.find((c: any) => c.id === 4); + expect(campaign.visibility).toHaveProperty("type", "available"); + }); +}); diff --git a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufMultiSelectQuestion.ts b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufMultiSelectQuestion.ts index ded74e2cc..fc11ca8fb 100644 --- a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufMultiSelectQuestion.ts +++ b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufMultiSelectQuestion.ts @@ -152,6 +152,19 @@ class CufMultiselectQuestion extends Question<{ ) ); } + + async isScreenedOut(item: { data: any }) { + if (!this.question.invalid_options) return false; + const values = item.data.value.id; + if (this.options.length === 0 || this.isNoneOfTheAbove(values)) + return false; + for (const v of values) { + if (this.question.invalid_options.includes(v)) { + return true; + } + } + return false; + } } export default CufMultiselectQuestion; diff --git a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufSelectableQuestion.ts b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufSelectableQuestion.ts index e1dc0d2bc..a16f82d93 100644 --- a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufSelectableQuestion.ts +++ b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufSelectableQuestion.ts @@ -125,6 +125,13 @@ class CufSelectQuestion extends Question<{ }); } } + + async isScreenedOut(item: { data: any }) { + if (!this.question.invalid_options) return false; + const value = item.data.value.id; + if (this.options.length === 0 || this.isNoneOfTheAbove(value)) return false; + return this.question.invalid_options.includes(value); + } } export default CufSelectQuestion; diff --git a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/SelectableQuestion.ts b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/SelectableQuestion.ts index 487059640..5c0ca2d67 100644 --- a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/SelectableQuestion.ts +++ b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/SelectableQuestion.ts @@ -96,6 +96,32 @@ class SelectableQuestion extends Question<{ private isMultiple() { return this.question.type === "multiselect"; } + + async isScreenedOut(item: { data: any }) { + if (!this.question.invalid_options) return false; + if (this.isSingle()) { + return this.isSingleScreenedOut(item.data); + } else if (this.isMultiple()) { + return this.isMultipleScreenedOut(item.data); + } + return false; + } + + private isSingleScreenedOut(data: any) { + const value = data.value.serialized; + return this.question.invalid_options.includes(value); + } + + private isMultipleScreenedOut(data: any) { + const value = data.value.serialized; + for (const v of value) { + if (this.question.invalid_options.includes(v)) { + return true; + } + } + + return false; + } } export default SelectableQuestion; diff --git a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/index.ts b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/index.ts index 2dbd62d1f..e0ec13d50 100644 --- a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/index.ts +++ b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/index.ts @@ -28,6 +28,10 @@ class Question { async insertData(item: { campaignId: number; data: any }): Promise { return; } + + async isScreenedOut(item: { data: any }) { + return false; + } } export default Question; diff --git a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/index.ts b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/index.ts index 81467e18d..46bafbad1 100644 --- a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/index.ts +++ b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/index.ts @@ -1,14 +1,14 @@ -import PreselectionFormFields from "@src/features/db/class/PreselectionFormFields"; -import CustomUserFields from "@src/features/db/class/CustomUserFields"; import CustomUserFieldExtras from "@src/features/db/class/CustomUserFieldExtras"; +import CustomUserFields from "@src/features/db/class/CustomUserFields"; +import PreselectionFormFields from "@src/features/db/class/PreselectionFormFields"; import Question from "./Questions"; -import SelectableQuestion from "./Questions/SelectableQuestion"; -import CufTextQuestion from "./Questions/CufTextQuestion"; -import CufSelectQuestion from "./Questions/CufSelectableQuestion"; +import AddressQuestion from "./Questions/AddressQuestion"; import CufMultiSelectQuestion from "./Questions/CufMultiSelectQuestion"; -import PhoneQuestion from "./Questions/PhoneQuestion"; +import CufSelectQuestion from "./Questions/CufSelectableQuestion"; +import CufTextQuestion from "./Questions/CufTextQuestion"; import GenderQuestion from "./Questions/GenderQuestion"; -import AddressQuestion from "./Questions/AddressQuestion"; +import PhoneQuestion from "./Questions/PhoneQuestion"; +import SelectableQuestion from "./Questions/SelectableQuestion"; import SimpleTextQuestion from "./Questions/SimpleTextQuestion"; type QuestionType = Awaited< diff --git a/src/routes/users/me/campaigns/campaignId/forms/_post/index.ts b/src/routes/users/me/campaigns/campaignId/forms/_post/index.ts index 6d5bdf502..9509615eb 100644 --- a/src/routes/users/me/campaigns/campaignId/forms/_post/index.ts +++ b/src/routes/users/me/campaigns/campaignId/forms/_post/index.ts @@ -1,5 +1,6 @@ /** OPENAPI-CLASS: post-users-me-campaigns-campaignId-forms */ +import { tryber } from "@src/features/database"; import CampaignApplications from "@src/features/db/class/CampaignApplications"; import Campaigns, { CampaignObject } from "@src/features/db/class/Campaigns"; import Experience from "@src/features/db/class/Experience"; @@ -140,8 +141,8 @@ class RouteItem extends UserRoute<{ protected async prepare(): Promise { try { - await this.handleForm(); - await this.applyToCampaign(); + const isScreenedOut = await this.handleForm(); + await this.applyToCampaign({ isScreenedOut }); await this.addExperiencePoints(); } catch (e) { this.setError(403, e as OpenapiError); @@ -150,12 +151,12 @@ class RouteItem extends UserRoute<{ this.setSuccess(200, {}); } - private async applyToCampaign() { - await this.db.applications.insert({ + private async applyToCampaign({ isScreenedOut }: { isScreenedOut: boolean }) { + await tryber.tables.WpCrowdAppqHasCandidate.do().insert({ campaign_id: this.campaignId, user_id: this.getWordpressId(), devices: this.deviceId.toString(), - accepted: 0, + accepted: isScreenedOut ? -1 : 0, }); } @@ -174,6 +175,7 @@ class RouteItem extends UserRoute<{ private async handleForm() { const questionItems = await this.getQuestionItems(); await this.checkQuestionValidity(questionItems); + let isScreenedOut = false; for (const field of this.form) { if (questionItems.hasOwnProperty(field.question)) { const question = questionItems[field.question]; @@ -181,8 +183,14 @@ class RouteItem extends UserRoute<{ campaignId: this.campaignId, data: field, }); + if (!isScreenedOut) { + isScreenedOut = await question.isScreenedOut({ + data: field, + }); + } } } + return isScreenedOut; } private async checkQuestionValidity(questionItems: { diff --git a/src/routes/users/me/campaigns/campaignId/forms/_post/screenout.spec.ts b/src/routes/users/me/campaigns/campaignId/forms/_post/screenout.spec.ts new file mode 100644 index 000000000..233d34866 --- /dev/null +++ b/src/routes/users/me/campaigns/campaignId/forms/_post/screenout.spec.ts @@ -0,0 +1,389 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("POST users/me/campaigns/:campaignId/forms - screenout", () => { + beforeEach(async () => { + await tryber.tables.WpAppqEvdProfile.do().insert({ + id: 1, + wp_user_id: 1, + email: "", + employment_id: 1, + education_id: 1, + }); + await tryber.tables.WpAppqEvdCampaign.do().insert({ + id: 1, + start_date: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], + end_date: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], + title: "Cp", + customer_title: "Cp", + is_public: 1, + os: "1,2", + platform_id: 1, + page_preview_id: 1, + page_manual_id: 1, + pm_id: 1, + project_id: 1, + customer_id: 1, + }); + await tryber.tables.WpAppqCustomUserField.do().insert({ + id: 1, + slug: "select-field", + name: "Select field", + type: "select", + placeholder: "", + extras: "", + custom_user_field_group_id: 0, + }); + await tryber.tables.WpAppqCustomUserField.do().insert({ + id: 2, + slug: "multiselect-field", + name: "Multiselect field", + type: "multiselect", + placeholder: "", + extras: "", + custom_user_field_group_id: 0, + }); + await tryber.tables.WpAppqCustomUserFieldExtras.do().insert([ + { + id: 1, + custom_user_field_id: 1, + name: "Yes", + }, + { + id: 2, + custom_user_field_id: 2, + name: "Red", + }, + { + id: 11, + custom_user_field_id: 1, + name: "No", + }, + { + id: 22, + custom_user_field_id: 2, + name: "Blue", + }, + { + id: 222, + custom_user_field_id: 2, + name: "Yellow", + }, + ]); + await tryber.tables.WpAppqCampaignPreselectionForm.do().insert({ + id: 1, + campaign_id: 1, + }); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 1, + form_id: 1, + type: "select", + options: JSON.stringify(["Yes", "No"]), + invalid_options: JSON.stringify(["No"]), + }); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 2, + form_id: 1, + type: "multiselect", + options: JSON.stringify(["Blue", "Red", "Yellow"]), + invalid_options: JSON.stringify(["Blue", "Red"]), + }); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 3, + form_id: 1, + type: "radio", + options: JSON.stringify(["Yes", "No"]), + invalid_options: JSON.stringify(["No"]), + }); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 4, + form_id: 1, + type: "cuf_1", + options: JSON.stringify([1, 11]), + invalid_options: JSON.stringify([1]), + }); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 5, + form_id: 1, + type: "cuf_2", + options: JSON.stringify([2, 22, 222]), + invalid_options: JSON.stringify([2, 22]), + }); + await tryber.tables.WpCrowdAppqDevice.do().insert({ + id: 1, + id_profile: 1, + enabled: 1, + platform_id: 1, + }); + }); + + afterEach(async () => { + await tryber.tables.WpAppqEvdProfile.do().delete(); + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpCrowdAppqDevice.do().delete(); + await tryber.tables.WpAppqCampaignPreselectionForm.do().delete(); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().delete(); + await tryber.tables.WpAppqCampaignPreselectionFormData.do().delete(); + await tryber.tables.WpAppqExpPoints.do().delete(); + await tryber.tables.WpCrowdAppqHasCandidate.do().delete(); + await tryber.tables.WpAppqCustomUserField.do().delete(); + await tryber.tables.WpAppqCustomUserFieldExtras.do().delete(); + }); + it("Should set candidate with applied -1 if invalid answer is selected for select", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 1, + value: { serialized: "No" }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: -1 }); + }); + it("Should set candidate with applied 0 if valid answer is selected for select", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 1, + value: { serialized: "Yes" }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: 0 }); + }); + it("Should set candidate with applied -1 if invalid answer is selected for radio", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 3, + value: { serialized: "No" }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: -1 }); + }); + it("Should set candidate with applied 0 if valid answer is selected for radio", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 3, + value: { serialized: "Yes" }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: 0 }); + }); + it("Should set candidate with applied -1 if all invalid answers are selected for multiselect", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 2, + value: { serialized: ["Blue", "Red"] }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: -1 }); + }); + it("Should set candidate with applied -1 if some invalid answers are selected for multiselect", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 2, + value: { serialized: ["Blue", "Yellow"] }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: -1 }); + }); + it("Should set candidate with applied 0 if all valid answers are selected for multiselect", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 2, + value: { serialized: ["Yellow"] }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: 0 }); + }); + it("Should set candidate with applied -1 if invalid answer is selected for cuf select", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 4, + value: { id: 1, serialized: "1" }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: -1 }); + }); + it("Should set candidate with applied 0 if valid answer is selected for cuf select", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 4, + value: { id: 11, serialized: "11" }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: 0 }); + }); + it("Should set candidate with applied -1 if all invalid answers are selected for multiselect cuf", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 5, + value: { id: [2, 22], serialized: ["Red", "Blue"] }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: -1 }); + }); + it("Should set candidate with applied -1 if some invalid answers are selected for multiselect cuf", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 5, + value: { id: [2, 222], serialized: ["Red", "Yellow"] }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: -1 }); + }); + it("Should set candidate with applied 0 if all valid answers are selected for multiselect cuf", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 5, + value: { id: [222], serialized: ["Yellow"] }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: 0 }); + }); +}); diff --git a/src/schema.ts b/src/schema.ts index a08a941f1..6c729f4a7 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -701,6 +701,8 @@ export interface components { visibility?: { freeSpots?: number; totalSpots?: number; + /** @enum {string} */ + type?: "available" | "unavailable" | "candidate"; }; }; CampaignRequired: { @@ -799,21 +801,21 @@ export interface components { short_name?: string; } & ( | { - /** @enum {string} */ - type: "text"; + type: components["schemas"]["PreselectionQuestionSimple"]; } | { - /** @enum {string} */ - type: "multiselect" | "select" | "radio"; - options: string[]; - } - | { - type: string; - options?: number[]; + type: components["schemas"]["PreselectionQuestionMultiple"]; + options?: { + value: string; + isInvalid?: boolean; + }[]; } | { - /** @enum {string} */ - type: "gender" | "phone_number" | "address"; + type: components["schemas"]["PreselectionQuestionCuf"]; + options?: { + value: number; + isInvalid?: boolean; + }[]; } ); /** Project */ @@ -939,6 +941,18 @@ export interface components { productType?: number; notes?: string; }; + /** + * PreselectionQuestionSimple + * @enum {string} + */ + PreselectionQuestionSimple: "gender" | "text" | "phone_number" | "address"; + /** + * PreselectionQuestionMultiple + * @enum {string} + */ + PreselectionQuestionMultiple: "multiselect" | "select" | "radio"; + /** PreselectionQuestionCuf */ + PreselectionQuestionCuf: string; }; responses: { /** A user */ @@ -1425,7 +1439,11 @@ export interface operations { /** Array with min and max */ filterByAge?: unknown; /** Show accepted/candidates or both */ - show?: "onlyAccepted" | "onlyCandidates" | "all"; + show?: + | "onlyAccepted" + | "onlyCandidates" + | "all" + | "candidatesAndExcluded"; }; }; responses: { @@ -1458,6 +1476,8 @@ export interface operations { title?: string; value?: string; }[]; + /** @enum {string} */ + status?: "candidate" | "excluded" | "selected"; }[]; } & components["schemas"]["PaginationData"]; }; @@ -3081,7 +3101,12 @@ export interface operations { /** How to order values (ASC, DESC) */ order?: components["parameters"]["order"]; /** The field for item order */ - orderBy?: "name" | "start_date" | "end_date" | "close_date"; + orderBy?: + | "name" + | "start_date" + | "end_date" + | "close_date" + | "visibility"; }; }; responses: { @@ -3280,7 +3305,9 @@ export interface operations { /** OK */ 200: { content: { - "application/json": (components["schemas"]["PreselectionFormQuestion"] & { + "application/json": ({ + question: string; + short_name?: string; value?: | number | { @@ -3294,7 +3321,19 @@ export interface operations { error?: string; }; id: number; - })[]; + } & ( + | { + type: components["schemas"]["PreselectionQuestionSimple"]; + } + | { + type: components["schemas"]["PreselectionQuestionMultiple"]; + options: string[]; + } + | { + type: components["schemas"]["PreselectionQuestionCuf"]; + options?: number[]; + } + ))[]; }; }; 403: components["responses"]["NotAuthorized"]; diff --git a/yarn.lock b/yarn.lock index c646e6a81..4229788f9 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.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== +"@appquality/tryber-database@^0.41.9": + version "0.41.9" + resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.41.9.tgz#ccf906ece07052a62959bf315214324b78ecfd9b" + integrity sha512-LR9rWCATzRJDezIGSmGnGALImaNRvUwcWStQsMMkcQkLl3uxouw0VGZKvRz/tVSBl8Jr8bPGiqpgCGwyckngAA== dependencies: better-sqlite3 "^8.1.0" knex "^2.5.1"