Skip to content

Commit

Permalink
feat(indicateurs): calcul de la trajectoire SNBC
Browse files Browse the repository at this point in the history
  • Loading branch information
dthib committed Jul 23, 2024
1 parent 50f2811 commit e607532
Show file tree
Hide file tree
Showing 17 changed files with 550 additions and 124 deletions.
1 change: 1 addition & 0 deletions .github/workflows/cd-backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,4 @@ jobs:
+backend-deploy
--ENV_NAME=${{ github.event.inputs.target }}
--KOYEB_API_KEY=${{ secrets.KOYEB_API_KEY }}
--TRAJECTOIRE_SNBC_SHEET_ID=${{ vars.TRAJECTOIRE_SNBC_SHEET_ID }}
4 changes: 3 additions & 1 deletion Earthfile
Original file line number Diff line number Diff line change
Expand Up @@ -324,14 +324,16 @@ backend-build:

backend-deploy: ## Déploie le backend dans une app Koyeb existante
ARG --required KOYEB_API_KEY
ARG --required TRAJECTOIRE_SNBC_SHEET_ID
FROM +koyeb
RUN ./koyeb services update $ENV_NAME-backend/backend \
--docker $BACKEND_IMG_NAME \
--env GIT_COMMIT_SHORT_SHA=$GIT_COMMIT_SHORT_SHA \
--env GCLOUD_SERVICE_ACCOUNT_KEY=@GCLOUD_SERVICE_ACCOUNT_KEY \
--env DATABASE_URL=@SUPABASE_DATABASE_URL_$ENV_NAME \
--env SUPABASE_URL=@SUPABASE_URL_$ENV_NAME \
--env SUPABASE_SERVICE_ROLE_KEY=@SUPABASE_SERVICE_ROLE_KEY_$ENV_NAME
--env SUPABASE_SERVICE_ROLE_KEY=@SUPABASE_SERVICE_ROLE_KEY_$ENV_NAME \
--env TRAJECTOIRE_SNBC_SHEET_ID=$TRAJECTOIRE_SNBC_SHEET_ID

app-build: ## construit l'image de l'app
ARG PLATFORM
Expand Down
4 changes: 4 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.4.0",
"@sentry/nestjs": "^8.19.0",
"@sentry/profiling-node": "^8.19.0",
"@supabase/supabase-js": "^2.40.0",
"async-retry": "^1.3.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"drizzle-orm": "^0.32.0",
"gaxios": "^6.7.0",
"google-auth-library": "^9.11.0",
Expand Down
22 changes: 0 additions & 22 deletions backend/src/app.controller.spec.ts

This file was deleted.

33 changes: 0 additions & 33 deletions backend/src/app.controller.ts

This file was deleted.

22 changes: 15 additions & 7 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { SheetModule } from './spreadsheets/sheet.module';
import { CommonModule } from './common/common.module';
import { ConfigModule } from '@nestjs/config';
import { CollectivitesModule } from './collectivites/collectivites.module';
import { CommonModule } from './common/common.module';
import { IndicateursModule } from './indicateurs/indicateurs.module';
import { SheetModule } from './spreadsheets/sheet.module';

@Module({
imports: [CommonModule, SheetModule, CollectivitesModule],
controllers: [AppController],
providers: [AppService],
imports: [
ConfigModule.forRoot({
ignoreEnvFile: process.env.NODE_ENV === 'production', // In production, environment variables are set by the deployment
}),
CommonModule,
SheetModule,
CollectivitesModule,
IndicateursModule,
],
controllers: [],
providers: [],
})
export class AppModule {}
8 changes: 0 additions & 8 deletions backend/src/app.service.ts

This file was deleted.

33 changes: 26 additions & 7 deletions backend/src/collectivites/services/collectivites.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import { boolean, integer, pgTable, serial, text } from 'drizzle-orm/pg-core';
import DatabaseService from '../../common/services/database.service';

