From 9f41f08132d7c7fed573da79ef150edc5307525b Mon Sep 17 00:00:00 2001 From: Laurent Paoletti Date: Tue, 28 Jan 2025 11:29:31 +0100 Subject: [PATCH] Transporter plates restriction --- .../bsda/__tests__/registry.integration.ts | 50 ++----- back/src/bsda/typeDefs/bsda.inputs.graphql | 2 +- .../__tests__/validation.integration.ts | 131 ++++++++++++++++- back/src/bsdasris/__tests__/factories.ts | 2 +- .../__tests__/registry.integration.ts | 18 +-- .../__tests__/validation.integration.ts | 2 +- .../__tests__/duplicateBsdasri.integration.ts | 30 ++++ .../resolvers/mutations/duplicateBsdasri.ts | 1 + .../bsdasris/typeDefs/bsdasri.inputs.graphql | 2 +- back/src/bsdasris/validation.ts | 31 +++- .../__tests__/bsds.bsdasri.integration.ts | 2 +- .../__tests__/bsds.bspaoh.integration.ts | 2 +- back/src/bsffs/__tests__/factories.ts | 2 +- .../bsffs/__tests__/registry.integration.ts | 50 ++----- back/src/bsffs/typeDefs/bsff.inputs.graphql | 2 +- .../bsff/__tests__/validation.integration.ts | 92 +++++++++++- .../bspaoh/__tests__/registry.integration.ts | 18 +-- .../__tests__/duplicateBspaoh.integration.ts | 52 +++++++ .../bspaoh/resolvers/mutations/duplicate.ts | 1 + .../src/bspaoh/typeDefs/bspaoh.inputs.graphql | 2 +- .../__tests__/validation.integration.ts | 133 +++++++++++++++++- back/src/bspaoh/validation/schema.ts | 41 ++---- .../bsvhu/__tests__/registry.integration.ts | 12 +- back/src/bsvhu/validation/refinements.ts | 36 ----- back/src/bsvhu/validation/schema.ts | 14 +- back/src/common/validation/messages.ts | 7 + back/src/common/validation/zod/schema.ts | 32 ++++- .../forms/__tests__/registry.integration.ts | 50 ++----- .../forms/__tests__/validation.integration.ts | 125 +++++++++++++++- .../signTransportForm.integration.ts | 20 ++- back/src/forms/typeDefs/bsdd.inputs.graphql | 6 +- back/src/forms/validation.ts | 52 +++++++ 32 files changed, 778 insertions(+), 242 deletions(-) create mode 100644 back/src/common/validation/messages.ts diff --git a/back/src/bsda/__tests__/registry.integration.ts b/back/src/bsda/__tests__/registry.integration.ts index f9cb1daa53..bebe82e603 100644 --- a/back/src/bsda/__tests__/registry.integration.ts +++ b/back/src/bsda/__tests__/registry.integration.ts @@ -76,31 +76,31 @@ const createBsdaWith5Transporters = async () => { data: [ { transporterCompanySiret: transporter.company.siret, - transporterTransportPlates: ["TRANSPORTER1-NBR-PLATES"], + transporterTransportPlates: ["TR-01-AA"], transporterCompanyAddress: transporter.company.address, number: 1 }, { transporterCompanySiret: transporter2.company.siret, - transporterTransportPlates: ["TRANSPORTER2-NBR-PLATES"], + transporterTransportPlates: ["TR-02-AA"], transporterCompanyAddress: transporter2.company.address, number: 2 }, { transporterCompanySiret: transporter3.company.siret, - transporterTransportPlates: ["TRANSPORTER3-NBR-PLATES"], + transporterTransportPlates: ["TR-03-AA"], transporterCompanyAddress: transporter3.company.address, number: 3 }, { transporterCompanySiret: transporter4.company.siret, - transporterTransportPlates: ["TRANSPORTER4-NBR-PLATES"], + transporterTransportPlates: ["TR-04-AA"], transporterCompanyAddress: transporter4.company.address, number: 4 }, { transporterCompanyVatNumber: transporter5.company.vatNumber, - transporterTransportPlates: ["TRANSPORTER5-NBR-PLATES"], + transporterTransportPlates: ["TR-05-AA"], transporterCompanyAddress: transporter5.company.address, number: 5 } @@ -658,29 +658,19 @@ describe("toTransportedWaste", () => { // Then expect(waste.transporterCompanySiret).toBe(data.transporter1.siret); - expect(waste.transporterNumberPlates).toStrictEqual([ - "TRANSPORTER1-NBR-PLATES" - ]); + expect(waste.transporterNumberPlates).toStrictEqual(["TR-01-AA"]); expect(waste.transporter2CompanySiret).toBe(data.transporter2.siret); - expect(waste.transporter2NumberPlates).toStrictEqual([ - "TRANSPORTER2-NBR-PLATES" - ]); + expect(waste.transporter2NumberPlates).toStrictEqual(["TR-02-AA"]); expect(waste.transporter3CompanySiret).toBe(data.transporter3.siret); - expect(waste.transporter3NumberPlates).toStrictEqual([ - "TRANSPORTER3-NBR-PLATES" - ]); + expect(waste.transporter3NumberPlates).toStrictEqual(["TR-03-AA"]); expect(waste.transporter4CompanySiret).toBe(data.transporter4.siret); - expect(waste.transporter4NumberPlates).toStrictEqual([ - "TRANSPORTER4-NBR-PLATES" - ]); + expect(waste.transporter4NumberPlates).toStrictEqual(["TR-04-AA"]); expect(waste.transporter5CompanySiret).toBe(data.transporter5.vatNumber); - expect(waste.transporter5NumberPlates).toStrictEqual([ - "TRANSPORTER5-NBR-PLATES" - ]); + expect(waste.transporter5NumberPlates).toStrictEqual(["TR-05-AA"]); }); }); @@ -1018,29 +1008,19 @@ describe("toAllWaste", () => { // Then expect(waste.transporterCompanySiret).toBe(data.transporter1.siret); - expect(waste.transporterNumberPlates).toStrictEqual([ - "TRANSPORTER1-NBR-PLATES" - ]); + expect(waste.transporterNumberPlates).toStrictEqual(["TR-01-AA"]); expect(waste.transporter2CompanySiret).toBe(data.transporter2.siret); - expect(waste.transporter2NumberPlates).toStrictEqual([ - "TRANSPORTER2-NBR-PLATES" - ]); + expect(waste.transporter2NumberPlates).toStrictEqual(["TR-02-AA"]); expect(waste.transporter3CompanySiret).toBe(data.transporter3.siret); - expect(waste.transporter3NumberPlates).toStrictEqual([ - "TRANSPORTER3-NBR-PLATES" - ]); + expect(waste.transporter3NumberPlates).toStrictEqual(["TR-03-AA"]); expect(waste.transporter4CompanySiret).toBe(data.transporter4.siret); - expect(waste.transporter4NumberPlates).toStrictEqual([ - "TRANSPORTER4-NBR-PLATES" - ]); + expect(waste.transporter4NumberPlates).toStrictEqual(["TR-04-AA"]); expect(waste.transporter5CompanySiret).toBe(data.transporter5.vatNumber); - expect(waste.transporter5NumberPlates).toStrictEqual([ - "TRANSPORTER5-NBR-PLATES" - ]); + expect(waste.transporter5NumberPlates).toStrictEqual(["TR-05-AA"]); }); it("if forwarding BSD, should contain the info of the initial emitter", async () => { diff --git a/back/src/bsda/typeDefs/bsda.inputs.graphql b/back/src/bsda/typeDefs/bsda.inputs.graphql index 4f37d23df2..ce1ceaa1bf 100644 --- a/back/src/bsda/typeDefs/bsda.inputs.graphql +++ b/back/src/bsda/typeDefs/bsda.inputs.graphql @@ -367,7 +367,7 @@ input BsdaBrokerRecepisseInput { input BsdaTransportInput { "Mode de transport" mode: TransportMode - "Plaque(s) d'immatriculation - maximum 2" + "Plaque(s) d'immatriculation - maximum 2- 4 à 12 caractères." plates: [String!] "Date de prise en charge" takenOverAt: DateTime diff --git a/back/src/bsda/validation/__tests__/validation.integration.ts b/back/src/bsda/validation/__tests__/validation.integration.ts index 8ddd85b836..a3ed70bdd3 100644 --- a/back/src/bsda/validation/__tests__/validation.integration.ts +++ b/back/src/bsda/validation/__tests__/validation.integration.ts @@ -124,6 +124,24 @@ describe("BSDA parsing", () => { expect(parsed).toBeDefined(); }); + test("when transporter plate is not present and transport mode is not ROAD before TRANSPORT signature", () => { + const data: ZodBsda = { + ...bsda, + transporters: [ + { + ...bsda.transporters![0], + transporterTransportPlates: [], + transporterTransportMode: "ROAD" as TransportMode + } + ] + }; + const parsed = parseBsda(data, { + ...context, + currentSignatureType: "EMISSION" + }); + expect(parsed).toBeDefined(); + }); + test("when transporter plate is not present and transport mode is not ROAD", () => { const data: ZodBsda = { ...bsda, @@ -149,7 +167,7 @@ describe("BSDA parsing", () => { { ...bsda.transporters![0], transporterTransportMode: "ROAD" as TransportMode, - transporterTransportPlates: ["TRANSPORTER-PLATES"] + transporterTransportPlates: ["AZ-12-BA"] } ] }; @@ -448,8 +466,8 @@ describe("BSDA parsing", () => { } }); - test.each([undefined, [], [""]])( - "when transporter plate is %p invalid and transporter mode is ROAD", + test.each([undefined, []])( + "when transporter plate is %p and transporter mode is ROAD", async invalidValue => { const data: ZodBsda = { ...bsda, @@ -477,6 +495,113 @@ describe("BSDA parsing", () => { } ); + test("when transporter plate is an empty string and transporter mode is ROAD", async () => { + const data: ZodBsda = { + ...bsda, + transporters: [ + { + ...bsda.transporters![0], + transporterTransportMode: "ROAD" as TransportMode, + transporterTransportPlates: [""] + } + ] + }; + + try { + parseBsda(data, { + ...context, + currentSignatureType: "TRANSPORT" + }); + } catch (err) { + expect((err as ZodError).issues).toEqual([ + expect.objectContaining({ + message: + "Le numéro d'immatriculation doit faire entre 4 et 12 caractères" + }) + ]); + } + }); + + test("when transporter plate is too short and transporter mode is ROAD", async () => { + const data: ZodBsda = { + ...bsda, + transporters: [ + { + ...bsda.transporters![0], + transporterTransportMode: "ROAD" as TransportMode, + transporterTransportPlates: ["x"] + } + ] + }; + + try { + parseBsda(data, { + ...context, + currentSignatureType: "TRANSPORT" + }); + } catch (err) { + expect((err as ZodError).issues).toEqual([ + expect.objectContaining({ + message: + "Le numéro d'immatriculation doit faire entre 4 et 12 caractères" + }) + ]); + } + }); + + test("when transporter plate is too long and transporter mode is ROAD", async () => { + const data: ZodBsda = { + ...bsda, + transporters: [ + { + ...bsda.transporters![0], + transporterTransportMode: "ROAD" as TransportMode, + transporterTransportPlates: ["AZ-12-ER-98-AA-12"] + } + ] + }; + + try { + parseBsda(data, { + ...context, + currentSignatureType: "TRANSPORT" + }); + } catch (err) { + expect((err as ZodError).issues).toEqual([ + expect.objectContaining({ + message: + "Le numéro d'immatriculation doit faire entre 4 et 12 caractères" + }) + ]); + } + }); + + test("when transporter plate only contains whitespace and transporter mode is ROAD", async () => { + const data: ZodBsda = { + ...bsda, + transporters: [ + { + ...bsda.transporters![0], + transporterTransportMode: "ROAD" as TransportMode, + transporterTransportPlates: [" "] + } + ] + }; + + try { + parseBsda(data, { + ...context, + currentSignatureType: "TRANSPORT" + }); + } catch (err) { + expect((err as ZodError).issues).toEqual([ + expect.objectContaining({ + message: "Le numéro de plaque fourni est incorrect" + }) + ]); + } + }); + test("when the grouped waste code is not equal to the grouping BSDA waste code", async () => { const grouping = [ await bsdaFactory({ diff --git a/back/src/bsdasris/__tests__/factories.ts b/back/src/bsdasris/__tests__/factories.ts index 2e842e02b3..5820140698 100644 --- a/back/src/bsdasris/__tests__/factories.ts +++ b/back/src/bsdasris/__tests__/factories.ts @@ -122,7 +122,7 @@ export const readyToTakeOverData = company => ({ transporterRecepisseNumber: "xyz", transporterRecepisseDepartment: "83", transporterRecepisseValidityLimit: new Date(), - transporterTransportPlates: ["TRANSPORTER-PLATE"], + transporterTransportPlates: ["AB-65-ML"], transporterTransportMode: TransportMode.ROAD, transporterWastePackagings: [ { type: "BOITE_CARTON", volume: 22, quantity: 3 } diff --git a/back/src/bsdasris/__tests__/registry.integration.ts b/back/src/bsdasris/__tests__/registry.integration.ts index bc05ff7b5d..483225ac2e 100644 --- a/back/src/bsdasris/__tests__/registry.integration.ts +++ b/back/src/bsdasris/__tests__/registry.integration.ts @@ -148,7 +148,7 @@ describe("toIncomingWaste", () => { opt: { destinationCompanyMail: "destination@mail.com", transporterCompanySiret: transporter.siret, - transporterTransportPlates: ["TRANSPORTER-NBR-PLATES"] + transporterTransportPlates: ["TR-12-AA"] } }); @@ -364,7 +364,7 @@ describe("toOutgoingWaste", () => { opt: { destinationCompanyMail: "destination@mail.com", transporterCompanySiret: transporter.siret, - transporterTransportPlates: ["TRANSPORTER-NBR-PLATES"] + transporterTransportPlates: ["TR-12-AA"] } }); @@ -447,7 +447,7 @@ describe("toTransportedWaste", () => { opt: { destinationCompanyMail: "destination@mail.com", transporterCompanySiret: transporter.siret, - transporterTransportPlates: ["TRANSPORTER-NBR-PLATES"] + transporterTransportPlates: ["TR-12-AA"] } }); @@ -462,9 +462,7 @@ describe("toTransportedWaste", () => { expect(waste.transporterCompanySiret).toBe( dasriForRegistry.transporterCompanySiret ); - expect(waste.transporterNumberPlates).toStrictEqual([ - "TRANSPORTER-NBR-PLATES" - ]); + expect(waste.transporterNumberPlates).toStrictEqual(["TR-12-AA"]); expect(waste.transporter2CompanySiret).toBeNull(); expect(waste.transporter2NumberPlates).toBeNull(); @@ -514,7 +512,7 @@ describe("toManagedWaste", () => { opt: { destinationCompanyMail: "destination@mail.com", transporterCompanySiret: transporter.siret, - transporterTransportPlates: ["TRANSPORTER-NBR-PLATES"] + transporterTransportPlates: ["TR-12-AA"] } }); @@ -686,7 +684,7 @@ describe("toAllWaste", () => { opt: { destinationCompanyMail: "destination@mail.com", transporterCompanySiret: transporter.siret, - transporterTransportPlates: ["TRANSPORTER-NBR-PLATES"] + transporterTransportPlates: ["TR-12-AA"] } }); @@ -701,9 +699,7 @@ describe("toAllWaste", () => { expect(waste.transporterCompanySiret).toBe( dasriForRegistry.transporterCompanySiret ); - expect(waste.transporterNumberPlates).toStrictEqual([ - "TRANSPORTER-NBR-PLATES" - ]); + expect(waste.transporterNumberPlates).toStrictEqual(["TR-12-AA"]); expect(waste.transporter2CompanySiret).toBeNull(); expect(waste.transporter2NumberPlates).toBeNull(); diff --git a/back/src/bsdasris/__tests__/validation.integration.ts b/back/src/bsdasris/__tests__/validation.integration.ts index 0418989571..edf7407a9e 100644 --- a/back/src/bsdasris/__tests__/validation.integration.ts +++ b/back/src/bsdasris/__tests__/validation.integration.ts @@ -140,7 +140,7 @@ describe("Mutation.signBsdasri emission", () => { const data = { ...bsdasri, transporterTransportMode: "ROAD", - transporterTransportPlates: ["TRANSPORTER-PLATES"] + transporterTransportPlates: ["AB-12-ZE"] }; const validated = await validateBsdasri(data as any, { transportSignature: true diff --git a/back/src/bsdasris/resolvers/mutations/__tests__/duplicateBsdasri.integration.ts b/back/src/bsdasris/resolvers/mutations/__tests__/duplicateBsdasri.integration.ts index a5bbdd198d..0336672fc5 100644 --- a/back/src/bsdasris/resolvers/mutations/__tests__/duplicateBsdasri.integration.ts +++ b/back/src/bsdasris/resolvers/mutations/__tests__/duplicateBsdasri.integration.ts @@ -487,4 +487,34 @@ describe("Mutation.duplicateBsdasri", () => { expect(duplicatedDasri.destinationOperationCode).toBeNull(); expect(duplicatedDasri.destinationOperationMode).toBeNull(); }); + + it("should *not* duplicate transporter plates", async () => { + const { user, company } = await userWithCompanyFactory("MEMBER"); + + const dasri = await bsdasriFactory({ + opt: { + ...initialData(company), + transporterTransportPlates: ["AZ-12-RT"] + } + }); + + const { mutate } = makeClient(user); // emitter + + const { data } = await mutate>( + DUPLICATE_DASRI, + { + variables: { + id: dasri.id + } + } + ); + expect(data.duplicateBsdasri.status).toBe("INITIAL"); + expect(data.duplicateBsdasri.isDraft).toBe(true); + + const duplicatedDasri = await prisma.bsdasri.findFirstOrThrow({ + where: { id: data.duplicateBsdasri.id } + }); + + expect(duplicatedDasri.transporterTransportPlates).toStrictEqual([]); + }); }); diff --git a/back/src/bsdasris/resolvers/mutations/duplicateBsdasri.ts b/back/src/bsdasris/resolvers/mutations/duplicateBsdasri.ts index dfa6942df0..7b7ec77983 100644 --- a/back/src/bsdasris/resolvers/mutations/duplicateBsdasri.ts +++ b/back/src/bsdasris/resolvers/mutations/duplicateBsdasri.ts @@ -73,6 +73,7 @@ async function duplicateBsdasri(user: Express.User, bsdasri: Bsdasri) { transporterWasteWeightValue, transporterWasteWeightIsEstimate, transporterWasteVolume, + transporterTransportPlates, handedOverToRecipientAt, transportSignatoryId, transporterTransportSignatureDate, diff --git a/back/src/bsdasris/typeDefs/bsdasri.inputs.graphql b/back/src/bsdasris/typeDefs/bsdasri.inputs.graphql index 953b1dedee..d426b5a09f 100644 --- a/back/src/bsdasris/typeDefs/bsdasri.inputs.graphql +++ b/back/src/bsdasris/typeDefs/bsdasri.inputs.graphql @@ -191,7 +191,7 @@ input BsdasriTransportInput { "Mode de transport" acceptation: BsdasriAcceptationInput mode: TransportMode - "Plaque(s) d'immatriculation - maximum 2" + "Plaque(s) d'immatriculation - maximum 2- 4 à 12 caractères." plates: [String!] } diff --git a/back/src/bsdasris/validation.ts b/back/src/bsdasris/validation.ts index 5aacf6d1fa..09f2b84f26 100644 --- a/back/src/bsdasris/validation.ts +++ b/back/src/bsdasris/validation.ts @@ -33,6 +33,13 @@ import { WeightUnits, transporterRecepisseSchema } from "../common/validation"; + +import { onlyWhiteSpace } from "../common/validation/zod/schema"; +import { + ERROR_TRANSPORTER_PLATES_TOO_MANY, + ERROR_TRANSPORTER_PLATES_INCORRECT_LENGTH, + ERROR_TRANSPORTER_PLATES_INCORRECT_FORMAT +} from "../common/validation/messages"; import { destinationOperationModeValidation } from "../common/validation/operationMode"; import { isDefined } from "../common/helpers"; @@ -310,7 +317,7 @@ export const transporterSchema: FactorySchemaOf< transporterTransportPlates: yup .array() .of(yup.string()) - .max(2, "Un maximum de 2 plaques d'immatriculation est accepté") + .max(2, ERROR_TRANSPORTER_PLATES_TOO_MANY) .test((transporterTransportPlates, ctx) => { const { transporterTransportMode } = ctx.parent; @@ -325,6 +332,26 @@ export const transporterSchema: FactorySchemaOf< ); } + if ( + transporterTransportPlates && + transporterTransportPlates.some( + plate => (plate ?? "").length > 12 || (plate ?? "").length < 4 + ) + ) { + return new yup.ValidationError( + ERROR_TRANSPORTER_PLATES_INCORRECT_LENGTH + ); + } + + if ( + transporterTransportPlates && + transporterTransportPlates.some(plate => onlyWhiteSpace(plate ?? "")) + ) { + return new yup.ValidationError( + ERROR_TRANSPORTER_PLATES_INCORRECT_FORMAT + ); + } + return true; }), transporterCompanyName: yup @@ -481,7 +508,7 @@ export const transportSchema: FactorySchemaOf< transporterTransportPlates: yup .array() .of(yup.string()) - .max(2, "Un maximum de 2 plaques d'immatriculation est accepté") as any, + .max(2, ERROR_TRANSPORTER_PLATES_TOO_MANY) as any, transporterTransportMode: yup .mixed() .nullable() diff --git a/back/src/bsds/resolvers/queries/__tests__/bsds.bsdasri.integration.ts b/back/src/bsds/resolvers/queries/__tests__/bsds.bsdasri.integration.ts index 23b883481e..907be3e942 100644 --- a/back/src/bsds/resolvers/queries/__tests__/bsds.bsdasri.integration.ts +++ b/back/src/bsds/resolvers/queries/__tests__/bsds.bsdasri.integration.ts @@ -154,7 +154,7 @@ describe("Query.bsds.dasris base workflow", () => { takenOverAt: new Date().toISOString() as any, weight: { value: 99, isEstimate: false }, - plates: ["TRANSPORTER-PLATE"], + plates: ["AB-65-ML"], packagings: [{ type: "FUT", quantity: 44, volume: 123 }], acceptation: { status: WasteAcceptationStatus.ACCEPTED } diff --git a/back/src/bsds/resolvers/queries/__tests__/bsds.bspaoh.integration.ts b/back/src/bsds/resolvers/queries/__tests__/bsds.bspaoh.integration.ts index 9d1f3d2cf8..5b02caf5fe 100644 --- a/back/src/bsds/resolvers/queries/__tests__/bsds.bspaoh.integration.ts +++ b/back/src/bsds/resolvers/queries/__tests__/bsds.bspaoh.integration.ts @@ -170,7 +170,7 @@ describe("Query.bsds.bspaohs base workflow", () => { transport: { takenOverAt: new Date().toISOString() as any, mode: "ROAD", - plates: ["TRANSPORTER-PLATE"] + plates: ["AB-12-TY"] } }, destination: { diff --git a/back/src/bsffs/__tests__/factories.ts b/back/src/bsffs/__tests__/factories.ts index a455a5fa5f..7a66631d81 100644 --- a/back/src/bsffs/__tests__/factories.ts +++ b/back/src/bsffs/__tests__/factories.ts @@ -198,7 +198,7 @@ export function createBsffBeforeTransport( ...opts, transporterData: { transporterTransportMode: TransportMode.ROAD, - transporterTransportPlates: ["TRANSPORTER-PLATE"], + transporterTransportPlates: ["AB-12-YZ"], transporterTransportTakenOverAt: new Date(), ...opts.transporterData } diff --git a/back/src/bsffs/__tests__/registry.integration.ts b/back/src/bsffs/__tests__/registry.integration.ts index 803ecdf960..79004315ff 100644 --- a/back/src/bsffs/__tests__/registry.integration.ts +++ b/back/src/bsffs/__tests__/registry.integration.ts @@ -66,7 +66,7 @@ const createBsffWith5Transporters = async () => { data: { destinationCompanyMail: "destination@mail.com" }, transporterData: { transporterCompanySiret: transporter1.company.siret, - transporterTransportPlates: ["TRANSPORTER1-NBR-PLATES"], + transporterTransportPlates: ["TR-01-AA"], transporterCompanyAddress: transporter1.company.address } } @@ -75,22 +75,22 @@ const createBsffWith5Transporters = async () => { await addBsffTransporter({ bsffId: bsff.id, transporter: transporter2, - opt: { transporterTransportPlates: ["TRANSPORTER2-NBR-PLATES"] } + opt: { transporterTransportPlates: ["TR-02-AA"] } }); await addBsffTransporter({ bsffId: bsff.id, transporter: transporter3, - opt: { transporterTransportPlates: ["TRANSPORTER3-NBR-PLATES"] } + opt: { transporterTransportPlates: ["TR-03-AA"] } }); await addBsffTransporter({ bsffId: bsff.id, transporter: transporter4, - opt: { transporterTransportPlates: ["TRANSPORTER4-NBR-PLATES"] } + opt: { transporterTransportPlates: ["TR-04-AA"] } }); await addBsffTransporter({ bsffId: bsff.id, transporter: transporter5, - opt: { transporterTransportPlates: ["TRANSPORTER5-NBR-PLATES"] } + opt: { transporterTransportPlates: ["TR-05-AA"] } }); return { @@ -559,37 +559,27 @@ describe("toTransportedWaste", () => { expect(waste.transporterCompanySiret).toBe( bsffForRegistry.transporters[0].transporterCompanySiret ); - expect(waste.transporterNumberPlates).toStrictEqual([ - "TRANSPORTER1-NBR-PLATES" - ]); + expect(waste.transporterNumberPlates).toStrictEqual(["TR-01-AA"]); expect(waste.transporter2CompanySiret).toBe( bsffForRegistry.transporters[1].transporterCompanySiret ); - expect(waste.transporter2NumberPlates).toStrictEqual([ - "TRANSPORTER2-NBR-PLATES" - ]); + expect(waste.transporter2NumberPlates).toStrictEqual(["TR-02-AA"]); expect(waste.transporter3CompanySiret).toBe( bsffForRegistry.transporters[2].transporterCompanySiret ); - expect(waste.transporter3NumberPlates).toStrictEqual([ - "TRANSPORTER3-NBR-PLATES" - ]); + expect(waste.transporter3NumberPlates).toStrictEqual(["TR-03-AA"]); expect(waste.transporter4CompanySiret).toBe( bsffForRegistry.transporters[3].transporterCompanySiret ); - expect(waste.transporter4NumberPlates).toStrictEqual([ - "TRANSPORTER4-NBR-PLATES" - ]); + expect(waste.transporter4NumberPlates).toStrictEqual(["TR-04-AA"]); expect(waste.transporter5CompanySiret).toBe( bsffForRegistry.transporters[4].transporterCompanyVatNumber ); - expect(waste.transporter5NumberPlates).toStrictEqual([ - "TRANSPORTER5-NBR-PLATES" - ]); + expect(waste.transporter5NumberPlates).toStrictEqual(["TR-05-AA"]); }); }); @@ -843,37 +833,27 @@ describe("toAllWaste", () => { expect(waste.transporterCompanySiret).toBe( bsffForRegistry.transporters[0].transporterCompanySiret ); - expect(waste.transporterNumberPlates).toStrictEqual([ - "TRANSPORTER1-NBR-PLATES" - ]); + expect(waste.transporterNumberPlates).toStrictEqual(["TR-01-AA"]); expect(waste.transporter2CompanySiret).toBe( bsffForRegistry.transporters[1].transporterCompanySiret ); - expect(waste.transporter2NumberPlates).toStrictEqual([ - "TRANSPORTER2-NBR-PLATES" - ]); + expect(waste.transporter2NumberPlates).toStrictEqual(["TR-02-AA"]); expect(waste.transporter3CompanySiret).toBe( bsffForRegistry.transporters[2].transporterCompanySiret ); - expect(waste.transporter3NumberPlates).toStrictEqual([ - "TRANSPORTER3-NBR-PLATES" - ]); + expect(waste.transporter3NumberPlates).toStrictEqual(["TR-03-AA"]); expect(waste.transporter4CompanySiret).toBe( bsffForRegistry.transporters[3].transporterCompanySiret ); - expect(waste.transporter4NumberPlates).toStrictEqual([ - "TRANSPORTER4-NBR-PLATES" - ]); + expect(waste.transporter4NumberPlates).toStrictEqual(["TR-04-AA"]); expect(waste.transporter5CompanySiret).toBe( bsffForRegistry.transporters[4].transporterCompanyVatNumber ); - expect(waste.transporter5NumberPlates).toStrictEqual([ - "TRANSPORTER5-NBR-PLATES" - ]); + expect(waste.transporter5NumberPlates).toStrictEqual(["TR-05-AA"]); }); }); diff --git a/back/src/bsffs/typeDefs/bsff.inputs.graphql b/back/src/bsffs/typeDefs/bsff.inputs.graphql index 3d3371d024..43ec4b842b 100644 --- a/back/src/bsffs/typeDefs/bsff.inputs.graphql +++ b/back/src/bsffs/typeDefs/bsff.inputs.graphql @@ -312,7 +312,7 @@ input BsffTransporterTransportInput { "Date de prise en charge" takenOverAt: DateTime mode: TransportMode - "Plaque(s) d'immatriculation - maximum 2" + "Plaque(s) d'immatriculation - maximum 2- 4 à 12 caractères." plates: [String!] } diff --git a/back/src/bsffs/validation/bsff/__tests__/validation.integration.ts b/back/src/bsffs/validation/bsff/__tests__/validation.integration.ts index bc41b5b084..77be029c7c 100644 --- a/back/src/bsffs/validation/bsff/__tests__/validation.integration.ts +++ b/back/src/bsffs/validation/bsff/__tests__/validation.integration.ts @@ -161,7 +161,9 @@ describe("validation > parseBsff", () => { it("should throw if a transporter has more than 2 plates", () => { const zodBsff: ZodBsff = { - transporters: [{ transporterTransportPlates: ["1", "2", "3"] }] + transporters: [ + { transporterTransportPlates: ["AA-12-AA", "AA-12-AB", "AA-12-AC"] } + ] }; expect.assertions(1); try { @@ -175,9 +177,71 @@ describe("validation > parseBsff", () => { } }); + it("should throw if transporter plate number is too short", () => { + const zodBsff: ZodBsff = { + transporters: [ + { + transporterTransportPlates: ["AA"] + } + ] + }; + expect.assertions(1); + try { + parseBsff(zodBsff); + } catch (e) { + expect(e.errors).toEqual([ + expect.objectContaining({ + message: + "Le numéro d'immatriculation doit faire entre 4 et 12 caractères" + }) + ]); + } + }); + + it("should throw if transporter plate number is too long", () => { + const zodBsff: ZodBsff = { + transporters: [ + { + transporterTransportPlates: ["AZ-ER-TY-UI-09-LP-87"] + } + ] + }; + expect.assertions(1); + try { + parseBsff(zodBsff); + } catch (e) { + expect(e.errors).toEqual([ + expect.objectContaining({ + message: + "Le numéro d'immatriculation doit faire entre 4 et 12 caractères" + }) + ]); + } + }); + + it("should throw if transporter contains only whitespace", () => { + const zodBsff: ZodBsff = { + transporters: [ + { + transporterTransportPlates: [" "] + } + ] + }; + expect.assertions(1); + try { + parseBsff(zodBsff); + } catch (e) { + expect(e.errors).toEqual([ + expect.objectContaining({ + message: "Le numéro de plaque fourni est incorrect" + }) + ]); + } + }); + it("should parse correctly if a transporter has 2 plates or less", () => { const zodBsff: ZodBsff = { - transporters: [{ transporterTransportPlates: ["1", "2"] }] + transporters: [{ transporterTransportPlates: ["AA-12-AA", "AA-12-AB"] }] }; expect(parseBsff(zodBsff)).toBeDefined(); }); @@ -868,6 +932,30 @@ describe("validation > parseBsff", () => { ).toBeDefined(); }); + test("immat plates are not required at emitter signature when transport mode is road", async () => { + const bsff = await createBsffBeforeTransport({ + emitter, + transporter, + destination + }); + const zodBsff = prismaToZodBsff(bsff); + expect( + parseBsff( + { + ...zodBsff, + transporters: [ + { + ...zodBsff.transporters![0], + transporterTransportPlates: [], + transporterTransportMode: TransportMode.ROAD + } + ] + }, + { currentSignatureType: "EMISSION" } + ) + ).toBeDefined(); + }); + test("immat plates are not required at transporter signature when transport mode is not road", async () => { const bsff = await createBsffBeforeTransport({ emitter, diff --git a/back/src/bspaoh/__tests__/registry.integration.ts b/back/src/bspaoh/__tests__/registry.integration.ts index b3c0e46ebc..eaf6822e9b 100644 --- a/back/src/bspaoh/__tests__/registry.integration.ts +++ b/back/src/bspaoh/__tests__/registry.integration.ts @@ -157,7 +157,7 @@ describe("toIncomingWaste", () => { transporters: { create: { transporterCompanySiret: transporter.siret, - transporterTransportPlates: ["TRANSPORTER1-NBR-PLATES"], + transporterTransportPlates: ["TR-01-AA"], number: 1 } } @@ -247,7 +247,7 @@ describe("toOutgoingWaste", () => { transporters: { create: { transporterCompanySiret: transporter.siret, - transporterTransportPlates: ["TRANSPORTER1-NBR-PLATES"], + transporterTransportPlates: ["TR-01-AA"], number: 1 } } @@ -335,7 +335,7 @@ describe("toTransportedWaste", () => { transporters: { create: { transporterCompanySiret: transporter.siret, - transporterTransportPlates: ["TRANSPORTER1-NBR-PLATES"], + transporterTransportPlates: ["TR-01-AA"], number: 1 } } @@ -353,9 +353,7 @@ describe("toTransportedWaste", () => { expect(waste.transporterCompanySiret).toBe( paohForRegistry.transporters[0].transporterCompanySiret ); - expect(waste.transporterNumberPlates).toStrictEqual([ - "TRANSPORTER1-NBR-PLATES" - ]); + expect(waste.transporterNumberPlates).toStrictEqual(["TR-01-AA"]); expect(waste.transporter2CompanySiret).toBeNull(); expect(waste.transporter2NumberPlates).toBeNull(); @@ -383,7 +381,7 @@ describe("toManagedWaste", () => { transporters: { create: { transporterCompanySiret: transporter.siret, - transporterTransportPlates: ["TRANSPORTER1-NBR-PLATES"], + transporterTransportPlates: ["TR-01-AA"], number: 1 } } @@ -453,7 +451,7 @@ describe("toAllWaste", () => { transporters: { create: { transporterCompanySiret: transporter.siret, - transporterTransportPlates: ["TRANSPORTER1-NBR-PLATES"], + transporterTransportPlates: ["TR-01-AA"], number: 1 } } @@ -471,9 +469,7 @@ describe("toAllWaste", () => { expect(waste.transporterCompanySiret).toBe( paohForRegistry.transporters[0].transporterCompanySiret ); - expect(waste.transporterNumberPlates).toStrictEqual([ - "TRANSPORTER1-NBR-PLATES" - ]); + expect(waste.transporterNumberPlates).toStrictEqual(["TR-01-AA"]); expect(waste.transporter2CompanySiret).toBeNull(); expect(waste.transporter2NumberPlates).toBeNull(); diff --git a/back/src/bspaoh/resolvers/mutations/__tests__/duplicateBspaoh.integration.ts b/back/src/bspaoh/resolvers/mutations/__tests__/duplicateBspaoh.integration.ts index 6befb00d96..4103cc4935 100644 --- a/back/src/bspaoh/resolvers/mutations/__tests__/duplicateBspaoh.integration.ts +++ b/back/src/bspaoh/resolvers/mutations/__tests__/duplicateBspaoh.integration.ts @@ -486,4 +486,56 @@ describe("Mutation.duplicateBspaoh", () => { } ]); }); + + it("should not duplicate tranporter plates", async () => { + const { user, company } = await userWithCompanyFactory("MEMBER"); + const transporterCompany = await companyFactory({ + transporterReceipt: { + create: { + receiptNumber: "TRANSPORTER-RECEIPT-NUMBER", + validityLimit: TODAY.toISOString(), + department: "83T" + } + } + }); + const bspaoh = await bspaohFactory({ + opt: { + emitterCompanySiret: company.siret, + + transporters: { + create: { + number: 1, + transporterCompanySiret: transporterCompany.siret, + transporterCompanyName: transporterCompany.name, + transporterCompanyAddress: transporterCompany.address, + transporterCompanyContact: transporterCompany.contact, + transporterCompanyMail: transporterCompany.contactEmail, + transporterCompanyPhone: transporterCompany.contactPhone, + transporterTransportPlates: ["AZ-77-PO"] + } + } + } + }); + + const { mutate } = makeClient(user); // emitter + + const { data } = await mutate>( + DUPLICATE_BSPAOH, + { + variables: { + id: bspaoh.id + } + } + ); + + expect(data.duplicateBspaoh.status).toBe("INITIAL"); + expect(data.duplicateBspaoh.isDraft).toBe(true); + + const duplicated = await prisma.bspaoh.findUnique({ + where: { id: data.duplicateBspaoh.id }, + include: { transporters: true } + }); + + expect(duplicated?.transporters[0].transporterTransportPlates).toEqual([]); + }); }); diff --git a/back/src/bspaoh/resolvers/mutations/duplicate.ts b/back/src/bspaoh/resolvers/mutations/duplicate.ts index 06a56ad6f5..e649f2dfb7 100644 --- a/back/src/bspaoh/resolvers/mutations/duplicate.ts +++ b/back/src/bspaoh/resolvers/mutations/duplicate.ts @@ -161,6 +161,7 @@ async function duplicateBspaoh( bspaohId, createdAt: trsCreatedAt, updatedAt: trsUpdatedAt, + transporterTransportPlates, ...trsFieldsToCopy } = bspaohTransporter; diff --git a/back/src/bspaoh/typeDefs/bspaoh.inputs.graphql b/back/src/bspaoh/typeDefs/bspaoh.inputs.graphql index 0c24237363..fdc5e4c90d 100644 --- a/back/src/bspaoh/typeDefs/bspaoh.inputs.graphql +++ b/back/src/bspaoh/typeDefs/bspaoh.inputs.graphql @@ -167,7 +167,7 @@ input BspaohRecepisseInput { input BspaohTransportInput { "Mode de transport" mode: TransportMode - "Plaque(s) d'immatriculation - maximum 2" + "Plaque(s) d'immatriculation - maximum 2- 4 à 12 caractères." plates: [String!] "Date de prise en charge" takenOverAt: DateTime diff --git a/back/src/bspaoh/validation/__tests__/validation.integration.ts b/back/src/bspaoh/validation/__tests__/validation.integration.ts index 5306678dff..a7ff578ae3 100644 --- a/back/src/bspaoh/validation/__tests__/validation.integration.ts +++ b/back/src/bspaoh/validation/__tests__/validation.integration.ts @@ -105,6 +105,7 @@ describe("BSPAOH validation", () => { }); const { preparedExistingBspaoh } = prepareBspaohForParsing(bspaoh); + await parseBspaohInContext( { persisted: preparedExistingBspaoh }, { @@ -145,13 +146,143 @@ describe("BSPAOH validation", () => { } }); + it("should throw if a transporter has more than 2 plates", async () => { + const bspaoh = await bspaohFactory({ + opt: { + status: BspaohStatus.SENT, + transporters: { + create: { + transporterTransportPlates: ["AA-12-AA", "AA-12-AB", "AA-12-AC"], + transporterTransportMode: "ROAD", + transporterTakenOverAt: new Date(), + + number: 1 + } + } + } + }); + + expect.assertions(1); + try { + const { preparedExistingBspaoh } = prepareBspaohForParsing(bspaoh); + await parseBspaohInContext( + { persisted: preparedExistingBspaoh }, + { currentSignatureType: "TRANSPORT" } + ); + } catch (error) { + expect(error.issues).toEqual([ + expect.objectContaining({ + message: "Un maximum de 2 plaques d'immatriculation est accepté" + }) + ]); + } + }); + + it("should throw if transporter plate number is too short", async () => { + const bspaoh = await bspaohFactory({ + opt: { + status: BspaohStatus.SENT, + transporters: { + create: { + transporterTransportPlates: ["AA"], + transporterTransportMode: "ROAD", + transporterTakenOverAt: new Date(), + + number: 1 + } + } + } + }); + + expect.assertions(1); + try { + const { preparedExistingBspaoh } = prepareBspaohForParsing(bspaoh); + await parseBspaohInContext( + { persisted: preparedExistingBspaoh }, + { currentSignatureType: "TRANSPORT" } + ); + } catch (error) { + expect(error.issues).toEqual([ + expect.objectContaining({ + message: + "Le numéro d'immatriculation doit faire entre 4 et 12 caractères" + }) + ]); + } + }); + + it("should throw if transporter plate number is too long", async () => { + const bspaoh = await bspaohFactory({ + opt: { + status: BspaohStatus.SENT, + transporters: { + create: { + transporterTransportPlates: ["AZ-ER-TY-UI-09-LP-87"], + transporterTransportMode: "ROAD", + transporterTakenOverAt: new Date(), + + number: 1 + } + } + } + }); + + expect.assertions(1); + try { + const { preparedExistingBspaoh } = prepareBspaohForParsing(bspaoh); + await parseBspaohInContext( + { persisted: preparedExistingBspaoh }, + { currentSignatureType: "TRANSPORT" } + ); + } catch (error) { + expect(error.issues).toEqual([ + expect.objectContaining({ + message: + "Le numéro d'immatriculation doit faire entre 4 et 12 caractères" + }) + ]); + } + }); + + it("should throw if transporter contains only whitespace", async () => { + const bspaoh = await bspaohFactory({ + opt: { + status: BspaohStatus.SENT, + transporters: { + create: { + transporterTransportPlates: [" "], + transporterTransportMode: "ROAD", + transporterTakenOverAt: new Date(), + + number: 1 + } + } + } + }); + + expect.assertions(1); + try { + const { preparedExistingBspaoh } = prepareBspaohForParsing(bspaoh); + await parseBspaohInContext( + { persisted: preparedExistingBspaoh }, + { currentSignatureType: "TRANSPORT" } + ); + } catch (error) { + expect(error.issues).toEqual([ + expect.objectContaining({ + message: "Le numéro de plaque fourni est incorrect" + }) + ]); + } + }); + it("should work if transport mode is ROAD & plates are defined", async () => { const bspaoh = await bspaohFactory({ opt: {} }); const data = { ...bspaoh, - transporterTransportPlates: ["xyz"], + transporterTransportPlates: ["AA-12-AB"], transporterTransportMode: "ROAD", transporterTakenOverAt: new Date() }; diff --git a/back/src/bspaoh/validation/schema.ts b/back/src/bspaoh/validation/schema.ts index c7acfcf6c8..79c7c464af 100644 --- a/back/src/bspaoh/validation/schema.ts +++ b/back/src/bspaoh/validation/schema.ts @@ -1,12 +1,12 @@ import { z } from "zod"; -import { WasteAcceptationStatus, TransportMode } from "@prisma/client"; +import { WasteAcceptationStatus } from "@prisma/client"; import getReadableId, { ReadableIdPrefix } from "../../forms/readableId"; import { isCrematoriumRefinement } from "./dynamicRefinements"; import { BSPAOH_WASTE_CODES, BSPAOH_WASTE_TYPES } from "@td/constants"; import { CompanyRole, - foreignVatNumberSchema, - siretSchema + siretSchema, + rawTransporterSchema } from "../../common/validation/zod/schema"; import { isRegisteredVatNumberRefinement } from "../../common/validation/zod/refinement"; @@ -125,41 +125,16 @@ const rawBspaohSchema = z.object({ export type ZodBspaoh = z.infer; -const rawBspaohTransporterSchema = z.object({ - transporterCompanyName: z.string().nullish(), - transporterCompanySiret: siretSchema(CompanyRole.Transporter).nullish(), // Further verifications done here under in superRefine - transporterCompanyAddress: z.string().nullish(), - transporterCompanyContact: z.string().nullish(), - transporterCompanyPhone: z.string().nullish(), - transporterCompanyMail: z.string().nullish(), - transporterCompanyVatNumber: foreignVatNumberSchema(CompanyRole.Transporter) - .nullish() - .superRefine(isRegisteredVatNumberRefinement), - transporterCustomInfo: z.string().nullish(), - transporterRecepisseIsExempted: z - .boolean() - .nullish() - .transform(v => Boolean(v)), - transporterRecepisseNumber: z.string().nullish(), - transporterRecepisseDepartment: z.string().nullish(), - transporterRecepisseValidityLimit: z.coerce.date().nullish(), - transporterTransportMode: z.nativeEnum(TransportMode).nullish(), - transporterTakenOverAt: z.coerce.date().nullish(), - transporterTransportPlates: z - .array(z.string()) - .max(2, "Un maximum de 2 plaques d'immatriculation est accepté") - .default([]), - transporterTransportTakenOverAt: z.coerce.date().nullish(), - transporterTransportSignatureAuthor: z.string().nullish(), - transporterTransportSignatureDate: z.coerce.date().nullish() -}); +const rawBspaohTransporterSchema = rawTransporterSchema + .omit({ id: true, number: true }) + .merge(z.object({ transporterTakenOverAt: z.coerce.date().nullish() })); export type ZodBspaohTransporter = z.infer; const rawFullBspaohSchema = rawBspaohSchema.merge(rawBspaohTransporterSchema); export const fullBspaohSchema = rawFullBspaohSchema - .superRefine((val, ctx) => { + .superRefine(async (val, ctx) => { // refine date order if ( val.destinationReceptionDate && @@ -181,6 +156,8 @@ export const fullBspaohSchema = rawFullBspaohSchema message: `La consistance ne peut être liquide pour ce type de déchet` }); } + // refine transporter vat + await isRegisteredVatNumberRefinement(val.transporterCompanyVatNumber, ctx); }) .transform(val => { return val; diff --git a/back/src/bsvhu/__tests__/registry.integration.ts b/back/src/bsvhu/__tests__/registry.integration.ts index bf893a3155..7fcce9732e 100644 --- a/back/src/bsvhu/__tests__/registry.integration.ts +++ b/back/src/bsvhu/__tests__/registry.integration.ts @@ -290,7 +290,7 @@ describe("toTransportedWaste", () => { const bsvhu = await bsvhuFactory({ opt: { destinationCompanyMail: "destination@mail.com", - transporterTransportPlates: ["TRANSPORTER1-NBR-PLATES"] + transporterTransportPlates: ["TR-01-AA"] } }); @@ -305,9 +305,7 @@ describe("toTransportedWaste", () => { expect(waste.transporterCompanySiret).toBe( bsvhuForRegistry.transporterCompanySiret ); - expect(waste.transporterNumberPlates).toStrictEqual([ - "TRANSPORTER1-NBR-PLATES" - ]); + expect(waste.transporterNumberPlates).toStrictEqual(["TR-01-AA"]); expect(waste.transporter2CompanySiret).toBeNull(); expect(waste.transporter2NumberPlates).toBeNull(); @@ -475,7 +473,7 @@ describe("toAllWaste", () => { const bsvhu = await bsvhuFactory({ opt: { destinationCompanyMail: "destination@mail.com", - transporterTransportPlates: ["TRANSPORTER1-NBR-PLATES"] + transporterTransportPlates: ["TR-01-AA"] } }); @@ -490,9 +488,7 @@ describe("toAllWaste", () => { expect(waste.transporterCompanySiret).toBe( bsvhuForRegistry.transporterCompanySiret ); - expect(waste.transporterNumberPlates).toStrictEqual([ - "TRANSPORTER1-NBR-PLATES" - ]); + expect(waste.transporterNumberPlates).toStrictEqual(["TR-01-AA"]); expect(waste.transporter2CompanySiret).toBeNull(); expect(waste.transporter2NumberPlates).toBeNull(); diff --git a/back/src/bsvhu/validation/refinements.ts b/back/src/bsvhu/validation/refinements.ts index b38467a9ee..81e591523a 100644 --- a/back/src/bsvhu/validation/refinements.ts +++ b/back/src/bsvhu/validation/refinements.ts @@ -310,42 +310,6 @@ export const checkTransportModeAndReceptionWeight: Refinement< ); }; -const onlyWhiteSpace = (str: string) => !str.trim().length; // check whitespaces, tabs, newlines and invisible chars - -export const checkTransportPlates: Refinement = ( - bsvhu, - ctx -) => { - const { transporterTransportPlates } = bsvhu; - const path = ["transporter", "transport", "plates"]; - if (transporterTransportPlates.length > 2) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path, - message: "Un maximum de 2 plaques d'immatriculation est accepté" - }); - } - - if ( - transporterTransportPlates.some(plate => plate.length > 12) || - transporterTransportPlates.some(plate => plate.length < 4) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path, - message: "Le numéro d'immatriculation doit faire entre 4 et 12 caractères" - }); - } - - if (transporterTransportPlates.some(plate => onlyWhiteSpace(plate))) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path, - message: "Le numéro de plaque fourni est incorrect" - }); - } -}; - export const checkRequiredFields: ( validationContext: BsvhuValidationContext ) => Refinement = validationContext => { diff --git a/back/src/bsvhu/validation/schema.ts b/back/src/bsvhu/validation/schema.ts index e07eacad76..690bb7c2c0 100644 --- a/back/src/bsvhu/validation/schema.ts +++ b/back/src/bsvhu/validation/schema.ts @@ -10,12 +10,12 @@ import { checkEmitterSituation, checkPackagingAndIdentificationType, checkTransportModeAndWeight, - checkTransportModeAndReceptionWeight, - checkTransportPlates + checkTransportModeAndReceptionWeight } from "./refinements"; import { BsvhuValidationContext } from "./types"; import { weightSchema } from "../../common/validation/weight"; import { WeightUnits } from "../../common/validation"; +import { validateTransporterPlates } from "../../common/validation/zod/schema"; import { CompanyRole, foreignVatNumberSchema, @@ -35,6 +35,7 @@ import { } from "@prisma/client"; import { fillIntermediariesOrgIds, runTransformers } from "./transformers"; import { TransportMode } from "@prisma/client"; +import { ERROR_TRANSPORTER_PLATES_TOO_MANY } from "../../common/validation/messages"; export const ZodWasteCodeEnum = z .enum(BSVHU_WASTE_CODES, { @@ -194,7 +195,11 @@ const rawBsvhuSchema = z.object({ transporterCustomInfo: z.string().nullish(), transporterTransportMode: z.nativeEnum(TransportMode).nullish(), - transporterTransportPlates: z.array(z.string()).default([]), + transporterTransportPlates: z + .array(z.string()) + .max(2, ERROR_TRANSPORTER_PLATES_TOO_MANY) + .default([]) + .superRefine(validateTransporterPlates), ecoOrganismeName: z.string().nullish(), ecoOrganismeSiret: siretSchema(CompanyRole.EcoOrganisme).nullish(), @@ -241,8 +246,7 @@ const refinedBsvhuSchema = rawBsvhuSchema .superRefine(checkEmitterSituation) .superRefine(checkPackagingAndIdentificationType) .superRefine(checkTransportModeAndWeight) - .superRefine(checkTransportModeAndReceptionWeight) - .superRefine(checkTransportPlates); + .superRefine(checkTransportModeAndReceptionWeight); // Transformations synchrones qui sont toujours // joués même si `enableCompletionTransformers=false` diff --git a/back/src/common/validation/messages.ts b/back/src/common/validation/messages.ts new file mode 100644 index 0000000000..57136dc6df --- /dev/null +++ b/back/src/common/validation/messages.ts @@ -0,0 +1,7 @@ +export const ERROR_TRANSPORTER_PLATES_TOO_MANY = + "Un maximum de 2 plaques d'immatriculation est accepté"; +export const ERROR_TRANSPORTER_PLATES_INCORRECT_LENGTH = + "Le numéro d'immatriculation doit faire entre 4 et 12 caractères"; + +export const ERROR_TRANSPORTER_PLATES_INCORRECT_FORMAT = + "Le numéro de plaque fourni est incorrect"; diff --git a/back/src/common/validation/zod/schema.ts b/back/src/common/validation/zod/schema.ts index 2663289fa1..5634423a5b 100644 --- a/back/src/common/validation/zod/schema.ts +++ b/back/src/common/validation/zod/schema.ts @@ -1,6 +1,11 @@ import { z } from "zod"; import { TransportMode } from "@prisma/client"; import { isForeignVat, isSiret, isVat } from "@td/constants"; +import { + ERROR_TRANSPORTER_PLATES_TOO_MANY, + ERROR_TRANSPORTER_PLATES_INCORRECT_LENGTH, + ERROR_TRANSPORTER_PLATES_INCORRECT_FORMAT +} from "../messages"; export enum CompanyRole { Emitter = "Émetteur", @@ -92,6 +97,28 @@ export const foreignVatNumberSchema = (expectedCompanyRole?: CompanyRole) => } ); +export const onlyWhiteSpace = (str: string) => !str.trim().length; // check whitespaces, tabs, newlines and invisible chars + +export const validateTransporterPlates = ( + plates: string[], + ctx: z.RefinementCtx +) => { + if (plates.some(plate => plate.length > 12 || plate.length < 4)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: ERROR_TRANSPORTER_PLATES_INCORRECT_LENGTH + }); + return; + } + + if (plates.some(plate => onlyWhiteSpace(plate))) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: ERROR_TRANSPORTER_PLATES_INCORRECT_FORMAT + }); + } +}; + export const rawTransporterSchema = z.object({ id: z.string().nullish(), number: z.number().nullish(), @@ -118,8 +145,9 @@ export const rawTransporterSchema = z.object({ transporterTransportMode: z.nativeEnum(TransportMode).nullish(), transporterTransportPlates: z .array(z.string()) - .max(2, "Un maximum de 2 plaques d'immatriculation est accepté") - .default([]), + .max(2, ERROR_TRANSPORTER_PLATES_TOO_MANY) + .default([]) + .superRefine(validateTransporterPlates), transporterTransportTakenOverAt: z.coerce.date().nullish(), transporterTransportSignatureAuthor: z.string().nullish(), transporterTransportSignatureDate: z.coerce.date().nullish() diff --git a/back/src/forms/__tests__/registry.integration.ts b/back/src/forms/__tests__/registry.integration.ts index 76b267ccc0..002156dee4 100644 --- a/back/src/forms/__tests__/registry.integration.ts +++ b/back/src/forms/__tests__/registry.integration.ts @@ -89,35 +89,35 @@ const createBsddWith5Transporters = async () => { data: [ { transporterCompanySiret: transporter.company.siret, - transporterNumberPlate: "TRANSPORTER1-NBR-PLATES", + transporterNumberPlate: "TR-01-AA", transporterCompanyAddress: transporter.company.address, takenOverAt: new Date(), number: 1 }, { transporterCompanySiret: transporter2.company.siret, - transporterNumberPlate: "TRANSPORTER2-NBR-PLATES", + transporterNumberPlate: "TR-02-AA", transporterCompanyAddress: transporter2.company.address, takenOverAt: new Date(), number: 2 }, { transporterCompanySiret: transporter3.company.siret, - transporterNumberPlate: "TRANSPORTER3-NBR-PLATES", + transporterNumberPlate: "TR-03-AA", transporterCompanyAddress: transporter3.company.address, takenOverAt: new Date(), number: 3 }, { transporterCompanySiret: transporter4.company.siret, - transporterNumberPlate: "TRANSPORTER4-NBR-PLATES", + transporterNumberPlate: "TR-04-AA", transporterCompanyAddress: transporter4.company.address, takenOverAt: new Date(), number: 4 }, { transporterCompanyVatNumber: transporter5.company.vatNumber, - transporterNumberPlate: "TRANSPORTER5-NBR-PLATES", + transporterNumberPlate: "TR-05-AA", transporterCompanyAddress: transporter5.company.address, takenOverAt: new Date(), number: 5 @@ -717,29 +717,19 @@ describe("toTransportedWaste", () => { // Then expect(waste.transporterCompanySiret).toBe(data.transporter1.siret); - expect(waste.transporterNumberPlates).toStrictEqual([ - "TRANSPORTER1-NBR-PLATES" - ]); + expect(waste.transporterNumberPlates).toStrictEqual(["TR-01-AA"]); expect(waste.transporter2CompanySiret).toBe(data.transporter2.siret); - expect(waste.transporter2NumberPlates).toStrictEqual([ - "TRANSPORTER2-NBR-PLATES" - ]); + expect(waste.transporter2NumberPlates).toStrictEqual(["TR-02-AA"]); expect(waste.transporter3CompanySiret).toBe(data.transporter3.siret); - expect(waste.transporter3NumberPlates).toStrictEqual([ - "TRANSPORTER3-NBR-PLATES" - ]); + expect(waste.transporter3NumberPlates).toStrictEqual(["TR-03-AA"]); expect(waste.transporter4CompanySiret).toBe(data.transporter4.siret); - expect(waste.transporter4NumberPlates).toStrictEqual([ - "TRANSPORTER4-NBR-PLATES" - ]); + expect(waste.transporter4NumberPlates).toStrictEqual(["TR-04-AA"]); expect(waste.transporter5CompanySiret).toBe(data.transporter5.vatNumber); - expect(waste.transporter5NumberPlates).toStrictEqual([ - "TRANSPORTER5-NBR-PLATES" - ]); + expect(waste.transporter5NumberPlates).toStrictEqual(["TR-05-AA"]); }); }); @@ -1228,30 +1218,20 @@ describe("toAllWaste", () => { // Then expect(waste.transporterCompanySiret).toBe(data.transporter1.siret); - expect(waste.transporterNumberPlates).toStrictEqual([ - "TRANSPORTER1-NBR-PLATES" - ]); + expect(waste.transporterNumberPlates).toStrictEqual(["TR-01-AA"]); expect(waste.transporter2CompanySiret).toBe(data.transporter2.siret); - expect(waste.transporter2NumberPlates).toStrictEqual([ - "TRANSPORTER2-NBR-PLATES" - ]); + expect(waste.transporter2NumberPlates).toStrictEqual(["TR-02-AA"]); expect(waste.transporter3CompanySiret).toBe(data.transporter3.siret); - expect(waste.transporter3NumberPlates).toStrictEqual([ - "TRANSPORTER3-NBR-PLATES" - ]); + expect(waste.transporter3NumberPlates).toStrictEqual(["TR-03-AA"]); expect(waste.transporter4CompanySiret).toBe(data.transporter4.siret); - expect(waste.transporter4NumberPlates).toStrictEqual([ - "TRANSPORTER4-NBR-PLATES" - ]); + expect(waste.transporter4NumberPlates).toStrictEqual(["TR-04-AA"]); // Foreign transporter expect(waste.transporter5CompanySiret).toBe(data.transporter5.vatNumber); - expect(waste.transporter5NumberPlates).toStrictEqual([ - "TRANSPORTER5-NBR-PLATES" - ]); + expect(waste.transporter5NumberPlates).toStrictEqual(["TR-05-AA"]); }); it("if forwarding BSD, should contain the info of the initial emitter", async () => { diff --git a/back/src/forms/__tests__/validation.integration.ts b/back/src/forms/__tests__/validation.integration.ts index bab82cbf25..36ee4b46e7 100644 --- a/back/src/forms/__tests__/validation.integration.ts +++ b/back/src/forms/__tests__/validation.integration.ts @@ -735,14 +735,18 @@ describe("beforeTransportSchema", () => { transporters: Partial[]; }; + let transporterCompany; + afterAll(resetDatabase); beforeAll(async () => { const emitterCompany = await companyFactory({ companyTypes: ["PRODUCER"] }); - const transporterCompany = await companyFactory({ + + transporterCompany = await companyFactory({ companyTypes: ["TRANSPORTER"] }); + const destinationCompany = await companyFactory({ companyTypes: ["WASTEPROCESSOR"] }); @@ -921,6 +925,123 @@ describe("beforeTransportSchema", () => { ); }); + it("transporter plate is required if transporter mode is ROAD - too short", async () => { + const testForm: Partial
& { + transporters: Partial[]; + } = { + ...beforeTransportForm, + transporters: [ + { + ...transporterData, + transporterTransportMode: "ROAD", + transporterNumberPlate: "ab" + } + ] + }; + const validateFn = () => + beforeTransportSchemaFn({ + signingTransporterOrgId: transporterData.transporterCompanySiret + }).validate(testForm); + + await expect(validateFn()).rejects.toThrow( + "Le numéro d'immatriculation doit faire entre 4 et 12 caractères" + ); + }); + + it("transporter plate is required if transporter mode is ROAD - too long", async () => { + const testForm: Partial & { + transporters: Partial[]; + } = { + ...beforeTransportForm, + transporters: [ + { + ...transporterData, + transporterTransportMode: "ROAD", + transporterNumberPlate: "abcdefghijklm" + } + ] + }; + const validateFn = () => + beforeTransportSchemaFn({ + signingTransporterOrgId: transporterData.transporterCompanySiret + }).validate(testForm); + + await expect(validateFn()).rejects.toThrow( + "Le numéro d'immatriculation doit faire entre 4 et 12 caractères" + ); + }); + + it("transporter plate is required if transporter mode is ROAD - 2 plates provided", async () => { + const testForm: Partial & { + transporters: Partial[]; + } = { + ...beforeTransportForm, + transporters: [ + { + ...transporterData, + transporterCompanySiret: transporterCompany.siret, + transporterTransportMode: "ROAD", + transporterNumberPlate: "AZ-12-TY, TY-LK-34" + } + ] + }; + const validateFn = () => + beforeTransportSchemaFn({ + signingTransporterOrgId: transporterData.transporterCompanySiret + }).validate(testForm); + + const isValid = await validateFn(); + + expect(isValid).toBeTruthy(); + }); + + it("transporter plate is required if transporter mode is ROAD - too many plates", async () => { + const testForm: Partial & { + transporters: Partial[]; + } = { + ...beforeTransportForm, + transporters: [ + { + ...transporterData, + transporterCompanySiret: transporterCompany.siret, + transporterTransportMode: "ROAD", + transporterNumberPlate: "AZ-12-TY, TY-LK-34, QS-23-TY" + } + ] + }; + const validateFn = () => + beforeTransportSchemaFn({ + signingTransporterOrgId: transporterData.transporterCompanySiret + }).validate(testForm); + + await expect(validateFn()).rejects.toThrow( + "Un maximum de 2 plaques d'immatriculation est accepté" + ); + }); + + it("transporter plate is required if transporter mode is ROAD - only whitespace", async () => { + const testForm: Partial & { + transporters: Partial[]; + } = { + ...beforeTransportForm, + transporters: [ + { + ...transporterData, + transporterTransportMode: "ROAD", + transporterNumberPlate: " " + } + ] + }; + const validateFn = () => + beforeTransportSchemaFn({ + signingTransporterOrgId: transporterData.transporterCompanySiret + }).validate(testForm); + + await expect(validateFn()).rejects.toThrow( + "Le numéro de plaque fourni est incorrect" + ); + }); + it("transporter plate is not required if transport mode is not ROAD", async () => { const testForm: Partial & { transporters: Partial[]; @@ -972,7 +1093,7 @@ describe("beforeTransportSchema", () => { { ...transporterData, transporterTransportMode: "ROAD", - transporterNumberPlate: "TRANSPORTER-PLATES" + transporterNumberPlate: "AZ-45-TR" } ] }; diff --git a/back/src/forms/resolvers/mutations/__tests__/signTransportForm.integration.ts b/back/src/forms/resolvers/mutations/__tests__/signTransportForm.integration.ts index 8f3010f2d8..0ae76483b7 100644 --- a/back/src/forms/resolvers/mutations/__tests__/signTransportForm.integration.ts +++ b/back/src/forms/resolvers/mutations/__tests__/signTransportForm.integration.ts @@ -1207,7 +1207,7 @@ describe("signTransportForm", () => { input: { takenOverAt: new Date().toISOString() as unknown as Date, takenOverBy: "Collecteur annexe 1", - transporterNumberPlate: "TRANSPORTER-PLATE" + transporterNumberPlate: "QR-33-TY" } } }); @@ -1219,7 +1219,7 @@ describe("signTransportForm", () => { where: { formId: appendix1Item.id } }); expect(appendix1ItemTransporters?.transporterNumberPlate).toBe( - "TRANSPORTER-PLATE" + "QR-33-TY" ); // Should copy plate to next items @@ -1227,16 +1227,14 @@ describe("signTransportForm", () => { where: { formId: appendix2Item.id } }); expect(appendix2ItemTransporters?.transporterNumberPlate).toBe( - "TRANSPORTER-PLATE" + "QR-33-TY" ); // Parent should have its plate updated as well const parentTransporters = await prisma.bsddTransporter.findFirst({ where: { formId: parent.id } }); - expect(parentTransporters?.transporterNumberPlate).toBe( - "TRANSPORTER-PLATE" - ); + expect(parentTransporters?.transporterNumberPlate).toBe("QR-33-TY"); // SECOND UPDATE (on the second child bsd) @@ -1249,7 +1247,7 @@ describe("signTransportForm", () => { input: { takenOverAt: new Date().toISOString() as unknown as Date, takenOverBy: "Collecteur annexe 1", - transporterNumberPlate: "TRANSPORTER-PLATE-2" + transporterNumberPlate: "QR-33-TY-2" } } }); @@ -1260,7 +1258,7 @@ describe("signTransportForm", () => { where: { formId: appendix2Item.id } }); expect(appendix2ItemTransportersBis?.transporterNumberPlate).toBe( - "TRANSPORTER-PLATE-2" + "QR-33-TY-2" ); // Should override plate from child bsd 1 @@ -1269,16 +1267,14 @@ describe("signTransportForm", () => { where: { formId: appendix1Item.id } }); expect(appendix1ItemTransportersBis?.transporterNumberPlate).toBe( - "TRANSPORTER-PLATE-2" + "QR-33-TY-2" ); // Parent should have its plate updated as well const parentTransportersBis = await prisma.bsddTransporter.findFirst({ where: { formId: parent.id } }); - expect(parentTransportersBis?.transporterNumberPlate).toBe( - "TRANSPORTER-PLATE-2" - ); + expect(parentTransportersBis?.transporterNumberPlate).toBe("QR-33-TY-2"); }); it("should disallow signing the appendix1 item if there is no quantity & packaging infos", async () => { diff --git a/back/src/forms/typeDefs/bsdd.inputs.graphql b/back/src/forms/typeDefs/bsdd.inputs.graphql index 80b74c42f1..366c0f175c 100644 --- a/back/src/forms/typeDefs/bsdd.inputs.graphql +++ b/back/src/forms/typeDefs/bsdd.inputs.graphql @@ -15,7 +15,11 @@ input SignEmissionFormInput { "Mention au titre des règlements RID, ADNR, IMDG (optionnel)" nonRoadRegulationMention: String - "Numéro de la plaque d'immatriculation transporteur" + """ + Numéro de la plaque d'immatriculation transporteur. + Jusqu'à 2 plaques séparées par une virgule ex: AZ-12-TR, AQ-34-JK + Format de chaque plaque: 4 à 12 caractères + """ transporterNumberPlate: String "Date de signature de l'émetteur" diff --git a/back/src/forms/validation.ts b/back/src/forms/validation.ts index 90ec20cdda..75281ce32d 100644 --- a/back/src/forms/validation.ts +++ b/back/src/forms/validation.ts @@ -76,6 +76,12 @@ import { isFinalOperationCode } from "../common/operationCodes"; import { flattenFormInput } from "./converter"; import { bsddWasteQuantities } from "./helpers/bsddWasteQuantities"; import { isDefined, isDefinedStrict } from "../common/helpers"; +import { onlyWhiteSpace } from "../common/validation/zod/schema"; +import { + ERROR_TRANSPORTER_PLATES_TOO_MANY, + ERROR_TRANSPORTER_PLATES_INCORRECT_LENGTH, + ERROR_TRANSPORTER_PLATES_INCORRECT_FORMAT +} from "../common/validation/messages"; // set yup default error messages configureYup(); @@ -1021,6 +1027,7 @@ export const transporterSchemaFn: FactorySchemaOf< transporterCompanySiret, transporterCompanyVatNumber } = ctx.parent; + // plates required or not if ( context.signingTransporterOrgId && @@ -1035,6 +1042,33 @@ export const transporterSchemaFn: FactorySchemaOf< ); } + if (!transporterNumberPlate) { + return true; + } + + // convert plate string to an array + const plates = formatInitialPlates(transporterNumberPlate); + + if (plates.length > 2) { + return new yup.ValidationError(ERROR_TRANSPORTER_PLATES_TOO_MANY); + } + + if ( + plates.some( + plate => (plate ?? "").length > 12 || (plate ?? "").length < 4 + ) + ) { + return new yup.ValidationError( + ERROR_TRANSPORTER_PLATES_INCORRECT_LENGTH + ); + } + + if (plates.some(plate => onlyWhiteSpace(plate ?? ""))) { + return new yup.ValidationError( + ERROR_TRANSPORTER_PLATES_INCORRECT_FORMAT + ); + } + return true; }), transporterCompanyName: yup @@ -2392,3 +2426,21 @@ export async function validateIntermediaries( await intermediarySchema.validate(companyInput, { abortEarly: false }); } } + +const formatInitialPlates = ( + transporterNumberPlate: string | null | undefined +): string[] => { + if (!transporterNumberPlate) { + return []; + } + const regex = /,+|,\s+/; + const containsComma = regex.test(transporterNumberPlate); + if (containsComma) { + return transporterNumberPlate?.split(regex); + } else { + if (transporterNumberPlate) { + return [transporterNumberPlate]; + } + return []; + } +};