diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 3579ae434..9e6318da0 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -5398,6 +5398,33 @@ paths: required: true schema: type: string + '/dossiers/{campaign}/costs': + parameters: + - schema: + type: string + name: campaign + in: path + required: true + get: + summary: Your GET endpoint + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + totalCost: + type: number + operationId: get-dossiers-campaign-costs + x-stoplight: + id: q9l4m7dj053wk + security: + - JWT: [] + parameters: + - $ref: '#/components/parameters/filterBy' /education: get: operationId: get-education diff --git a/src/routes/dossiers/campaignId/costs/_get/index.spec.ts b/src/routes/dossiers/campaignId/costs/_get/index.spec.ts new file mode 100644 index 000000000..47199cc55 --- /dev/null +++ b/src/routes/dossiers/campaignId/costs/_get/index.spec.ts @@ -0,0 +1,178 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +const campaign_1 = { + id: 1, + project_id: 1, + platform_id: 1, + start_date: "2023-01-13 10:10:10", + end_date: "2023-01-14 10:10:10", + title: "", + page_preview_id: 1, + page_manual_id: 1, + customer_id: 1, + pm_id: 1, + customer_title: "", + tokens_usage: 25, +}; +const campaign_2 = { + ...campaign_1, + id: 2, + project_id: 1, + tokens_usage: 10, +}; +const campaign_3 = { + ...campaign_1, + id: 3, + project_id: 1, + tokens_usage: 5, +}; +const project = { + display_name: "", + edited_by: 1, +}; + +const payment_1 = { + id: 11, + tester_id: 1, + campaign_id: 1, + amount: 100.5, + is_paid: 1, + is_requested: 1, + work_type: "test", + note: "note", + receipt_id: -1, + work_type_id: 1, +}; + +const payment_2 = { + ...payment_1, + id: 22, + tester_id: 2, + campaign_id: 1, + amount: 200.75, + is_paid: 1, + is_requested: 1, + work_type_id: 2, +}; + +const payment_3 = { + ...payment_1, + id: 33, + tester_id: 3, + campaign_id: 2, + amount: 300.25, + is_paid: 1, + is_requested: 1, +}; + +describe("GET /dossiers/campaignId/costs", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert([ + campaign_1, + campaign_2, + campaign_3, + ]); + await tryber.tables.WpAppqProject.do().insert([ + { + ...project, + display_name: "Project 1", + id: 1, + customer_id: 1, + }, + { + ...project, + display_name: "Project 3", + id: 3, + customer_id: 2, + }, + ]); + await tryber.tables.WpAppqPayment.do().insert([ + payment_1, + payment_2, + payment_3, + ]); + }); + afterAll(async () => { + await tryber.tables.WpAppqCustomer.do().delete(); + }); + + it("Should answer 403 if not logged in", () => { + return request(app).get("/dossiers/1/costs").expect(403); + }); + it("Should answer 403 if logged in without permissions", async () => { + const response = await request(app) + .get("/dossiers/1/costs") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(403); + }); + it("Should answer 400 if campaign does not exists", async () => { + const response = await request(app) + .get("/dossiers/100/costs") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(400); + }); + + it("Should answer 403 if logged as user without permissions on the campaign", async () => { + const response = await request(app) + .get("/dossiers/2/costs") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(403); + }); + + it("Should answer 200 if logged as user with full access on the campaign", async () => { + const response = await request(app) + .get("/dossiers/1/costs") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(200); + }); + + it("Should answer with the campaigns costs", async () => { + const response = await request(app) + .get("/dossiers/1/costs") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(200); + expect(response.body).toEqual({ + totalCost: payment_1.amount + payment_2.amount, + }); + }); + it("Should filterBy the costs by work_type_id", async () => { + const response = await request(app) + .get("/dossiers/1/costs?filterBy[type]=2") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(200); + expect(response.body).toEqual({ + totalCost: payment_2.amount, + }); + }); + + it("Should filterBy the costs by multiple work_type_id", async () => { + const response = await request(app) + .get("/dossiers/1/costs?filterBy[type]=1,2") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(200); + expect(response.body).toEqual({ + totalCost: payment_1.amount + payment_2.amount, + }); + }); + + it("Should ignore the filter if it the type is invalid", async () => { + const response = await request(app) + .get("/dossiers/1/costs?filterBy[type]=invalid") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(200); + expect(response.body).toEqual({ + totalCost: payment_1.amount + payment_2.amount, + }); + }); + it("Should ignore the filter if it the filter is invalid", async () => { + const response = await request(app) + .get("/dossiers/1/costs?filterBy[invalid]=1") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(200); + expect(response.body).toEqual({ + totalCost: payment_1.amount + payment_2.amount, + }); + }); +}); diff --git a/src/routes/dossiers/campaignId/costs/_get/index.ts b/src/routes/dossiers/campaignId/costs/_get/index.ts new file mode 100644 index 000000000..006d7329a --- /dev/null +++ b/src/routes/dossiers/campaignId/costs/_get/index.ts @@ -0,0 +1,86 @@ +/** OPENAPI-CLASS : get-dossiers-campaign-costs */ + +import OpenapiError from "@src/features/OpenapiError"; +import { tryber } from "@src/features/database"; +import CampaignRoute from "@src/features/routes/CampaignRoute"; + +export default class RouteItem extends CampaignRoute<{ + response: StoplightOperations["get-dossiers-campaign-costs"]["responses"]["200"]["content"]["application/json"]; + parameters: StoplightOperations["get-dossiers-campaign-costs"]["parameters"]["path"]; + query: StoplightOperations["get-dossiers-campaign-costs"]["parameters"]["query"]; +}> { + private campaignId: number; + private filterBy: { + type?: string | string[]; + } = {}; + + constructor(configuration: RouteClassConfiguration) { + super(configuration); + const query = this.getQuery(); + this.campaignId = Number(this.getParameters().campaign); + if (query.filterBy) { + this.filterBy = query.filterBy; + } + } + + protected async filter() { + if (!(await super.filter())) return false; + + if (!this.hasAccessToCampaign(this.campaignId)) { + this.setError(403, new OpenapiError("You are not authorized to do this")); + return false; + } + + if (!(await this.campaignExists())) { + this.setError(403, new OpenapiError("Campaign does not exist")); + return false; + } + + return true; + } + + private async campaignExists(): Promise { + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select("id") + .where({ + id: this.campaignId, + }) + .first(); + if (!campaign) return false; + + return true; + } + + protected async prepare(): Promise { + const cost = await this.calculateTotalCost(); + return this.setSuccess(200, cost); + } + + private async calculateTotalCost() { + let query = tryber.tables.WpAppqPayment.do().where({ + campaign_id: this.campaignId, + }); + + if (this.filterBy && this.filterBy.type !== undefined) { + const rawTypes = Array.isArray(this.filterBy.type) + ? this.filterBy.type + : [this.filterBy.type]; + + const types = rawTypes + .map((t) => Number(t)) + .filter((t) => !Number.isNaN(t)); + + if (types.length > 0) { + query = query.whereIn("work_type_id", types); + } + } + + const paymentsTotal = (await query + .sum("amount as totalAmount") + .first()) as unknown as { totalAmount: string | number | null }; + + return { + totalCost: paymentsTotal ? Number(paymentsTotal.totalAmount || 0) : 0, + }; + } +} diff --git a/src/schema.ts b/src/schema.ts index 8e74a7907..66f4617a9 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -395,6 +395,14 @@ export interface paths { }; }; }; + "/dossiers/{campaign}/costs": { + get: operations["get-dossiers-campaign-costs"]; + parameters: { + path: { + campaign: string; + }; + }; + }; "/education": { /** Get all education levels */ get: operations["get-education"]; @@ -3229,6 +3237,27 @@ export interface operations { }; }; }; + "get-dossiers-campaign-costs": { + parameters: { + path: { + campaign: string; + }; + query: { + /** Key-value Array for item filtering */ + filterBy?: components["parameters"]["filterBy"]; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + totalCost?: number; + }; + }; + }; + }; + }; /** Get all education levels */ "get-education": { responses: {