From dee0247cdfa64fdba449a3e0c59fa484075c40ff Mon Sep 17 00:00:00 2001 From: "it@app-quality.com" Date: Mon, 2 Feb 2026 11:04:30 +0100 Subject: [PATCH 1/6] Modified src/reference/openapi.yml --- src/reference/openapi.yml | 78 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index bfd704803..6fa770306 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -13451,6 +13451,11 @@ paths: name: campaign in: path required: true + - schema: + type: string + name: campaign + in: path + required: true get: summary: Your GET endpoint tags: [] @@ -13553,6 +13558,79 @@ paths: id: 9hp8r67rwl59d security: - JWT: [] + post: + summary: Your POST endpoint + tags: [] + responses: + '201': + description: Created + '400': + description: Bad Request + '403': + $ref: '#/components/responses/NotAuthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + description: Internal Server Error + operationId: post-campaigns-campaign-finance-otherCosts + x-stoplight: + id: aujq76gdkus39 + description: Create a new campaign cost + requestBody: + content: + application/json: + schema: + type: object + x-examples: + Example 1: + description: Riparazione hardware ufficio + type_id: 3 + supplier_id: 105 + cost: 250.5 + attachments: + - url: 'https://esempio.com/documenti/fattura.pdf' + mime_type: application/pdf + - url: 'https://esempio.com/immagini/danno.jpg' + mime_type: image/jpeg + required: + - description + - type_id + - supplier_id + - cost + - attachments + properties: + description: + type: string + type_id: + type: integer + supplier_id: + type: integer + cost: + type: number + attachments: + type: array + items: + type: object + required: + - url + - mime_type + properties: + url: + type: string + mime_type: + type: string + examples: + Example 1: + value: + description: Riparazione hardware ufficio + type_id: 3 + supplier_id: 105 + cost: 250.5 + attachments: + - url: 'https://esempio.com/documenti/fattura.pdf' + mime_type: application/pdf + - url: 'https://esempio.com/immagini/danno.jpg' + mime_type: image/jpeg servers: - url: 'https://api.app-quality.com' tags: From ca68578ffad66aef7ac317ee185d4b967db921c3 Mon Sep 17 00:00:00 2001 From: "it@app-quality.com" Date: Mon, 2 Feb 2026 11:09:50 +0100 Subject: [PATCH 2/6] Modified src/reference/openapi.yml --- src/reference/openapi.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 6fa770306..8f28384df 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -13451,11 +13451,6 @@ paths: name: campaign in: path required: true - - schema: - type: string - name: campaign - in: path - required: true get: summary: Your GET endpoint tags: [] From 63bb92a8913768953677d2a651c099727f0731ca Mon Sep 17 00:00:00 2001 From: "it@app-quality.com" Date: Mon, 2 Feb 2026 11:10:41 +0100 Subject: [PATCH 3/6] Modified src/reference/openapi.yml --- src/reference/openapi.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 8f28384df..851c42a12 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -13446,11 +13446,7 @@ paths: - JWT: [] '/campaigns/{campaign}/finance/otherCosts': parameters: - - schema: - type: string - name: campaign - in: path - required: true + - $ref: '#/components/parameters/campaign' get: summary: Your GET endpoint tags: [] From 3089b5811a78f823c87f803d6bc07a91e40c0441 Mon Sep 17 00:00:00 2001 From: Kariamos Date: Mon, 2 Feb 2026 11:13:19 +0100 Subject: [PATCH 4/6] feat: add endpoint to create new campaign cost and update campaign parameter type --- src/schema.ts | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/schema.ts b/src/schema.ts index 1a508ac4e..09a9f787f 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -824,9 +824,12 @@ export interface paths { }; "/campaigns/{campaign}/finance/otherCosts": { get: operations["get-campaigns-campaign-finance-otherCosts"]; + /** Create a new campaign cost */ + post: operations["post-campaigns-campaign-finance-otherCosts"]; parameters: { path: { - campaign: string; + /** A campaign id */ + campaign: components["parameters"]["campaign"]; }; }; }; @@ -5625,7 +5628,8 @@ export interface operations { "get-campaigns-campaign-finance-otherCosts": { parameters: { path: { - campaign: string; + /** A campaign id */ + campaign: components["parameters"]["campaign"]; }; }; responses: { @@ -5661,6 +5665,39 @@ export interface operations { 500: unknown; }; }; + /** Create a new campaign cost */ + "post-campaigns-campaign-finance-otherCosts": { + parameters: { + path: { + /** A campaign id */ + campaign: components["parameters"]["campaign"]; + }; + }; + responses: { + /** Created */ + 201: unknown; + /** Bad Request */ + 400: unknown; + 403: components["responses"]["NotAuthorized"]; + 404: components["responses"]["NotFound"]; + /** Internal Server Error */ + 500: unknown; + }; + requestBody: { + content: { + "application/json": { + description: string; + type_id: number; + supplier_id: number; + cost: number; + attachments: { + url: string; + mime_type: string; + }[]; + }; + }; + }; + }; } export interface external {} From 4eac84125bfb3c835a0debe5806eaf627d7911ec Mon Sep 17 00:00:00 2001 From: Kariamos Date: Mon, 2 Feb 2026 11:28:49 +0100 Subject: [PATCH 5/6] feat: add security definition for JWT in OpenAPI specification --- src/reference/openapi.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 851c42a12..5651f404c 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -13622,6 +13622,8 @@ paths: mime_type: application/pdf - url: 'https://esempio.com/immagini/danno.jpg' mime_type: image/jpeg + security: + - JWT: [] servers: - url: 'https://api.app-quality.com' tags: From b4a6458d7d31993cdde8867098886e3525a51edb Mon Sep 17 00:00:00 2001 From: Kariamos Date: Mon, 2 Feb 2026 11:38:10 +0100 Subject: [PATCH 6/6] feat: implement POST endpoint for campaign other costs with validation and attachment handling --- .../finance/otherCosts/_post/index.spec.ts | 501 ++++++++++++++++++ .../finance/otherCosts/_post/index.ts | 133 +++++ 2 files changed, 634 insertions(+) create mode 100644 src/routes/campaigns/campaignId/finance/otherCosts/_post/index.spec.ts create mode 100644 src/routes/campaigns/campaignId/finance/otherCosts/_post/index.ts diff --git a/src/routes/campaigns/campaignId/finance/otherCosts/_post/index.spec.ts b/src/routes/campaigns/campaignId/finance/otherCosts/_post/index.spec.ts new file mode 100644 index 000000000..ce842050b --- /dev/null +++ b/src/routes/campaigns/campaignId/finance/otherCosts/_post/index.spec.ts @@ -0,0 +1,501 @@ +import request from "supertest"; +import app from "@src/app"; +import { tryber } from "@src/features/database"; + +describe("POST /campaigns/campaignId/finance/otherCosts", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdProfile.do().insert([ + { + id: 1, + name: "John", + surname: "Doe", + wp_user_id: 1, + email: "", + employment_id: 1, + education_id: 1, + }, + { + id: 2, + name: "Jane", + surname: "Doe", + wp_user_id: 2, + email: "", + employment_id: 1, + education_id: 1, + }, + ]); + await tryber.tables.WpUsers.do().insert([{ ID: 1 }, { ID: 2 }]); + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { + id: 1, + platform_id: 1, + start_date: "2020-01-01", + end_date: "2020-01-01", + title: "This is the title", + page_preview_id: 1, + page_manual_id: 1, + customer_id: 1, + pm_id: 1, + project_id: 1, + customer_title: "", + }, + { + id: 2, + platform_id: 1, + start_date: "2020-01-01", + end_date: "2020-01-01", + title: "Another campaign", + page_preview_id: 1, + page_manual_id: 1, + customer_id: 1, + pm_id: 1, + project_id: 1, + customer_title: "", + }, + ]); + await tryber.tables.WpAppqCampaignOtherCostsType.do().insert([ + { + id: 1, + name: "Type 1", + }, + { + id: 2, + name: "Type 2", + }, + { + id: 3, + name: "Type 3", + }, + ]); + await tryber.tables.WpAppqCampaignOtherCostsSupplier.do().insert([ + { + id: 1, + name: "Supplier 1", + created_by: 1, + created_on: "2024-01-01 10:00:00", + }, + { + id: 2, + name: "Supplier 2", + created_by: 1, + created_on: "2024-01-02 11:00:00", + }, + { + id: 105, + name: "Supplier 105", + created_by: 1, + created_on: "2024-01-03 12:00:00", + }, + ]); + }); + + afterEach(async () => { + await tryber.tables.WpAppqCampaignOtherCostsAttachment.do().delete(); + await tryber.tables.WpAppqCampaignOtherCosts.do().delete(); + }); + + afterAll(async () => { + await tryber.tables.WpAppqCampaignOtherCostsSupplier.do().delete(); + await tryber.tables.WpAppqCampaignOtherCostsType.do().delete(); + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpUsers.do().delete(); + await tryber.tables.WpAppqEvdProfile.do().delete(); + }); + + const validPayload = { + description: "Riparazione hardware ufficio", + type_id: 3, + supplier_id: 105, + cost: 250.5, + attachments: [ + { + url: "https://esempio.com/documenti/fattura.pdf", + mime_type: "application/pdf", + }, + { + url: "https://esempio.com/immagini/danno.jpg", + mime_type: "image/jpeg", + }, + ], + }; + + describe("Not enough permissions", () => { + it("Should return 403 if logged out", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send(validPayload); + expect(response.status).toBe(403); + }); + + it("Should return 403 if logged in as not admin user", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send(validPayload) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(403); + }); + + it("Should return 403 if no access to the campaign", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send(validPayload) + .set("Authorization", 'Bearer tester olp {"appq_campaign":[2]}'); + expect(response.status).toBe(403); + }); + }); + + describe("Validation errors", () => { + it("Should return 400 if description is empty", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send({ ...validPayload, description: "" }) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(400); + expect(response.body).toEqual( + expect.objectContaining({ + message: "Description should not be empty", + }) + ); + }); + + it("Should return 400 if description is only whitespace", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send({ ...validPayload, description: " " }) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(400); + expect(response.body).toEqual( + expect.objectContaining({ + message: "Description should not be empty", + }) + ); + }); + + it("Should return 400 if cost is 0", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send({ ...validPayload, cost: 0 }) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(400); + expect(response.body).toEqual( + expect.objectContaining({ + message: "Cost must be greater than 0", + }) + ); + }); + + it("Should return 400 if cost is negative", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send({ ...validPayload, cost: -10 }) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(400); + expect(response.body).toEqual( + expect.objectContaining({ + message: "Cost must be greater than 0", + }) + ); + }); + + it("Should return 400 if type_id does not exist", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send({ ...validPayload, type_id: 999 }) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(400); + expect(response.body).toEqual( + expect.objectContaining({ + message: "Type not found", + }) + ); + }); + + it("Should return 400 if supplier_id does not exist", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send({ ...validPayload, supplier_id: 999 }) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(400); + expect(response.body).toEqual( + expect.objectContaining({ + message: "Supplier not found", + }) + ); + }); + + it("Should return 400 if attachments array is empty", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send({ ...validPayload, attachments: [] }) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(400); + expect(response.body).toEqual( + expect.objectContaining({ + message: "At least one attachment is required", + }) + ); + }); + + it("Should return 400 if attachment url is empty", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send({ + ...validPayload, + attachments: [ + { + url: "", + mime_type: "application/pdf", + }, + ], + }) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(400); + expect(response.body).toEqual( + expect.objectContaining({ + message: "Attachment URL is required", + }) + ); + }); + + it("Should return 400 if attachment mime_type is empty", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send({ + ...validPayload, + attachments: [ + { + url: "https://esempio.com/documenti/fattura.pdf", + mime_type: "", + }, + ], + }) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(400); + expect(response.body).toEqual( + expect.objectContaining({ + message: "Attachment mime_type is required", + }) + ); + }); + }); + + describe("Success - admin permissions", () => { + it("Should return 201 if logged in as admin", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send(validPayload) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(201); + }); + + it("Should create other cost in database", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send(validPayload) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(201); + + const getResponse = await request(app) + .get("/campaigns/1/finance/otherCosts") + .set("Authorization", "Bearer admin"); + expect(getResponse.status).toBe(200); + expect(getResponse.body.items).toHaveLength(1); + expect(getResponse.body.items[0]).toEqual( + expect.objectContaining({ + description: "Riparazione hardware ufficio", + type: { name: "Type 3", id: 3 }, + supplier: { name: "Supplier 105", id: 105 }, + }) + ); + }); + + it("Should create attachments in database", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send(validPayload) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(201); + + const getResponse = await request(app) + .get("/campaigns/1/finance/otherCosts") + .set("Authorization", "Bearer admin"); + expect(getResponse.status).toBe(200); + expect(getResponse.body.items[0].attachments).toHaveLength(2); + expect(getResponse.body.items[0].attachments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + url: "https://esempio.com/documenti/fattura.pdf", + mimetype: "application/pdf", + }), + expect.objectContaining({ + url: "https://esempio.com/immagini/danno.jpg", + mimetype: "image/jpeg", + }), + ]) + ); + }); + + it("Should create cost with single attachment", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send({ + ...validPayload, + attachments: [ + { + url: "https://esempio.com/documenti/fattura.pdf", + mime_type: "application/pdf", + }, + ], + }) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(201); + + const getResponse = await request(app) + .get("/campaigns/1/finance/otherCosts") + .set("Authorization", "Bearer admin"); + expect(getResponse.status).toBe(200); + expect(getResponse.body.items[0].attachments).toHaveLength(1); + }); + + it("Should create cost with multiple attachments", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send({ + ...validPayload, + attachments: [ + { + url: "https://esempio.com/documenti/fattura1.pdf", + mime_type: "application/pdf", + }, + { + url: "https://esempio.com/documenti/fattura2.pdf", + mime_type: "application/pdf", + }, + { + url: "https://esempio.com/immagini/danno.jpg", + mime_type: "image/jpeg", + }, + ], + }) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(201); + + const getResponse = await request(app) + .get("/campaigns/1/finance/otherCosts") + .set("Authorization", "Bearer admin"); + expect(getResponse.status).toBe(200); + expect(getResponse.body.items[0].attachments).toHaveLength(3); + }); + + it("Should create multiple costs independently", async () => { + const response1 = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send(validPayload) + .set("Authorization", "Bearer admin"); + expect(response1.status).toBe(201); + + const response2 = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send({ + ...validPayload, + description: "Second cost", + cost: 100.0, + }) + .set("Authorization", "Bearer admin"); + expect(response2.status).toBe(201); + + const getResponse = await request(app) + .get("/campaigns/1/finance/otherCosts") + .set("Authorization", "Bearer admin"); + expect(getResponse.status).toBe(200); + expect(getResponse.body.items).toHaveLength(2); + }); + + it("Should accept decimal cost values", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send({ ...validPayload, cost: 123.456 }) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(201); + + const getResponse = await request(app) + .get("/campaigns/1/finance/otherCosts") + .set("Authorization", "Bearer admin"); + expect(getResponse.status).toBe(200); + expect(getResponse.body.items[0]).toBeDefined(); + }); + }); + + describe("Success - olp permissions", () => { + it("Should return 201 if logged in as olp with access to campaign", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send(validPayload) + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(201); + }); + + it("Should create other cost in database with olp permissions", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send(validPayload) + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(201); + + const getResponse = await request(app) + .get("/campaigns/1/finance/otherCosts") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(getResponse.status).toBe(200); + expect(getResponse.body.items).toHaveLength(1); + expect(getResponse.body.items[0]).toEqual( + expect.objectContaining({ + description: "Riparazione hardware ufficio", + }) + ); + }); + + it("Should create attachments with olp permissions", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send(validPayload) + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(201); + + const getResponse = await request(app) + .get("/campaigns/1/finance/otherCosts") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(getResponse.status).toBe(200); + expect(getResponse.body.items[0].attachments).toHaveLength(2); + }); + + it("Should return 403 if olp does not have access to campaign", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send(validPayload) + .set("Authorization", 'Bearer tester olp {"appq_campaign":[2]}'); + expect(response.status).toBe(403); + }); + }); + + describe("Campaign isolation", () => { + it("Should create cost only for specified campaign", async () => { + const response = await request(app) + .post("/campaigns/1/finance/otherCosts") + .send(validPayload) + .set("Authorization", "Bearer admin"); + expect(response.status).toBe(201); + + const getResponse1 = await request(app) + .get("/campaigns/1/finance/otherCosts") + .set("Authorization", "Bearer admin"); + expect(getResponse1.status).toBe(200); + expect(getResponse1.body.items).toHaveLength(1); + + const getResponse2 = await request(app) + .get("/campaigns/2/finance/otherCosts") + .set("Authorization", "Bearer admin"); + expect(getResponse2.status).toBe(200); + expect(getResponse2.body.items).toHaveLength(0); + }); + }); +}); diff --git a/src/routes/campaigns/campaignId/finance/otherCosts/_post/index.ts b/src/routes/campaigns/campaignId/finance/otherCosts/_post/index.ts new file mode 100644 index 000000000..537d1d4ca --- /dev/null +++ b/src/routes/campaigns/campaignId/finance/otherCosts/_post/index.ts @@ -0,0 +1,133 @@ +/** OPENAPI-CLASS: post-campaigns-campaign-finance-otherCosts */ + +import CampaignRoute from "@src/features/routes/CampaignRoute"; +import { tryber } from "@src/features/database"; +import OpenapiError from "@src/features/OpenapiError"; + +export default class OtherCostsPostRoute extends CampaignRoute<{ + response: StoplightOperations["post-campaigns-campaign-finance-otherCosts"]["responses"]["201"]; + parameters: StoplightOperations["post-campaigns-campaign-finance-otherCosts"]["parameters"]["path"]; + body: StoplightOperations["post-campaigns-campaign-finance-otherCosts"]["requestBody"]["content"]["application/json"]; +}> { + protected async filter(): Promise { + if (!(await super.filter())) return false; + + if (!this.hasAccessToCampaign(this.cp_id)) { + this.setError(403, new OpenapiError("Access denied")); + return false; + } + + const body = this.getBody(); + + // Validate description + if (!body.description || body.description.trim() === "") { + this.setError(400, new OpenapiError("Description should not be empty")); + return false; + } + + // Validate cost + if (body.cost <= 0) { + this.setError(400, new OpenapiError("Cost must be greater than 0")); + return false; + } + + // Validate type_id exists + if (!(await this.typeExists(body.type_id))) { + this.setError(400, new OpenapiError("Type not found")); + return false; + } + + // Validate supplier_id exists + if (!(await this.supplierExists(body.supplier_id))) { + this.setError(400, new OpenapiError("Supplier not found")); + return false; + } + + // Validate attachments + if (!body.attachments || body.attachments.length === 0) { + this.setError( + 400, + new OpenapiError("At least one attachment is required") + ); + return false; + } + + for (const attachment of body.attachments) { + if (!attachment.url || attachment.url.trim() === "") { + this.setError(400, new OpenapiError("Attachment URL is required")); + return false; + } + if (!attachment.mime_type || attachment.mime_type.trim() === "") { + this.setError( + 400, + new OpenapiError("Attachment mime_type is required") + ); + return false; + } + } + + return true; + } + + protected async prepare(): Promise { + try { + const body = this.getBody(); + const costId = await this.createOtherCost(body); + await this.createAttachments(costId, body.attachments); + + return this.setSuccess(201, undefined); + } catch (e) { + console.error("Error creating other cost: ", e); + return this.setError(500, new OpenapiError("Error creating other cost")); + } + } + + private async createOtherCost( + body: StoplightOperations["post-campaigns-campaign-finance-otherCosts"]["requestBody"]["content"]["application/json"] + ): Promise { + const result = await tryber.tables.WpAppqCampaignOtherCosts.do() + .insert({ + campaign_id: this.cp_id, + description: body.description, + cost: body.cost, + type_id: body.type_id, + supplier_id: body.supplier_id, + }) + .returning("id"); + + const id = result[0]?.id ?? result[0]; + + if (!id) throw new Error("Error creating other cost"); + + return id; + } + + private async createAttachments( + costId: number, + attachments: { url: string; mime_type: string }[] + ): Promise { + const attachmentsData = attachments.map((attachment) => ({ + cost_id: costId, + url: attachment.url, + mime_type: attachment.mime_type, + })); + + await tryber.tables.WpAppqCampaignOtherCostsAttachment.do().insert( + attachmentsData + ); + } + + private async typeExists(typeId: number): Promise { + const type = await tryber.tables.WpAppqCampaignOtherCostsType.do() + .where({ id: typeId }) + .first(); + return type !== undefined; + } + + private async supplierExists(supplierId: number): Promise { + const supplier = await tryber.tables.WpAppqCampaignOtherCostsSupplier.do() + .where({ id: supplierId }) + .first(); + return supplier !== undefined; + } +}