betagouv/service-national-universel

View on GitHub
api/src/crons/JeVeuxAiderDaily.js

Summary

Maintainability
C
7 hrs
Test Coverage
const fetch = require("node-fetch");
const path = require("path");
const { addYears, addHours } = require("date-fns");
const fileName = path.basename(__filename, ".js");
const { capture } = require("../sentry");
const config = require("config");
const slack = require("../slack");
const { MissionModel } = require("../models");
const { StructureModel } = require("../models");
const { ReferentModel } = require("../models");

const { sendTemplate } = require("../brevo");

const { ROLES, departmentLookUp, department2region, MISSION_DOMAINS, MISSION_STATUS, SENDINBLUE_TEMPLATES } = require("snu-lib");
const { updateApplicationStatus, updateApplicationTutor } = require("../services/application");
const { getTutorName } = require("../services/mission");

let startTime = new Date();

const fromUser = { firstName: `Cron ${fileName}` };

const jva2SnuDomaines = {
  education: MISSION_DOMAINS.EDUCATION,
  environnement: MISSION_DOMAINS.ENVIRONMENT,
  sport: MISSION_DOMAINS.SPORT,
  sante: MISSION_DOMAINS.HEALTH,
  "solidarite-insertion": MISSION_DOMAINS.SOLIDARITY,
  "prevention-protection": MISSION_DOMAINS.SECURITY,
  "culture-loisirs": MISSION_DOMAINS.CULTURE,
  "benevolat-competences": MISSION_DOMAINS.SOLIDARITY,
  "vivre-ensemble": MISSION_DOMAINS.CITIZENSHIP,
};

const JvaStructureException = [
  "6890", //Fédération Médico-psychosociale à la Personne-Initiative
  "14392", //Autisme espoir
  "13419", //AUTISME ESPOIR VERS L'ÉCOLE
];
const SnuStructureException = [
  "62b9f02788469607439b733a", //Fédération Médico-psychosociale à la Personne-Initiative
  "63762279c6ccb008d446f6bc", //Autisme espoir
  "62de85c299f28207ac826fc5", //AUTISME ESPOIR VERS L'ÉCOLE
];

const fetchMission = (skip = 0) =>
  fetch(`${config.API_ENGAGEMENT_URL}/v2/mission?snu=true&skip=${skip}&limit=50`, {
    headers: { "x-api-key": config.API_ENGAGEMENT_KEY },
    method: "GET",
  })
    .then((response) => response.json())
    .then((result) => sync(result))
    .then((rest) => (rest ? fetchMission(skip + 50) : cleanData()))
    .catch((error) => capture(error));

const fetchStructure = async (id) => {
  return fetch(`https://www.jeveuxaider.gouv.fr/api/api-engagement/organisations/${id}?apikey=${config.JVA_API_KEY}`, {
    method: "GET",
    redirect: "follow",
  })
    .then((response) => response.json())
    .catch((error) => capture(error));
};

