Skip to content

Commit

Permalink
feat: evening and morning notifs (#380)
Browse files Browse the repository at this point in the history
  • Loading branch information
arnaudambro authored Jan 29, 2024
1 parent b9f662d commit 88808dd
Show file tree
Hide file tree
Showing 22 changed files with 994 additions and 84 deletions.
Binary file modified api-node/.yarn/install-state.gz
Binary file not shown.
1 change: 1 addition & 0 deletions api-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"date-fns-tz": "^2.0.0",
"dayjs": "^1.11.10",
"dotenv": "^16.3.1",
"expo-server-sdk": "^3.7.0",
"express": "^4.18.2",
"helmet": "^7.1.0",
"morgan": "^1.10.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "appversion" TEXT,
ADD COLUMN "appversion_build" TEXT;

-- CreateTable
CREATE TABLE "Notification" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"data" TEXT NOT NULL,
"ticket" TEXT,
"error" TEXT,
"push_notif_token" TEXT,
"appversion" TEXT,
"appbuild" TEXT,
"appdevice" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "Notification_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
10 changes: 10 additions & 0 deletions api-node/prisma/migrations/20240129161830_user_app/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
Warnings:
- You are about to drop the column `appversion_build` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "appversion_build",
ADD COLUMN "appbuild" TEXT,
ADD COLUMN "appdevice" TEXT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
Warnings:
- Added the required column `indicatorId` to the `Notification` table without a default value. This is not possible if the table is not empty.
- Added the required column `indicatorSlug` to the `Notification` table without a default value. This is not possible if the table is not empty.
- Added the required column `indicatorValue` to the `Notification` table without a default value. This is not possible if the table is not empty.
- Made the column `push_notif_token` on table `Notification` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "Notification" ADD COLUMN "indicatorId" TEXT NOT NULL,
ADD COLUMN "indicatorSlug" "IndicatorsSlugEnum" NOT NULL,
ADD COLUMN "indicatorValue" INTEGER NOT NULL,
ADD COLUMN "recommandationId" TEXT,
ADD COLUMN "typeWeatherAlert" TEXT,
ALTER COLUMN "push_notif_token" SET NOT NULL;
26 changes: 26 additions & 0 deletions api-node/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ enum IndicatorsSlugEnum {
model User {
id String @id @default(uuid())
matomo_id String @unique
appversion String?
appbuild String?
appdevice String?
municipality_insee_code String? // code INSEE
municipality_name String?
municipality_zip_code String?
Expand All @@ -35,6 +38,7 @@ model User {
updated_at DateTime @default(now()) @updatedAt
favorite_indicator IndicatorsSlugEnum?
notifications_preference NotifationEnum[]
notifications Notification[]
@@index([push_notif_token], type: Hash)
}
Expand Down Expand Up @@ -257,3 +261,25 @@ model Recommandation {
@@index([indicator, indicator_value], name: "recommandation_indeicator_indicator_value")
}

model Notification {
id String @id @default(uuid())
user_id String
user User @relation(fields: [user_id], references: [id])
title String
body String
data String
ticket String? // for debug purpose
error String? // for debug purpose
push_notif_token String // for debug purpose
appversion String? // for debug purpose
appbuild String? // for debug purpose
appdevice String? // for debug purpose
indicatorSlug IndicatorsSlugEnum
indicatorId String
indicatorValue Int
recommandationId String?
typeWeatherAlert String?
created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt
}
3 changes: 2 additions & 1 deletion api-node/src/aggregators/weather_alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
import { PORTAL_API_METEOFRANCE_API_KEY } from '~/config';
import { departments, departmentsCoastalArea } from '~/utils/departments';
import { getPhenomenonDBKeyById } from '~/utils/weather_alert';

import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
/*
Documentation:
Réponse de l'api: https://donneespubliques.meteofrance.fr/client/document/descriptiftechnique_vigilancemetropole_donneespubliques_v4_20230911_307.pdf
Expand Down
8 changes: 4 additions & 4 deletions api-node/src/controllers/indicators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import type { Indicator } from '~/types/api/indicator';
import type { CustomError } from '~/types/error';
import type { RequestWithUser } from '~/types/request';
import { getIndiceUvFromMunicipalityAndDate } from '~/getters/indice_uv';
import { getIndiceAtmoFromMunicipalityAndDate } from '~/getters/indice_atmo';
import { getPollensFromMunicipalityAndDate } from '~/getters/pollens';
import { getWeatherAlertFromMunicipalityAndDate } from '~/getters/weather_alert';
import { indicatorsList } from '~/getters/indicators_list';
import { indicatorsMock } from './mocks/indicators';
import { withUser } from '~/middlewares/auth';
import utc from 'dayjs/plugin/utc';
import { getIndiceAtmoFromMunicipalityAndDate } from '~/getters/indice_atmo';
import { getPollensFromMunicipalityAndDate } from '~/getters/pollens';
import { getWeatherAlertFromMunicipalityAndDate } from '~/getters/weather_alert';
dayjs.extend(utc);

const router = express.Router();
Expand Down Expand Up @@ -83,7 +83,7 @@ router.get(
next(weatherAlert);
return;
}
console.log('weatherAlert', weatherAlert);

if (weatherAlert) indicators.push(weatherAlert);

indicators.push(...indicatorsMock);
Expand Down
9 changes: 9 additions & 0 deletions api-node/src/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ router.put(
if (bodyHasProperty('favorite_indicator')) {
updatedUser.favorite_indicator = req.body.favorite_indicator;
}
if (bodyHasProperty('appversion')) {
updatedUser.appversion = `${req.body.appversion}`;
}
if (bodyHasProperty('appbuild')) {
updatedUser.appbuild = `${req.body.appbuild}`;
}
if (bodyHasProperty('appdevice')) {
updatedUser.appdevice = req.body.appdevice;
}
if (bodyHasProperty('notifications_preference')) {
updatedUser.notifications_preference =
req.body.notifications_preference;
Expand Down
6 changes: 5 additions & 1 deletion api-node/src/cronjobs/aggregators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export async function initAggregators() {
// sont à effectuer 1 à 2 fois par jour, en dehors de ces horaires.
cronTime: '15 15,18 * * *', // every day at 3:15pm and 6:15pm
job: getAtmoIndicator,
runOnInit: true,
}),
)
.then(
Expand All @@ -45,6 +46,7 @@ export async function initAggregators() {
// Data is valid from the day of issue until 7 days later
cronTime: '5 0/4 * * *', // every day starting at 00:05 every 4 hours
job: getPollensIndicator,
runOnInit: true,
}),
)
.then(
Expand All @@ -55,6 +57,7 @@ export async function initAggregators() {
// Data is available for the current day, J+1 and J+2
cronTime: '10 8 * * *', // every day at 8:10am
job: getIndiceUVIndicator,
runOnInit: true,
}),
)
.then(
Expand All @@ -65,10 +68,11 @@ export async function initAggregators() {
// Data is available for the current day, J+1 and J+2
cronTime: '20 * * * *', // every day every hour at HH:20
job: getWeatherAlert,
runOnInit: true,
}),
)
.then(() => {
console.log('All cron jobs are set up');
console.log('All aggregators cron jobs are set up');
})
.catch(capture);
}
5 changes: 3 additions & 2 deletions api-node/src/cronjobs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import '~/prisma';

import { initAggregators } from './aggregators';
import { initRecommandations } from './recommandations';
import { initNotifications } from './notifications';

Promise.resolve()
.then(initRecommandations) //
.then(initAggregators); //
// .then(initNotifications) // TODO: uncomment when notifications are ready
.then(initAggregators) //
.then(initNotifications); //
48 changes: 48 additions & 0 deletions api-node/src/cronjobs/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { setupCronJob } from './utils';
import { capture } from '~/third-parties/sentry';
import {
sendEveningNotification,
sendMorningNotification,
} from '~/utils/notifications';

/*
*
*
Initialization of the cron jobs
We call them one after the other,
in order to avoid to launch them all at the same time
and have logs that are mixed and not readable.
Test it: run `npm run dev-cronjobs` and check the logs
*/

export async function initNotifications() {
await Promise.resolve()
.then(() => {
console.log('Inside notifications cronjobs');
})
.then(
async () =>
await setupCronJob({
name: 'Morning notifications',
cronTime: '0 7 * * *',
job: sendMorningNotification,
runOnInit: false,
}),
)
.then(
async () =>
await setupCronJob({
name: 'Evening notifications',
cronTime: '21 20 * * *',
job: sendEveningNotification,
runOnInit: false,
}),
)
.then(() => {
console.log('All notifications cron jobs are set up');
})
.catch(capture);
}
1 change: 1 addition & 0 deletions api-node/src/cronjobs/recommandations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export async function initRecommandations() {
name: 'Recommandations from Google Sheet',
cronTime: '0/5 * * * *', // every 5 minutes TODO: remove this when recos are more stable
job: getRecommandationsFromGoogleSheet,
runOnInit: true,
}),
)
.then(() => {
Expand Down
7 changes: 6 additions & 1 deletion api-node/src/cronjobs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ interface SetupCronJob {
cronTime: string;
name: string;
job: TaskFn;
runOnInit: boolean;
}

export async function setupCronJob({
cronTime,
name,
job,
runOnInit = true,
}: SetupCronJob): Promise<boolean> {
return await new Promise((resolve) => {
cron.CronJob.from({
Expand All @@ -86,9 +88,12 @@ export async function setupCronJob({
console.log(`${name}: next run at ${cron.sendAt(cronTime).toISO()}`);
resolve(cronStarted);
},
runOnInit: true,
runOnInit,
start: true,
timeZone: 'Europe/Paris',
});
if (!runOnInit) {
resolve(true);
}
});
}
75 changes: 52 additions & 23 deletions api-node/src/getters/indice_atmo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,9 @@ async function getIndiceAtmoFromMunicipalityAndDate({
throw error;
}

const indice_atmo_j0 = await prisma.indiceAtmospheric.findFirst({
where: {
municipality_insee_code,
data_availability: DataAvailabilityEnum.AVAILABLE,
validity_start: {
lte: dayjs(date_UTC_ISO).utc().startOf('day').toISOString(),
},
},
orderBy: [{ diffusion_date: 'desc' }, { validity_start: 'desc' }],
const indice_atmo_j0 = await getIndiceAtmoForJ0({
municipality_insee_code,
date_UTC_ISO,
});

if (indice_atmo_j0?.code_qual == null) {
Expand All @@ -62,19 +56,9 @@ async function getIndiceAtmoFromMunicipalityAndDate({
return null;
}

const indice_atmo_j1 = await prisma.indiceAtmospheric.findFirst({
where: {
municipality_insee_code,
data_availability: DataAvailabilityEnum.AVAILABLE,
validity_start: {
lte: dayjs(date_UTC_ISO)
.utc()
.add(1, 'day')
.startOf('day')
.toISOString(),
},
},
orderBy: [{ diffusion_date: 'desc' }, { validity_start: 'desc' }],
const indice_atmo_j1 = await getIndiceAtmoForJ1({
municipality_insee_code,
date_UTC_ISO,
});

const recommandationsJ0 = await prisma.recommandation
Expand Down Expand Up @@ -213,4 +197,49 @@ async function getIndiceAtmoFromMunicipalityAndDate({
return indiceAtmoIndicator;
}

export { getIndiceAtmoFromMunicipalityAndDate };
async function getIndiceAtmoForJ0({
municipality_insee_code,
date_UTC_ISO,
}: {
municipality_insee_code: Municipality['COM'];
date_UTC_ISO: string | undefined;
}) {
const indice_atmo_j0 = await prisma.indiceAtmospheric.findFirst({
where: {
municipality_insee_code,
data_availability: DataAvailabilityEnum.AVAILABLE,
validity_start: {
lte: dayjs(date_UTC_ISO).utc().toISOString(),
},
},
orderBy: [{ diffusion_date: 'desc' }, { validity_start: 'desc' }],
});
return indice_atmo_j0;
}

async function getIndiceAtmoForJ1({
municipality_insee_code,
date_UTC_ISO,
}: {
municipality_insee_code: Municipality['COM'];
date_UTC_ISO: string | undefined;
}) {
const indice_atmo_j1 = await prisma.indiceAtmospheric.findFirst({
where: {
municipality_insee_code,
data_availability: DataAvailabilityEnum.AVAILABLE,
validity_start: {
lte: dayjs(date_UTC_ISO).utc().add(1, 'day').toISOString(),
},
},
orderBy: [{ diffusion_date: 'desc' }, { validity_start: 'desc' }],
});

return indice_atmo_j1;
}

export {
getIndiceAtmoFromMunicipalityAndDate,
getIndiceAtmoForJ0,
getIndiceAtmoForJ1,
};
Loading

0 comments on commit 88808dd

Please sign in to comment.