betagouv/service-national-universel

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

Summary

Maintainability
A
0 mins
Test Coverage
const esClient = require("../es");
const path = require("path");

const { capture } = require("../sentry");
const { YoungModel, CohortModel } = require("../models");

const { sendTemplate } = require("../brevo");
const slack = require("../slack");
const { SENDINBLUE_TEMPLATES, translate, formatStringDate } = require("snu-lib");
const config = require("config");
const { getCcOfYoung } = require("../utils");
const fileName = path.basename(__filename, ".js");

exports.handler = async () => {
  try {
    let countTotal = 0;
    let countHit = 0;
    let countMissionSent = {};
    let countMissionSentCohort = {};

    const oneYearAgo = new Date();
    oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);

    const cohorts = await CohortModel.find({ dateEnd: { $gte: oneYearAgo } });
    const cohortsName = cohorts.map((s) => s.name);

    const cursor = YoungModel.find({
      cohort: { $in: cohortsName },
      status: "VALIDATED",
      statusPhase1: "DONE",
      statusPhase2: { $nin: ["VALIDATED", "WITHDRAWN"] },
    }).cursor();
    await cursor.eachAsync(async function (young) {
      countTotal++;
      const applicationsCount = young?.phase2ApplicationStatus.filter((obj) => ["WAITING_VALIDATION", "WAITING_VERIFICATION"].includes(obj)).length;
      if (applicationsCount < 15) {
        const esMissions = await getMissions({ young });
        const missions = esMissions?.map((mission) => ({
          structureName: mission._source.structureName?.toUpperCase(),
          name: mission._source.name,
          startAt: formatStringDate(mission._source.startAt),
          endAt: formatStringDate(mission._source.endAt),
          address: `${mission._source.city}, ${mission._source.zip}`,
          domains: mission._source.domains?.map(translate)?.join(", "),
          cta: `${config.APP_URL}/mission/${mission._id}`,
        }));
        countMissionSent[missions?.length] = (countMissionSent[missions?.length] || 0) + 1;
        if (!missions) return;
        countMissionSentCohort[young?.cohort] = (countMissionSentCohort[young?.cohort] || 0) + 1;
        if (missions?.length > 0) {
          countHit++;
          // send a mail to the young
          let template = SENDINBLUE_TEMPLATES.young.MISSION_PROPOSITION_AUTO;
          let cc = getCcOfYoung({ template, young });
          await sendTemplate(template, {
            emailTo: [{ name: `${young.firstName} ${young.lastName}`, email: young.email }],
            params: {
              missions,
              cta: `${config.APP_URL}/mission?utm_campaign=transactionnel+nouvelles+mig+publiees&utm_source=notifauto&utm_medium=mail+237+acceder`,
            },
            cc,
          });
          // stock the list in young
          const missionsInMail = (young.missionsInMail || []).concat(esMissions?.map((mission) => ({ missionId: mission._id, date: Date.now() })));
          // This is used in order to minimize risk of version conflict.
          const youngForUpdate = await YoungModel.findById(young._id);
          youngForUpdate.set({ missionsInMail });
          await youngForUpdate.save({ fromUser: { firstName: `Cron ${fileName}` } });
        }
      }
    });
    if (countHit === 0) {
      slack.info({
        title: "noticePushMission",
        text: `Pas de jeunes ciblé(e)s.\nmails envoyés: ${countHit}\nPas de missions proposées.`,
      });
    } else {
      slack.info({
        title: "noticePushMission",
        text: `${countHit}/${countTotal} (${((countHit / countTotal) * 100).toFixed(
          2,
        )}%) jeunes ciblé(e)s.\nmails envoyés: ${countHit}\nnombre de missions proposées / mail : ${JSON.stringify(
          countMissionSent,
        )}\ncohortes (si missions proposées) : ${JSON.stringify(countMissionSentCohort)}`,
      });
    }
  } catch (e) {
    capture(e);
    slack.error({ title: "noticePushMission", text: JSON.stringify(e) });
    throw e;
  }
};

const getMissions = async ({ young }) => {
  if (!young || !young.location || !young.location.lat || !young.location.lon) return;
  try {
    const excludedMissionIds = young?.missionsInMail?.map((m) => m.missionId);

    const header = { index: "mission", type: "_doc" };
    const query = {
      bool: {
        must_not: [
          {
            ids: {
              values: excludedMissionIds,
            },
          },
        ],
        filter: [
          // only validated missions...
          { term: { "status.keyword": "VALIDATED" } },
          //... that didn't reach their deadline...
          {
            range: {
              endAt: {
                gte: "now",
              },
            },
          },
          //... that have places left...
          {
            range: {
              placesLeft: {
                gt: 0,
              },
            },
          },
          //... that is in 20 km radius...
          {
            geo_distance: {
              distance: "20km",
              location: young.location,
            },
          },
        ],
      },
    };

    const sort = [
      {
        _geo_distance: {
          location: [young.location.lon, young.location.lat],
          order: "asc",
          unit: "km",
          mode: "min",
        },
      },
    ];

    const body = [
      header,
      {
        query,
        sort,
        size: 3,
      },
    ]
      .map((e) => `${JSON.stringify(e)}\n`)
      .join("");
    const response = await esClient.msearch({
      index: "mission",
      body,
    });
    return response?.body?.responses[0]?.hits?.hits;
  } catch (e) {
    capture(e);
    slack.error({ title: "noticePushMission", text: JSON.stringify(e) });
  }
};