From 3f88b9f983ec46e3d8cc07879bf6b4f921d7a603 Mon Sep 17 00:00:00 2001 From: ZecD Date: Fri, 5 Dec 2025 09:36:17 +0100 Subject: [PATCH] feat: add human resources PUT route Modified src/reference/openapi.yml feat: implement PUT route for updating human resources in campaign test: enhance PUT route tests for human resources with additional assertions feat: enhance validation for human resources PUT route to check profile and work rate existence Modified src/reference/openapi.yml Modified src/reference/openapi.yml fix: remove exclusiveMinimum property from assignee in campaigns path --- src/reference/openapi.yml | 44 +++++ .../humanResources/_put/index.spec.ts | 151 ++++++++++++++++++ .../campaignId/humanResources/_put/index.ts | 93 +++++++++++ src/schema.ts | 27 ++++ 4 files changed, 315 insertions(+) create mode 100644 src/routes/dossiers/campaignId/humanResources/_put/index.spec.ts create mode 100644 src/routes/dossiers/campaignId/humanResources/_put/index.ts diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 608534e94..eaf818ca0 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -6663,8 +6663,52 @@ paths: summary: Get Human Resources for a campaign tags: [] parameters: + - schema: + type: string + name: campaign + in: path + required: true - $ref: '#/components/parameters/campaign' description: Updates tokens_usage in campaign and updates the link between cp_id and agreementId + put: + summary: Your PUT endpoint + tags: [] + responses: + '200': + description: OK + '403': + $ref: '#/components/responses/NotAuthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + description: Internal Server Error + operationId: put-dossiers-campaign-humanResources + x-stoplight: + id: 9b1ouv9rd766s + security: + - JWT: [] + requestBody: + description: Overwrites the data for the given campaign in the campaign_human_resources table + content: + application/json: + schema: + type: array + items: + type: object + required: + - assignee + - days + - rate + properties: + assignee: + type: integer + minimum: 1 + days: + type: number + minimum: 0 + rate: + type: integer + minimum: 1 '/dossiers/{campaign}/manual': parameters: - $ref: '#/components/parameters/campaign' diff --git a/src/routes/dossiers/campaignId/humanResources/_put/index.spec.ts b/src/routes/dossiers/campaignId/humanResources/_put/index.spec.ts new file mode 100644 index 000000000..14112af07 --- /dev/null +++ b/src/routes/dossiers/campaignId/humanResources/_put/index.spec.ts @@ -0,0 +1,151 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("Route PUT/dossiers/:campaignId/humanResources", () => { + beforeAll(async () => { + const campaign = { + title: "Test Campaign", + customer_title: "Test Campaign", + start_date: "2023-01-01", + end_date: "2023-12-31", + pm_id: 1, + platform_id: 1, + page_preview_id: 1, + page_manual_id: 1, + customer_id: 1, + project_id: 1, + }; + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { ...campaign, id: 1 }, + { ...campaign, id: 2 }, + ]); + const tester = { + education_id: 1, + email: "", + employment_id: 1, + }; + await tryber.tables.WpAppqEvdProfile.do().insert([ + { ...tester, id: 1, wp_user_id: 10, name: "Tester One" }, + { ...tester, id: 2, wp_user_id: 20, name: "Tester Two" }, + { ...tester, id: 3, wp_user_id: 30, name: "Tester Three" }, + ]); + await tryber.tables.WorkRates.do().insert([ + { id: 1, name: "Researcher", daily_rate: 1.5 }, + { id: 2, name: "PM", daily_rate: 2.0 }, + ]); + }); + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqEvdProfile.do().delete(); + await tryber.tables.WorkRates.do().delete(); + }); + + it("Should answer 403 if not logged in", async () => { + const response = await request(app) + .put("/dossiers/1/humanResources") + .send([{ assignee: 3, days: 5, rate: 1 }]); + expect(response.status).toBe(403); + }); + + it("Should answer 403 if no access to campaign", async () => { + const response = await request(app) + .put("/dossiers/2/humanResources") + .send([{ assignee: 3, days: 5, rate: 1 }]) + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(403); + }); + + it("Should answer 400 if invalid body", async () => { + const response = await request(app) + .put("/dossiers/1/humanResources") + .send([{ assignee: -3, days: 5, rate: 1 }]) + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(400); + }); + + it("Should answer 400 if profile does not exist", async () => { + const response = await request(app) + .put("/dossiers/1/humanResources") + .send([{ assignee: 999, days: 5, rate: 1 }]) + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(400); + }); + + it("Should answer 400 if work rate does not exist", async () => { + const response = await request(app) + .put("/dossiers/1/humanResources") + .send([{ assignee: 3, days: 5, rate: 999 }]) + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(400); + }); + + it("Should answer 200 if admin", async () => { + const response = await request(app) + .put("/dossiers/1/humanResources") + .send([{ assignee: 3, days: 5, rate: 1 }]) + .set("authorization", "Bearer admin"); + expect(response.status).toBe(200); + }); + it("Should answer 400 if tester", async () => { + const response = await request(app) + .put("/dossiers/1/humanResources") + .send([{ assignee: 3, days: 5, rate: 1 }]) + .set("authorization", "Bearer tester"); + expect(response.status).toBe(403); + }); + it("Should answer 200 if has access to campaign", async () => { + const response = await request(app) + .put("/dossiers/1/humanResources") + .send([{ assignee: 3, days: 5, rate: 1 }]) + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(200); + }); + + describe("With basic data", () => { + beforeAll(async () => { + await tryber.tables.CampaignHumanResources.do().insert([ + { + id: 1, + campaign_id: 1, + profile_id: 1, + days: 10, + work_rate_id: 1, + }, + ]); + }); + afterAll(async () => { + await tryber.tables.CampaignHumanResources.do().delete(); + }); + + it("Should remove the old records and update the human resources", async () => { + const response = await request(app) + .put("/dossiers/1/humanResources") + .send([ + { assignee: 3, days: 5, rate: 1 }, + { assignee: 2, days: 8, rate: 2 }, + ]) + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + const humanResources = + await tryber.tables.CampaignHumanResources.do().select(); + expect(humanResources).toHaveLength(2); + expect(humanResources).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + campaign_id: 1, + profile_id: 3, + days: 5, + work_rate_id: 1, + }), + expect.objectContaining({ + campaign_id: 1, + profile_id: 2, + days: 8, + work_rate_id: 2, + }), + ]) + ); + expect(response.status).toBe(200); + }); + }); +}); diff --git a/src/routes/dossiers/campaignId/humanResources/_put/index.ts b/src/routes/dossiers/campaignId/humanResources/_put/index.ts new file mode 100644 index 000000000..1ab54ae16 --- /dev/null +++ b/src/routes/dossiers/campaignId/humanResources/_put/index.ts @@ -0,0 +1,93 @@ +/** OPENAPI-CLASS: put-dossiers-campaign-humanResources */ +import { tryber } from "@src/features/database"; +import OpenapiError from "@src/features/OpenapiError"; +import CampaignRoute from "@src/features/routes/CampaignRoute"; + +export default class RouteItem extends CampaignRoute<{ + response: StoplightOperations["put-dossiers-campaign-humanResources"]["responses"]["200"]; + parameters: StoplightOperations["put-dossiers-campaign-humanResources"]["parameters"]["path"]; + body: StoplightOperations["put-dossiers-campaign-humanResources"]["requestBody"]["content"]["application/json"]; +}> { + protected async filter() { + if (!(await super.filter())) return false; + + if (!this.hasAccessToCampaign(this.cp_id)) { + this.setError(403, new OpenapiError("You are not authorized to do this")); + return false; + } + if (!(await this.validateHumanResources())) { + return false; + } + return true; + } + + private async isProfileValid(profileId: number) { + const profile = await tryber.tables.WpAppqEvdProfile.do() + .select("id") + .where("id", profileId) + .first(); + return !!profile; + } + + private async isWorkRateValid(rateId: number) { + const workRate = await tryber.tables.WorkRates.do() + .select("id") + .where("id", rateId) + .first(); + return !!workRate; + } + + private async validateHumanResources() { + const body = this.getBody(); + for (const item of body) { + if (!(await this.isProfileValid(item.assignee))) { + this.setError( + 400, + new OpenapiError(`Profile with id ${item.assignee} does not exist`) + ); + return false; + } + if (!(await this.isWorkRateValid(item.rate))) { + this.setError( + 400, + new OpenapiError(`Work rate with id ${item.rate} does not exist`) + ); + return false; + } + } + return true; + } + + private async updateHumanResources() { + const body = this.getBody(); + + try { + await tryber.tables.CampaignHumanResources.do() + .delete() + .where("campaign_id", this.cp_id); + + await tryber.tables.CampaignHumanResources.do().insert([ + ...body.map((item) => ({ + campaign_id: this.cp_id, + profile_id: item.assignee, + days: item.days, + work_rate_id: item.rate, + })), + ]); + } catch (error) { + throw new OpenapiError("Failed to update human resources"); + } + } + + protected async prepare() { + try { + await this.updateHumanResources(); + } catch (error: OpenapiError | any) { + this.setError(500, { + message: error.message, + } as OpenapiError); + return; + } + this.setSuccess(200, {}); + } +} diff --git a/src/schema.ts b/src/schema.ts index 1e23908e0..7fa5eb36c 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -367,6 +367,7 @@ export interface paths { }; "/dossiers/{campaign}/humanResources": { get: operations["get-dossiers-campaign-humanResources"]; + put: operations["put-dossiers-campaign-humanResources"]; parameters: { path: { /** A campaign id */ @@ -3245,6 +3246,32 @@ export interface operations { }; }; }; + "put-dossiers-campaign-humanResources": { + parameters: { + path: { + /** A campaign id */ + campaign: components["parameters"]["campaign"]; + }; + }; + responses: { + /** OK */ + 200: unknown; + 403: components["responses"]["NotAuthorized"]; + 404: components["responses"]["NotFound"]; + /** Internal Server Error */ + 500: unknown; + }; + /** Overwrites the data for the given campaign in the campaign_human_resources table */ + requestBody: { + content: { + "application/json": { + assignee: number; + days: number; + rate: number; + }[]; + }; + }; + }; "post-dossiers-campaign-manual": { parameters: { path: {