@Injectable()
export default class CollectivitesService {
private readonly collectiviteTable = pgTable('collectivite', {
private readonly logger = new Logger(CollectivitesService.name);

public readonly collectiviteTable = pgTable('collectivite', {
id: serial('id').primaryKey(),
access_restreint: boolean('access_restreint'),
});

private readonly epciTable = pgTable('epci', {
public readonly epciTable = pgTable('epci', {
id: serial('id').primaryKey(),
collectivite_id: integer('collectivite_id')
.notNull()
Expand All @@ -21,17 +23,34 @@ export default class CollectivitesService {

constructor(private readonly databaseService: DatabaseService) {}

async getEpciById(id: number) {
return this.databaseService.db
async getEpciByCollectiviteId(collectiviteId: number) {
this.logger.log(
`Récupération de l'epci avec l'identifiant ${collectiviteId}`,
);
const epciByIdResult = await this.databaseService.db
.select()
.from(this.epciTable)
.where(eq(this.epciTable.id, id));
.where(eq(this.epciTable.collectivite_id, collectiviteId));
if (!epciByIdResult?.length) {
throw new NotFoundException(
`EPCI avec l'identifiant de collectivite ${collectiviteId} introuvable`,
);
}

this.logger.log(`Epci trouvé avec l'id ${epciByIdResult[0].id}`);
return epciByIdResult[0];
}

async getEpciBySiren(siren: string) {
return this.databaseService.db
this.logger.log(`Récupération de l'epci à partir du siren ${siren}`);
const epciBySirenResult = await this.databaseService.db
.select()
.from(this.epciTable)
.where(eq(this.epciTable.siren, siren));
if (!epciBySirenResult?.length) {
throw new NotFoundException(`EPCI avec le siren ${siren} introuvable`);
}
this.logger.log(`Epci trouvé avec l'id ${epciBySirenResult[0].id}`);
return epciBySirenResult[0];
}
}
7 changes: 7 additions & 0 deletions backend/src/common/services/optionalBooleanMapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const optionalBooleanMapper = new Map([
['undefined', undefined],
['true', true],
['false', false],
]);

export default optionalBooleanMapper;
18 changes: 18 additions & 0 deletions backend/src/indicateurs/controllers/trajectoires.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Controller, Get, Logger, Query } from '@nestjs/common';
import CalculTrajectoireRequest from '../models/calcultrajectoire.request';
import TrajectoiresService from '../service/trajectoires.service';

@Controller('trajectoires')
export class TrajectoiresController {
private readonly logger = new Logger(TrajectoiresController.name);

constructor(private readonly trajectoiresService: TrajectoiresService) {}

@Get('snbc') // TODO: plutôt un post
calculeTrajectoireSnbc(@Query() request: CalculTrajectoireRequest) {
this.logger.log(
`Calcul de la trajectoire SNBC pour la collectivité ${request.collectivite_id}`,
);
return this.trajectoiresService.calculeTrajectoireSnbc(request);
}
}
15 changes: 15 additions & 0 deletions backend/src/indicateurs/indicateurs.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { CollectivitesModule } from '../collectivites/collectivites.module';
import { CommonModule } from '../common/common.module';
import { SheetModule } from '../spreadsheets/sheet.module';
import { TrajectoiresController } from './controllers/trajectoires.controller';
import IndicateursService from './service/indicateurs.service';
import TrajectoiresService from './service/trajectoires.service';

@Module({
imports: [CommonModule, CollectivitesModule, SheetModule],
providers: [IndicateursService, TrajectoiresService],
exports: [IndicateursService, TrajectoiresService],
controllers: [TrajectoiresController],
})
export class IndicateursModule {}
14 changes: 14 additions & 0 deletions backend/src/indicateurs/models/calcultrajectoire.request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Transform, Type } from 'class-transformer';
import { IsBoolean, IsInt, IsOptional } from 'class-validator';
import optionalBooleanMapper from '../../common/services/optionalBooleanMapper';

export default class CalculTrajectoireRequest {
@IsInt()
@Type(() => Number)
collectivite_id: number;

@IsBoolean()
@Transform(({ value }) => optionalBooleanMapper.get(value)) // Useful for query param
@IsOptional()
conserve_fichier_temporaire?: boolean;
}
125 changes: 125 additions & 0 deletions backend/src/indicateurs/service/indicateurs.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Injectable, Logger } from '@nestjs/common';
import { and, eq, gte, inArray, lte, sql, SQLWrapper } from 'drizzle-orm';
import {
boolean,
date,
doublePrecision,
integer,
pgTable,
serial,
text,
timestamp,
uuid,
} from 'drizzle-orm/pg-core';
import CollectivitesService from '../../collectivites/services/collectivites.service';
import DatabaseService from '../../common/services/database.service';

@Injectable()
export default class IndicateursService {
private readonly logger = new Logger(IndicateursService.name);
public readonly indicateurDefinitionTable;
public readonly indicateurValeurTable;

constructor(
private readonly databaseService: DatabaseService,
private readonly collectivitesService: CollectivitesService,
) {
this.indicateurDefinitionTable = pgTable('indicateur_definition', {
id: serial('id').primaryKey(),
groupement_id: integer('groupement_id'), // TODO: references
collectivite_id: integer('collectivite_id')
.notNull()
.references(() => this.collectivitesService.collectiviteTable.id, {
onDelete: 'cascade',
}),
identifiant_referentiel: text('identifiant_referentiel').unique(),
titre: text('titre').notNull(),
titre_long: text('titre_long'),
description: text('description'),
unite: text('unite').notNull(),
borne_min: doublePrecision('borne_min'),
borne_max: doublePrecision('borne_max'),
participation_score: boolean('participation_score')
.default(false)
.notNull(),
sans_valeur_utilisateur: boolean('sans_valeur_utilisateur')
.default(false)
.notNull(),
valeur_calcule: text('valeur_calcule'),
modified_at: timestamp('modified_at', { withTimezone: true })
.default(sql.raw(`CURRENT_TIMESTAMP`))
.notNull(), // with time zone default CURRENT_TIMESTAMP
created_at: timestamp('created_at', { withTimezone: true })
.default(sql.raw(`CURRENT_TIMESTAMP`))
.notNull(), // with time zone default CURRENT_TIMESTAMP
modified_by: uuid('modified_by'), // TODO: default auth.uid() references auth.users
created_by: uuid('created_by'), // TODO: default auth.uid() references auth.users
});

this.indicateurValeurTable = pgTable('indicateur_valeur', {
id: serial('id').primaryKey(),
collectivite_id: integer('collectivite_id')
.notNull()
.references(() => this.collectivitesService.collectiviteTable.id, {
onDelete: 'cascade',
}),
indicateur_id: integer('indicateur_id')
.notNull()
.references(() => this.indicateurDefinitionTable.id, {
onDelete: 'cascade',
}),
date_valeur: date('date_valeur').notNull(),
metadonnee_id: integer('metadonnee_id'), // TODO: references
resultat: doublePrecision('resultat'),
resultat_commentaire: text('resultat_commentaire'),
objectif: doublePrecision('objectif'),
objectif_commentaire: text('objectif_commentaire'),
estimation: doublePrecision('estimation'),
modified_at: timestamp('modified_at', { withTimezone: true })
.default(sql.raw(`CURRENT_TIMESTAMP`))
.notNull(), // with time zone default CURRENT_TIMESTAMP
created_at: timestamp('created_at', { withTimezone: true })
.default(sql.raw(`CURRENT_TIMESTAMP`))
.notNull(), // with time zone default CURRENT_TIMESTAMP
modified_by: uuid('modified_by'), // TODO: default auth.uid() references auth.users
created_by: uuid('created_by'), // TODO: default auth.uid() references auth.users
});
}

async getReferentielIndicateursValeurs(
collectiviteId: number,
identifiantsReferentiel: string[],
dateDebut?: string,
dateFin?: string,
) {
this.logger.log(
`Récupération des valeurs des indicateurs ${identifiantsReferentiel.join(',')} pour la collectivite ${collectiviteId} et la plage de date ${dateDebut} - ${dateFin}`,
);
const conditions: SQLWrapper[] = [
eq(this.indicateurValeurTable.collectivite_id, collectiviteId),
inArray(
this.indicateurDefinitionTable.identifiant_referentiel,
identifiantsReferentiel,
),
];

if (dateDebut) {
conditions.push(gte(this.indicateurValeurTable.date_valeur, dateDebut));
}
if (dateFin) {
conditions.push(lte(this.indicateurValeurTable.date_valeur, dateFin));
}

return this.databaseService.db
.select()
.from(this.indicateurValeurTable)
.leftJoin(
this.indicateurDefinitionTable,
eq(
this.indicateurValeurTable.indicateur_id,
this.indicateurDefinitionTable.id,
),
)
.where(and(...conditions));
}
}
Loading

0 comments on commit e607532

Please sign in to comment.