const sync = async (result) => {
  if (!result.ok) {
    throw new Error("sync with JVA missions : " + result.code);
  }

  for (let i = 0; i < result.data.length; i++) {
    try {
      const mission = result.data[i];

      //stop sync for exception
      if (JvaStructureException.includes(mission.organizationId)) continue;

      let structure = await StructureModel.findOne({ jvaStructureId: mission.organizationId });

      //stop sync for exception
      if (SnuStructureException.includes(structure?._id.toString())) continue;

      if (!structure) {
        //get JVA struture
        let jvaStructure = await fetchStructure(mission.organizationId);

        //Struct without resp skip
        if (!jvaStructure?.responsables.length) {
          continue;
        }

        //Create responsable
        let newResps = [];
        for (const resp of jvaStructure.responsables) {
          //Check unique email
          const exist = await ReferentModel.findOne({ email: resp.email });
          if (exist) {
            continue;
          }

          //Create referent
          const user = await ReferentModel.create({
            firstName: resp.first_name,
            lastName: resp.last_name,
            email: resp.email,
            phone: resp?.phone !== "null" ? resp?.phone : undefined,
            mobile: resp?.mobile !== "null" ? resp?.mobile : undefined,
            role: ROLES.RESPONSIBLE,
          });
          newResps.push(user);
        }

        //Error on referent creation
        if (!newResps.length) {
          continue;
        }

        //Create structure
        const infoStructure = {
          name: jvaStructure.name,
          description: jvaStructure.description,
          website: jvaStructure.website,
          facebook: jvaStructure.facebook,
          twitter: jvaStructure.twitter,
          instagram: jvaStructure.instagram,
          status: "VALIDATED",
          isNetwork: false,
          address: jvaStructure.address.address,
          zip: jvaStructure.address.zip,
          city: jvaStructure.address.city,
          department: departmentLookUp[jvaStructure.address.department],
          region: department2region[departmentLookUp[jvaStructure.address.department]],
          country: jvaStructure.address.country,
          location: {
            lon: Number(jvaStructure.address.longitude),
            lat: Number(jvaStructure.address.latitude),
          },
          isJvaStructure: "true",
          jvaStructureId: jvaStructure.id,
          jvaRawData: jvaStructure,
        };

        const newStructure = await StructureModel.create(infoStructure);

        //Set structureId to referent
        for (const resp of newResps) {
          resp.set({ structureId: newStructure.id });
          await resp.save({ fromUser });
        }
        structure = newStructure;
      }

      //Get referent mission
      let referentMission = await ReferentModel.findOne({ structureId: structure.id });

      //Create or updade mission
      const startAt = new Date(mission.startAt);
      const endAt = new Date(mission.endAt);
      const [description, actions] = mission.descriptionHtml.split("Objectifs:");

      const infoMission = {
        name: mission.title,
        description,
        actions,
        mainDomain: jva2SnuDomaines[mission.domain],
        startAt: addHours(startAt, 2),
        endAt: mission.endAt ? addHours(endAt, 2) : addYears(startAt, 1),
        placesTotal: mission.snuPlaces,
        frequence: mission.schedule,
        structureId: structure.id,
        structureName: structure.name,
        status: MISSION_STATUS.WAITING_VALIDATION,
        tutorId: referentMission.id,
        tutorName: getTutorName(referentMission),
        zip: mission.postalCode,
        address: mission.address,
        city: mission.city,
        department: mission.department,
        region: mission.region,
        country: "France",
        location: mission.location,
        isJvaMission: true,
        jvaMissionId: mission.clientId,
        apiEngagementId: mission._id,
        jvaRawData: mission,
        lastSyncAt: Date.now(),

        updatedAt: new Date(mission.updatedAt),
        createdAt: new Date(mission.postedAt),
      };

      //Check if mission exist
      const missionExist = await MissionModel.findOne({ jvaMissionId: mission.clientId });
      if (!missionExist) {
        const data = await MissionModel.create({ ...infoMission, placesLeft: mission.snuPlaces });

        //Send mail to responsable department
        const referentsDepartment = await ReferentModel.find({
          department: data.department,
          subRole: { $in: ["manager_department_phase2", "manager_phase2"] },
        });

        if (referentsDepartment?.length) {
          await sendTemplate(SENDINBLUE_TEMPLATES.referent.NEW_MISSION, {
            emailTo: referentsDepartment?.map((referent) => ({ name: `${referent.firstName} ${referent.lastName}`, email: referent.email })),
            params: {
              cta: `${config.ADMIN_URL}/mission/${data._id}`,
            },
          });
        }

        //Send mail to responsable mission
        if (referentMission) {
          await sendTemplate(SENDINBLUE_TEMPLATES.referent.MISSION_WAITING_VALIDATION, {
            emailTo: [{ name: `${referentMission.firstName} ${referentMission.lastName}`, email: referentMission.email }],
            params: {
              missionName: data.name,
            },
          });
        }
      } else {
        const oldMissionTutorId = missionExist.tutorId;
        delete infoMission.status;
        delete infoMission.name;
        delete infoMission.description;
        delete infoMission.actions;
        delete infoMission.frequence;
        const left = missionExist.placesLeft + infoMission.placesTotal - missionExist.placesTotal;
        missionExist.set({ ...infoMission, placesLeft: left });
        await missionExist.save({ fromUser });
        if (oldMissionTutorId !== infoMission.tutorId) {
          await updateApplicationTutor(missionExist, fromUser);
        }
      }
    } catch (e) {
      capture(e);
    }
  }
  return result.skip < result.total ? true : false;
};

const cleanData = async () => {
  slack.info({ title: "sync with JVA missions", text: "I'm cleaning the outdated JVA missions !" });
  try {
    const missionsTodelete = await MissionModel.find({
      lastSyncAt: { $lte: startTime },
      isJvaMission: "true",
      status: { $nin: [MISSION_STATUS.CANCEL, MISSION_STATUS.ARCHIVED, MISSION_STATUS.REFUSED] },
    });
    //Cancel mission
    for (const mission of missionsTodelete) {
      mission.set({ status: MISSION_STATUS.CANCEL });
      await mission.save({ fromUser });

      //Check and cancel application link to the mission
      await updateApplicationStatus(mission, fromUser);

      //Send mail to referent mission
      const referent = await ReferentModel.findOne({ _id: mission.tutorId });
      await sendTemplate(SENDINBLUE_TEMPLATES.referent.MISSION_CANCEL, {
        emailTo: [{ name: `${referent.firstName} ${referent.lastName}`, email: referent.email }],
        params: {
          missionName: mission.name,
        },
      });
    }
    await slack.success({ title: "sync with JVA missions" });
  } catch (error) {
    capture(error);
    slack.error({ title: "sync with JVA missions", text: "Error while deleting outdated missions !" });
  }
};

exports.handler = async () => {
  slack.info({ title: "sync with JVA missions", text: "I'm starting the synchronization !" });
  if (!config.JVA_API_KEY) {
    slack.error({ title: "sync with JVA missions", text: "I do not have any JVA_API_KEY !" });
    const err = new Error("NO JVA_API_KEY");
    capture(err);
    throw err;
  }
  if (!config.API_ENGAGEMENT_KEY) {
    slack.error({ title: "sync with API-ENGAGEMENT missions", text: "I do not have any API_ENGAGEMENT_KEY !" });
    const err = new Error("NO API_ENGAGEMENT_KEY");
    capture(err);
    throw err;
  }
  try {
    await fetchMission();
  } catch (e) {
    capture(e);
    throw e;
  }
};