Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ fileignoreconfig:
checksum: 4716f6a0de6f2dc2af6d8ac8abf11a253a5a719c231df5ae51c783e6148c42ed
- filename: packages/frontend-usagers/src/assets/Download.svg
checksum: a5c2caf47b1b715b2cf8240e9af9090061faae6261b21bc62173689f1666536e
- filename: packages/frontend-usagers/src/assets/mon-agrement.svg
checksum: 33d0005c016a8443dbb23ce27d8fc822b98399a67d286e27e79fbb6ab128f4dc
- filename: packages/frontend-usagers/src/components/ApiTokenManager.vue
checksum: 56a62209a9c1ac0d43c66a8971c23c9db1bba9e77bc222211e10468fdc795d79
- filename: packages/frontend-usagers/src/components/DS/hebergements-sejour-detail.vue
Expand Down Expand Up @@ -315,10 +317,10 @@ fileignoreconfig:
checksum: 0c60a5a9f3321ab3c36025bdeb35663395586d5bca3d21984fdbe5997b46c6eb
- filename: packages/migrations/src/scripts/reset-s3.js
checksum: 7b52636c4c49bf85a592f649f8fbf653e0203b390f4f9d220fe3ae4da06422a7
- filename: packages/shared-bridge/src/dto/paginationQueryDto.ts
checksum: 171d7cf0f68b404e3b648fcb9d7b3e36891da049bad2ede67007d0c03661145a
- filename: packages/shared-bridge/src/constantes/file.ts
checksum: dded09b86eeb4c664e493ff12b023d20914b18ac5c9a98ac7e60235daec3927d
- filename: packages/shared-bridge/src/dto/paginationQueryDto.ts
checksum: 171d7cf0f68b404e3b648fcb9d7b3e36891da049bad2ede67007d0c03661145a
- filename: packages/shared-bridge/src/utils/request.ts
checksum: c29fc1527b710e177395fabd040b7e3cdd44b429b21d504ba6f192faf1cd861a
- filename: packages/shared-ui/src/components/DsfrMultiselectV2.vue
Expand Down
51 changes: 51 additions & 0 deletions packages/backend/src/__tests__/usagers/agrements.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import request from "supertest";

