chore: remove stats v1

This commit is contained in:
Sébastien Arod 2024-12-18 09:10:45 +01:00
parent f8109fe524
commit b35a479c49
12 changed files with 45 additions and 612 deletions

View file

@ -1,9 +1,7 @@
import { Client } from "@notionhq/client";
import { writeFileSync } from "fs";
import { fetchFamiliesWithEventsFromNotion } from "./notion/fetch/fetchFamiliesWithEventsFromNotion";
import { publishStatisticsToNotion } from "./notion/publish/v1/publishStatisticsToNotion";
import { publishStatsToPage } from "./notion/publish/v2/publishStatsToPage";
import { computeELStats } from "./statistiques/v1/computeELStats";
import { computeStatsPenales } from "./statistiques/v2/penales/computeStatsPenales";
import { statsPenalesDesc } from "./statistiques/v2/penales/StatsPenales";
import { computeStatsGenerales } from "./statistiques/v2/generales/computeStatsGenerales";
@ -20,6 +18,7 @@ import { typeEvenementsProcedurePenale } from "./data/TypeEvenementsPenal";
import { nettoyerDonneesFamilles } from "./data/nettoyage/familles/preparerDonneesFamilles";
import { statsAutresDesc } from "./statistiques/v2/autres/StatsAutres";
import { computeStatsAutres } from "./statistiques/v2/autres/computeStatsAutres";
import { updateUpdateDate as updateRootPageUpdateDate } from "./notion/publish/updateUpdateDate";
type ProcessOptions = {
dryRun: boolean;
@ -105,10 +104,7 @@ function buildProcessOptions(): ProcessOptions {
const currentDate = new Date(Date.now());
console.log("Calcul des statistiques...");
const elStats = computeELStats(familles, currentDate);
const statsGenerales = computeStatsGenerales(familles);
const statsPenales = computeStatsPenales(familles);
const statsSociales = computeStatsSociales(familles);
const statsAutres = computeStatsAutres(familles);
@ -127,7 +123,6 @@ function buildProcessOptions(): ProcessOptions {
console.log(
"Dry run => Pas de publication. Les stats sont écrite localement dans el-stats-xxx"
);
writeFileSync("./el-stats-v1.json", JSON.stringify(elStats, null, " "));
writeFileSync(
"./el-stats-v2.json",
@ -145,7 +140,7 @@ function buildProcessOptions(): ProcessOptions {
);
} else {
console.log("Publishing statistics...");
await publishStatisticsToNotion(elStats, currentDate, notionClient);
await updateRootPageUpdateDate(notionClient, currentDate);
const header = `Notes:
- Dernière mise à jour le ${formatDate(currentDate, "dd/MM/yyyy à HH:mm")}

View file

@ -0,0 +1,43 @@
import { Client, isFullBlock } from "@notionhq/client";
import { listAllChildrenBlocks } from "../utils/listAllChildrenBlocks";
import { richTextToPlainText } from "../utils/text/richTextToPlainText";
export const statsRootPageId = "2b91cd90e3694e96bb196d69aeca59b1";
export async function updateUpdateDate(notionClient: Client, updateDate: Date) {
const childrenBlocks = (
await listAllChildrenBlocks(notionClient, {
block_id: statsRootPageId,
})
).filter(isFullBlock);
const blockTextPrefix = "Dernière mise à jour des statistiques : ";
const block = childrenBlocks.find(
(b) => b.type === "paragraph" &&
richTextToPlainText(b.paragraph.rich_text).startsWith(blockTextPrefix)
);
if (!block) {
console.log(`Could not find block starting with "${blockTextPrefix}"`);
return;
}
await notionClient.blocks.update({
block_id: block?.id,
paragraph: {
rich_text: [
{
text: {
content: blockTextPrefix,
},
},
{
mention: {
date: {
start: updateDate.toISOString(),
},
},
},
],
},
});
}

View file

@ -1,179 +0,0 @@
import { Client, isFullPage } from "@notionhq/client";
import {
PageObjectResponse,
UpdateDatabaseParameters,
} from "@notionhq/client/build/src/api-endpoints";
import { formatValueWithEvol } from "../../../format/formatValueWithEvol";
import {
ELStatsPeriod,
PeriodStatsValues,
ValueWithEvol,
} from "../../../statistiques/v1/ELStats";
import { titlePropertyToText } from "../../utils/properties/titlePropertyToText";
import { queryAllDbResults } from "../../utils/queryAllDbResults";
import { removeBlocks } from "../../utils/removeBlocks";
import { CreatePageProperties } from "../../utils/types/CreatePageProperties";
import { StatPublishOptions, statPublishOptions } from "./statPublishOptions";
const periodeDbPropertyName = "Période";
export async function publishPeriodStats(
notionClient: Client,
periodStatsDbId: string,
statsPeriods: ELStatsPeriod[]
) {
if (statsPeriods.length > 0) {
await updateDbProps(notionClient, periodStatsDbId, statsPeriods[0].stats);
}
const periodRows = (
await queryAllDbResults(notionClient, {
database_id: periodStatsDbId,
})
).filter(isFullPage);
const indexedPeriodRows: { [period: string]: PageObjectResponse } =
Object.fromEntries(
periodRows.map((r) => [
titlePropertyToText(r.properties, periodeDbPropertyName),
r,
])
);
const indexedPeriodStats = Object.fromEntries(
statsPeriods.map((stat) => [stat.periodId, stat])
);
const rowIdsToDelete = Object.entries(indexedPeriodRows)
.filter(
([periodId]) =>
!Object.prototype.hasOwnProperty.call(indexedPeriodStats, periodId)
)
.map(([, row]) => row.id);
const periodIdsToUpdate = Object.entries(indexedPeriodRows)
.filter(([periodId]) =>
Object.prototype.hasOwnProperty.call(indexedPeriodStats, periodId)
)
.map(([periodId]) => periodId);
const periodIdsToCreate = Object.entries(indexedPeriodStats)
.filter(
([periodId]) =>
!Object.prototype.hasOwnProperty.call(indexedPeriodRows, periodId)
)
.map(([periodId]) => periodId);
// Delete rows to delte
await removeBlocks(notionClient, rowIdsToDelete);
// Create rows to create
for (const periodId of periodIdsToCreate) {
const periodStats = indexedPeriodStats[periodId];
await notionClient.pages.create({
parent: {
database_id: periodStatsDbId,
},
properties: buildRowPropertiesForUpsert(periodId, periodStats.stats),
});
}
// Update rows
for (const periodId of periodIdsToUpdate) {
const periodStats = indexedPeriodStats[periodId];
const row = indexedPeriodRows[periodId];
await notionClient.pages.update({
page_id: row.id,
properties: buildRowPropertiesForUpsert(periodId, periodStats.stats),
});
}
}
async function updateDbProps(
notionClient: Client,
periodStatsDbId: string,
stats: PeriodStatsValues<ValueWithEvol>
) {
const db = await notionClient.databases.retrieve({
database_id: periodStatsDbId,
});
const statsNotionProps: UpdateDatabaseParameters["properties"] =
Object.fromEntries(
(Object.keys(stats) as Array<keyof PeriodStatsValues<ValueWithEvol>>).map(
(jsProp) => {
const publishOptions = statPublishOptions(jsProp);
return [
publishOptions.notionPropName,
{
rich_text: {},
},
];
}
)
);
const propsToRemove = Object.fromEntries(
Object.keys(db.properties)
.filter(
(k) =>
!Object.prototype.hasOwnProperty.call(statsNotionProps, k) &&
k !== periodeDbPropertyName
)
.map((k) => [k, null])
);
await notionClient.databases.update({
database_id: periodStatsDbId,
properties: {
[periodeDbPropertyName]: {
title: {},
},
...statsNotionProps,
...propsToRemove,
},
});
}
function buildRowPropertiesForUpsert(
periodId: string,
stats: PeriodStatsValues<ValueWithEvol>
): CreatePageProperties {
const statsNotionProps: CreatePageProperties = Object.fromEntries(
(Object.keys(stats) as Array<keyof PeriodStatsValues<ValueWithEvol>>).map(
(jsProp) => {
const value: ValueWithEvol = stats[jsProp];
const publishOptions = statPublishOptions(jsProp);
return [
publishOptions.notionPropName,
valueWithEvolProp(value, publishOptions),
];
}
)
);
return {
[periodeDbPropertyName]: {
title: [
{
text: {
content: periodId,
},
},
],
},
...statsNotionProps,
};
}
function valueWithEvolProp(
n: ValueWithEvol,
publishOptions: StatPublishOptions
) {
const formatted = formatValueWithEvol(n, publishOptions);
return {
rich_text: [
{
text: {
content: formatted,
},
},
],
};
}

View file

@ -1,63 +0,0 @@
import { Client, isFullBlock } from "@notionhq/client";
import { ELStats } from "../../../statistiques/v1/ELStats";
import { listAllChildrenBlocks } from "../../utils/listAllChildrenBlocks";
import { richTextToPlainText } from "../../utils/text/richTextToPlainText";
import { publishPeriodStats } from "./publishPeriodStats";
import { publishStatsActuelles } from "./publishStatsActuelles";
export const statsPageId = "2b91cd90e3694e96bb196d69aeca59b1";
export const currentStatsHeading = "Statistiques v1";
const yearStatsDb = "4b19a72aa07840eab948525ea41878ee";
const monthStatsDb = "8418a8a4a7544f6a8c54e6003be7efe5";
export async function publishStatisticsToNotion(
stats: ELStats,
currentDate: Date,
notionClient: Client
) {
await updateUpdateDate(notionClient, currentDate);
await publishStatsActuelles(notionClient, stats.actuelles);
await publishPeriodStats(notionClient, yearStatsDb, stats.annees);
await publishPeriodStats(notionClient, monthStatsDb, stats.mois);
}
async function updateUpdateDate(notionClient: Client, updateDate: Date) {
const childrenBlocks = (
await listAllChildrenBlocks(notionClient, {
block_id: statsPageId,
})
).filter(isFullBlock);
const blockTextPrefix = "Dernière mise à jour des statistiques : ";
const block = childrenBlocks.find(
(b) =>
b.type === "paragraph" &&
richTextToPlainText(b.paragraph.rich_text).startsWith(blockTextPrefix)
);
if (!block) {
console.log(`Could not find block starting with "${blockTextPrefix}"`);
return;
}
await notionClient.blocks.update({
block_id: block?.id,
paragraph: {
rich_text: [
{
text: {
content: blockTextPrefix,
},
},
{
mention: {
date: {
start: updateDate.toISOString(),
},
},
},
],
},
});
}

View file

@ -1,81 +0,0 @@
import { Client, isFullBlock } from "@notionhq/client";
import { BlockObjectRequest } from "@notionhq/client/build/src/api-endpoints";
import { formatValue } from "../../../format/formatValue";
import { ELStatsAtDate } from "../../../statistiques/v1/ELStats";
import { listAllChildrenBlocks } from "../../utils/listAllChildrenBlocks";
import { removeBlocks } from "../../utils/removeBlocks";
import { richTextToPlainText } from "../../utils/text/richTextToPlainText";
import { currentStatsHeading, statsPageId } from "./publishStatisticsToNotion";
import { StatPublishOptions, statPublishOptions } from "./statPublishOptions";
export async function publishStatsActuelles(
notionClient: Client,
statsActuelles: ELStatsAtDate<number>
) {
const newBlocks = (
Object.keys(statsActuelles) as Array<keyof typeof statsActuelles>
).map((jsProp) => {
const value: number = statsActuelles[jsProp];
const publishOptions = statPublishOptions(jsProp);
return currentStatBlock(value, publishOptions);
});
await updateStatsActuellesBlocks(notionClient, newBlocks);
}
function currentStatBlock(
value: number,
publishOptions: StatPublishOptions
): BlockObjectRequest {
const formattedValue = formatValue(value, publishOptions);
const content = `${publishOptions.notionPropName} : ${formattedValue}`;
return {
paragraph: {
rich_text: [
{
text: {
content: content,
},
},
],
},
};
}
async function updateStatsActuellesBlocks(
notionClient: Client,
blocks: BlockObjectRequest[]
) {
const childrenBlocks = (
await listAllChildrenBlocks(notionClient, {
block_id: statsPageId,
})
).filter(isFullBlock);
const currentStatsHeadingBlockIndex = childrenBlocks.findIndex(
(block) =>
block.type === "heading_1" &&
richTextToPlainText(block.heading_1.rich_text) === currentStatsHeading
);
const endOfCurrentStats = childrenBlocks.findIndex(
(block, index) =>
index > currentStatsHeadingBlockIndex && block.type === "heading_1"
);
if (currentStatsHeadingBlockIndex === -1 || endOfCurrentStats === -1) {
throw new Error("Cannot find expected headings in statistics page");
}
const currentStatsHeadingBlock =
childrenBlocks[currentStatsHeadingBlockIndex];
const blocksIdsToRemove = childrenBlocks
.slice(currentStatsHeadingBlockIndex + 1, endOfCurrentStats)
.map((b) => b.id);
await removeBlocks(notionClient, blocksIdsToRemove);
await notionClient.blocks.children.append({
block_id: statsPageId,
after: currentStatsHeadingBlock.id,
children: [...blocks],
});
}

View file

@ -1,33 +0,0 @@
import { EvolFormatOptions } from "../../../format/EvolFormatOptions";
import { ValueFormatOptions } from "../../../format/ValueFormatOptions";
import { AllStatsPropNames } from "../../../statistiques/v1/ELStats";
export function statPublishOptions(
statJsPropName: AllStatsPropNames
): StatPublishOptions {
return statPropsPublishOptions[statJsPropName];
}
const statPropsPublishOptions: {
[jsPropName in AllStatsPropNames]: StatPublishOptions;
} = {
// Autre
nbFamillesAvecContrôleFiscal: {
notionPropName: "Nb familles ayant eu un contrôle fiscal",
},
pourcentageFamillesAvecContrôleFiscal: {
notionPropName: "% familles ayant eu un contrôle fiscal",
unit: "%",
},
nbFamillesAvecContrôleURSSAF: {
notionPropName: "Nb familles ayant eu un contrôle URSAFF",
},
pourcentageFamillesAvecContrôleURSSAF: {
notionPropName: "% familles ayant eu un contrôle URSAFF",
unit: "%",
},
};
export type StatPublishOptions = {
notionPropName: string;
} & ValueFormatOptions &
EvolFormatOptions;

View file

@ -1,29 +0,0 @@
export type ELStats = {
actuelles: ELStatsAtDate<number>;
annees: ELStatsPeriod[];
mois: ELStatsPeriod[];
};
export type ELStatsAtDate<V> = {
// Autre
nbFamillesAvecContrôleFiscal: V;
pourcentageFamillesAvecContrôleFiscal: V;
nbFamillesAvecContrôleURSSAF: V;
pourcentageFamillesAvecContrôleURSSAF: V;
};
export type ELStatsPeriod = {
periodId: string;
stats: PeriodStatsValues<ValueWithEvol>;
};
export type PeriodStatsValues<V> = ELStatsAtDate<V>;
export type ValueWithEvol = {
value: number;
evol: number;
evolPercent: number;
};
export type AllStatsPropNames = keyof ELStatsAtDate<number>;

View file

@ -1,74 +0,0 @@
import { Famille } from "../../data/Famille";
import { IdentifiedPeriod } from "../../period/IdentifiedPeriod";
import { Period } from "../../period/Period";
import { ELStatsPeriod, PeriodStatsValues, ValueWithEvol } from "./ELStats";
import { computeELStatsAtDate } from "./computeELStatsAtDate";
export function computeELPeriodStats(
familles: Famille[],
periods: IdentifiedPeriod[]
): ELStatsPeriod[] {
let previousPeriodStatNumberValues: PeriodStatsValues<number> | null = null;
return periods.map((period) => {
const periodStatNumberValues: PeriodStatsValues<number> =
computePeriodStatsNumberValues(familles, period);
// Compute evol
const statsWithEvol = computeStatsEvol(
periodStatNumberValues,
previousPeriodStatNumberValues
);
previousPeriodStatNumberValues = periodStatNumberValues;
const periodStats: ELStatsPeriod = {
periodId: period.id,
stats: statsWithEvol,
};
return periodStats;
});
}
function computePeriodStatsNumberValues(
familles: Famille[],
period: Period
): PeriodStatsValues<number> {
return computeELStatsAtDate(familles, period.end);
}
function computeStatsEvol(
periodStatNumberValues: PeriodStatsValues<number>,
previousPeriodStatNumberValues: PeriodStatsValues<number> | null | undefined
): PeriodStatsValues<ValueWithEvol> {
return Object.fromEntries(
(
Object.entries(periodStatNumberValues) as Array<
[prop: keyof PeriodStatsValues<number>, number]
>
).map(([k, v]) => [
k,
valueWithEvol(v, previousPeriodStatNumberValues?.[k]),
])
) as PeriodStatsValues<ValueWithEvol>;
}
function valueWithEvol(
value: number,
previous: number | undefined
): ValueWithEvol {
return {
value: value,
evol: evol(value, previous),
evolPercent: evolPercent(value, previous),
};
}
function evolPercent(current: number, previous: number | undefined): number {
if (previous === undefined) return NaN;
const evolValue = evol(current, previous);
if (evolValue === 0) return 0;
return (100 * evolValue) / previous;
}
function evol(current: number, previous: number | undefined): number {
if (previous === undefined) return NaN;
return current - previous;
}

View file

@ -1,23 +0,0 @@
import { Famille } from "../../data/Famille";
import { computeELPeriodStats } from "./computeELPeriodStats";
import { computeELStatsAtDate } from "./computeELStatsAtDate";
import { ELStats } from "./ELStats";
import { generateELMonths } from "../../period/generateELMonths";
import { generateELYears } from "../../period/generateELYears";
export function computeELStats(
families: Famille[],
currentDate: Date
): ELStats {
const actuelles = computeELStatsAtDate(families, currentDate);
const yearsStats = computeELPeriodStats(families, generateELYears());
const monthsStats = computeELPeriodStats(families, generateELMonths());
return {
actuelles: actuelles,
annees: yearsStats,
mois: monthsStats,
};
}

View file

@ -1,35 +0,0 @@
import { Famille, isExResistant, isResistant } from "../../data/Famille";
import { percent } from "../../utils/math/percent";
import { ELStatsAtDate } from "./ELStats";
export function computeELStatsAtDate(
familles: Famille[],
asOfDate: Date
): ELStatsAtDate<number> {
const familleResistantesOrEx = familles.filter(
(famille) =>
isResistant(famille, asOfDate) || isExResistant(famille, asOfDate)
);
const famillesAvecContrôleFiscal = familleResistantesOrEx.filter((f) =>
f.EvenementsEL.find((e) => e.Type === "Contrôle fiscal")
);
const famillesAvecContrôleURSAFF = familleResistantesOrEx.filter((f) =>
f.EvenementsEL.find((e) => e.Type === "Contrôle URSSAF")
);
const elStats: ELStatsAtDate<number> = {
// Autre
nbFamillesAvecContrôleFiscal: famillesAvecContrôleFiscal.length,
pourcentageFamillesAvecContrôleFiscal: percent(
famillesAvecContrôleFiscal.length,
familleResistantesOrEx.length
),
nbFamillesAvecContrôleURSSAF: famillesAvecContrôleURSAFF.length,
pourcentageFamillesAvecContrôleURSSAF: percent(
famillesAvecContrôleURSAFF.length,
familleResistantesOrEx.length
),
};
return elStats;
}

View file

@ -1,71 +0,0 @@
import { addMonths } from "date-fns";
import {
EvenementFamille,
isEvenementBefore,
} from "../../data/EvenementFamille";
import { Famille } from "../../data/Famille";
import { percent } from "../../utils/math/percent";
import { statutExResistant, statutResistant } from "../../data/StatutFamille";
type FamillesWithEventsConditionInEarlyPeriod = {
[name: string]: {
nbFamillesWithAtLeastDuration: number;
nbFamillesWithEventPredicate: number;
percentage: number;
};
};
export const computeFamillesWithEventsConditionInEarlyPeriod = (
familles: Famille[],
eventsPredicate: (events: EvenementFamille[]) => boolean,
durations: {
[name: string]: number;
} = {
"0 mois": 0,
"3 mois": 3,
"6 mois": 6,
"12 mois": 12,
"24 mois": 24,
}
): FamillesWithEventsConditionInEarlyPeriod => {
const evalDate = new Date(Date.now());
return Object.fromEntries(
Object.entries(durations).map(([name, months]) => {
const famillesWithAtLeastDurationOfDc = familles
.filter(
(f) => f.Statut === statutResistant || f.Statut === statutExResistant
)
.filter(
(f) =>
dcStartDate(f) !== null &&
addMonths(dcStartDate(f)!, months) < evalDate
);
const famillesWithEventPredicate = famillesWithAtLeastDurationOfDc.filter(
(f) => {
const dcDate = dcStartDate(f)!;
const dcPeriodEnd = addMonths(dcDate, months);
const eventsBeforeDate = f.EvenementsEL.filter((e) =>
isEvenementBefore(e, dcPeriodEnd)
);
return eventsPredicate(eventsBeforeDate);
}
);
return [
name,
{
nbFamillesWithAtLeastDuration: famillesWithAtLeastDurationOfDc.length,
nbFamillesWithEventPredicate: famillesWithEventPredicate.length,
percentage: percent(
famillesWithEventPredicate.length,
famillesWithAtLeastDurationOfDc.length
),
},
];
})
);
};
const dcStartDate = (famille: Famille): Date | null => {
return famille.Integration;
};

View file

@ -1,17 +0,0 @@
import { Famille } from "../../data/Famille";
import { percent } from "../../utils/math/percent";
export function computePourcentageEntreeApresMiseEnDemeure(
familles: Famille[]
) {
const entreeApresMiseEnDemeure = familles.filter(
(f) =>
f.ContexteEntree === "Après mise en demeure" ||
f.ContexteEntree === "Après poursuite procureur"
);
const pourcentageEntreeApresMiseEnDemeure = percent(
entreeApresMiseEnDemeure.length,
familles.length
);
return pourcentageEntreeApresMiseEnDemeure;
}