import app from "../../app";
import { mailService } from "../../services/mail";
import { buildAgrementFixture } from "../helper/fixtures/agrementsFixture";
import {
createAgrement,
Expand All @@ -25,6 +26,10 @@ jest.mock("../../middlewares/common/checkJWT", () => {
beforeAll(async () => await createTestContainer());
afterAll(async () => await removeTestContainer());

beforeEach(() => {
jest.clearAllMocks();
});

describe("GET /agrements/organisme/:organismeId", () => {
it("devrait retourner un agrément par ID de l'organisme avec succès", async () => {
authUser = await createUsagersUser();
Expand Down Expand Up @@ -91,3 +96,49 @@ describe("POST /agrements", () => {
expect(response.body).toBeDefined();
});
});

it("devrait changer le statut d'un agrément avec succès", async () => {
authUser = await createUsagersUser();
const organismeId = await createOrganisme({ userId: authUser.id });
const agrementData = await buildAgrementFixture({ organismeId });
const agrementId = await createAgrement({
agrement: agrementData,
organismeId,
});

const response = await request(app)
.patch(`/agrements/${agrementId}/statut`)
.send({ statut: "TRANSMIS" });

expect(response.status).toBe(200);
expect(response.body.success).toBe(true);

const { agrement } = await getAgrement(organismeId);
expect(agrement.statut).toBe("TRANSMIS");
});

jest.mock("../../services/mail", () => ({
mailService: { send: jest.fn() },
}));

it("workflow changement statut de l'agrement", async () => {
authUser = await createUsagersUser();
const organismeId = await createOrganisme({ userId: authUser.id });
const agrementData = await buildAgrementFixture({ organismeId });
const agrementId = await createAgrement({
agrement: agrementData,
organismeId,
});

const response = await request(app)
.patch(`/agrements/${agrementId}/statut`)
.send({ statut: "TRANSMIS" });

expect(response.status).toBe(200);
expect(response.body.success).toBe(true);

const { agrement } = await getAgrement(organismeId);
expect(agrement.statut).toBe("TRANSMIS");

expect(mailService.send).toHaveBeenCalledTimes(1);
});
24 changes: 23 additions & 1 deletion packages/backend/src/usagers/agrements/agrements.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,28 @@ export const AgrementController = {
next(error);
}
},
async patchStatut(
req: RouteRequest<AgrementUsagersRoutes["PatchStatut"]>,
res: RouteResponse<AgrementUsagersRoutes["PatchStatut"]>,
next: NextFunction,
) {
log.i("PATCH statut IN");

const { id: usagerUserId } = req.decoded!;
const agrementId = Number(req.validatedParams!.agrementId);
const { statut } = req.validatedBody!;
try {
await AgrementService.updateStatut({
agrementId,
statut,
usagerUserId,
});
res.json({ success: true });
} catch (error) {
log.w("PATCH statut error", error);
next(error);
}
},
async post(
req: RouteRequest<AgrementUsagersRoutes["PostAgrement"]>,
res: RouteResponse<AgrementUsagersRoutes["PostAgrement"]>,
Expand All @@ -73,7 +95,7 @@ export const AgrementController = {
// boUserId: null,
// metadata: null,
// source: "Organisateur",
// type: "Mise à jour page",
// type: AGREMENT_HISTORY_TYPE.CREATION,
// typePrecision: "Renuvellement en cours de complétion",
// usagerUserId: 1,
// });
Expand Down
56 changes: 54 additions & 2 deletions packages/backend/src/usagers/agrements/agrements.repository.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type {
AGREMENT_HISTORY_TYPE,
AgrementHistoryItem,
AgrementHistoryRow,
} from "@vao/shared-bridge";
import { AgrementDto } from "@vao/shared-bridge";
import { AGREMENT_STATUT, AgrementDto } from "@vao/shared-bridge";

import { saveAdresse } from "../../services/adresse";
import {
Expand Down Expand Up @@ -252,6 +253,33 @@ export const AgrementsRepository = {
return result.rows;
},

/**
* Récupère un agrément par son ID. Retourne agrement enrichi avec l'email du user responsable.
*/
async getById(
agrementId: number,
): Promise<(AgrementEntity & { user_mail: string | null }) | null> {
const client = await getPool().connect();
try {
const result = await client.query(
`SELECT a.*, u.mail AS user_mail
FROM front.agrements a
JOIN front.organismes o ON a.organisme_id = o.id
JOIN front.user_organisme uo ON uo.org_id = o.id
JOIN front.users u ON uo.use_id = u.id
WHERE a.id = $1
LIMIT 1;`,
[agrementId],
);
if (result.rows.length === 0) {
return null;
}
return result.rows[0] as AgrementEntity & { user_mail: string | null };
} finally {
client.release();
}
},

/**
* Récupère un agrément par organisme ID (avec ou sans détails liés)
*/
Expand Down Expand Up @@ -357,6 +385,7 @@ export const AgrementsRepository = {

return agrementDto;
},

async getHistory(agrementId: number): Promise<AgrementHistoryItem[]> {
const client = await getPool().connect();
try {
Expand Down Expand Up @@ -413,6 +442,7 @@ export const AgrementsRepository = {
client.release();
}
},

async insertHistoryEvent({
source,
agrementId,
Expand All @@ -426,7 +456,7 @@ export const AgrementsRepository = {
agrementId: number;
usagerUserId?: number | null;
boUserId?: number | null;
type?: string | null;
type?: AGREMENT_HISTORY_TYPE | null;
typePrecision?: string | null;
metadata?: Record<string, unknown> | null;
}): Promise<number> {
Expand Down Expand Up @@ -455,6 +485,7 @@ export const AgrementsRepository = {
const { rows } = await getPool().query(query, values);
return rows[0].id;
},

async update({
agrement,
dateFinValidite,
Expand Down Expand Up @@ -619,4 +650,25 @@ export const AgrementsRepository = {
}
return agrementId;
},
/**
* Met à jour uniquement le statut d'un agrément
*/
async updateStatut({
agrementId,
statut,
}: {
agrementId: number;
statut: AGREMENT_STATUT;
}): Promise<boolean> {
const client = await getPool().connect();
try {
const result = await client.query(
`UPDATE front.agrements SET statut = $1, updated_at = NOW() WHERE id = $2`,
[statut, agrementId],
);
return result.rowCount > 0;
} finally {
client.release();
}
},
};
8 changes: 8 additions & 0 deletions packages/backend/src/usagers/agrements/agrements.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,11 @@ router.get(
);

export default router;

router.patch(
"/:agrementId/statut",
checkJWT,
requestValidatorMiddleware(AgrementUsagersRoutesSchema["PatchStatut"]),
checkPermissionAgrement,
AgrementController.patchStatut,
);
58 changes: 56 additions & 2 deletions packages/backend/src/usagers/agrements/agrements.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import {
ACTIVITE_TYPE,
ActiviteDto,
addYears,
AGREMENT_HISTORY_TYPE,
AGREMENT_STATUT,
AgrementDto,
} from "@vao/shared-bridge";

import { getById } from "../../services/adresse";
import { getFileMetaData } from "../../services/Document";
import { mailService } from "../../services/mail";
import AppError from "../../utils/error";
import logger from "../../utils/logger";
import MailUtils from "../../utils/mail";
import { AgrementsRepository } from "./agrements.repository";

const log = logger(module.filename);
Expand Down Expand Up @@ -40,7 +45,6 @@ export const AgrementService = {
}
return agrement;
},

async getAllActivites(): Promise<ActiviteDto[]> {
const activites = await AgrementsRepository.getAllActivites();
return activites.map((activite) => ({
Expand All @@ -50,6 +54,7 @@ export const AgrementService = {
libelle: activite.libelle,
}));
},

async getHistory(agrementId: number) {
const history = await AgrementsRepository.getHistory(agrementId);
return history;
Expand Down Expand Up @@ -78,10 +83,59 @@ export const AgrementService = {
agrementId: number;
usagerUserId?: number | null;
boUserId?: number | null;
type?: string | null;
type?: AGREMENT_HISTORY_TYPE | null;
typePrecision?: string | null;
metadata?: Record<string, unknown> | null;
}) {
return AgrementsRepository.insertHistoryEvent(event);
},
async updateStatut({
agrementId,
statut,
usagerUserId,
}: {
agrementId: number;
statut: AGREMENT_STATUT;
usagerUserId: string;
}): Promise<boolean> {
const agrement = await AgrementsRepository.getById(agrementId);
if (!agrement) {
log.w("Agrement non trouvé", agrementId);
throw new AppError("Agrement non trouvé", { statusCode: 404 });
}

const updated = await AgrementsRepository.updateStatut({
agrementId,
statut,
});
if (!updated) {
throw new AppError("Échec de la mise à jour du statut", {
statusCode: 500,
});
}

await AgrementService.trackEvent({
agrementId,
source: "usager",
type: AGREMENT_HISTORY_TYPE.STATUT_CHANGE,
typePrecision: statut,
usagerUserId: Number(usagerUserId),
});

if (statut === AGREMENT_STATUT.TRANSMIS) {
const email = agrement.user_mail;
if (email) {
try {
await mailService.send(
MailUtils.usagers.agrement.sendStatutTransmisMail({ email }),
);
} catch (e) {
log.w("Erreur lors de l'envoi de l'email de transmission", e);
}
} else {
log.w("Aucun email trouvé pour l'agrément", agrementId);
}
}
return true;
},
};
33 changes: 33 additions & 0 deletions packages/backend/src/utils/mail.js
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,39 @@ module.exports = {
return params;
},
},
agrement: {
sendStatutTransmisMail: ({ email }) => {
log.i("sendStatutTransmisMail - In", { email });
if (!email) {
throw new AppError(
"Email manquant pour l'envoi du mail de transmission d'agrément",
);
}
const html = sendTemplate.getBody(
"VAO - Demande d'agrément transmise",
[
{
p: [
"Bonjour,",
"Votre demande de renouvellement d'agrément a bien été transmise.",
assistanceText,
],
type: "p",
},
],
`L'équipe du SI VAO<BR><a href=${frontUsagersDomain}>Portail VAO</a>`,
);
const params = {
from: senderEmail,
html,
replyTo: senderEmail,
subject: "VAO - Demande d'agrément transmise",
to: email,
};
log.d("sendStatutTransmisMail post email", { params });
return params;
},
},
authentication: {
sendAccountAlreadyExist: ({ email }) => {
log.i("sendAccountAlreadyExist - In", {
Expand Down
Loading
Loading