Skip to content

Commit b3f234a

Browse files
committed
feat: update the stat API to fill the incubateur requirements
1 parent 72e2383 commit b3f234a

File tree

7 files changed

+95
-151
lines changed

7 files changed

+95
-151
lines changed

apps/nuxt/src/server/api/statistics/index.get.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { defineEventHandler, getQuery } from 'h3'
2+
import { Monitor, StatisticsService } from '@tee/backend-ddd'
3+
import { StatsPeriodicity } from '@tee/common'
4+
5+
export default defineEventHandler(async (event) => {
6+
try {
7+
const query = getQuery(event)
8+
9+
const periodicity = ['day', 'week', 'month', 'year'].includes(query.periodicity as StatsPeriodicity)
10+
? (query.periodicity as StatsPeriodicity)
11+
: undefined
12+
const since = typeof query.since === 'string' ? query.since : undefined
13+
const to = typeof query.to === 'string' ? query.to : undefined
14+
15+
const statsResult = await new StatisticsService().getNorthStarStats({ periodicity, since, to })
16+
17+
return statsResult
18+
} catch (error: any) {
19+
Monitor.error('Error in /api/stats', { error })
20+
throw createError({
21+
statusCode: 500,
22+
statusMessage: `Server internal error: ${error.message}`
23+
})
24+
}
25+
})
File renamed without changes.
Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import { Result } from 'true-myth'
2-
import { StatsData } from '@tee/common'
3-
import { ProgramService } from '../../program'
1+
import { StatOutput, StatQueryParams } from '@tee/common'
42
import StatisticsFeatures from '../domain/statisticsFeatures'
5-
import { brevoRepository } from '../../opportunity/infrastructure/api/brevo/brevoDeal'
63

74
export class StatisticsService {
8-
public async get(): Promise<Result<StatsData, Error>> {
9-
return await new StatisticsFeatures(brevoRepository, new ProgramService()).computeStatistics()
5+
public async getNorthStarStats(params: StatQueryParams): Promise<StatOutput> {
6+
return await new StatisticsFeatures().fetchNorthStarStats(params)
107
}
118
}

libs/backend-ddd/src/statistics/domain/statisticsCache.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 55 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,75 @@
1-
import { DemandsAtTime, OpportunityStats, ProgramStats, StatsData } from '@tee/common'
2-
import { ProgramService } from '../../program/application/programService'
3-
import { Result } from 'true-myth'
4-
import { OpportunityRepository } from '../../opportunity/domain/spi'
5-
import StatisticsCache from './statisticsCache'
6-
import Monitor from '../../common/domain/monitoring/monitor'
1+
import { Stat, StatOutput, StatQueryParams } from '@tee/common'
72

83
export default class StatisticsFeatures {
9-
private readonly _opportunityRepository: OpportunityRepository
10-
private readonly _cache: StatisticsCache
11-
private readonly _programService: ProgramService
4+
async fetchNorthStarStats(params: StatQueryParams): Promise<StatOutput> {
5+
const METABASE_URL = 'http://tee-metabase.osc-fr1.scalingo.io/public/question/6969b2ab-ec49-44a0-9db8-3e2f9afbcf29.json'
126

13-
constructor(opportunityRepository: OpportunityRepository, programService: ProgramService) {
14-
this._opportunityRepository = opportunityRepository
15-
this._cache = StatisticsCache.getInstance()
16-
this._programService = programService
17-
}
18-
19-
async computeStatistics(): Promise<Result<StatsData, Error>> {
20-
if (!this._cache.statistics || !this._cache.isValid()) {
21-
const opportunityStats = await this.getOpportunityStatistics()
22-
const programStats = this.getProgramStatistics()
7+
const periodicity = params.periodicity || 'month'
238

24-
const statistics: StatsData = {
25-
...programStats,
26-
...opportunityStats
27-
}
28-
this._cache.statistics = {
29-
statistics: statistics,
30-
timestamp: Date.now()
31-
}
32-
}
33-
return Result.ok(this._cache.statistics.statistics)
34-
}
35-
36-
async getOpportunityStatistics(): Promise<OpportunityStats> {
37-
const opportunitiesDates = await this.getOpportunitiesCreated()
38-
let datesWithinLast30Days = 0
39-
let timeSeries: DemandsAtTime[] = []
40-
if (opportunitiesDates) {
41-
const thirtyDaysAgo = new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000)
42-
datesWithinLast30Days = opportunitiesDates.filter((date) => date >= thirtyDaysAgo).length
43-
timeSeries = this.convertDatesToCumulativeTimeSeries(opportunitiesDates)
44-
}
45-
return {
46-
countOpportunitiesTotal: opportunitiesDates ? opportunitiesDates.length : null,
47-
countOpportunities30Days: opportunitiesDates ? datesWithinLast30Days : null,
48-
demandsTimeSeries: timeSeries
49-
}
50-
}
9+
const today = new Date()
10+
const since = params.since ? new Date(params.since) : new Date(today.getFullYear(), 0, 1)
11+
const to = params.to ? new Date(params.to) : new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1)
5112

52-
getProgramStatistics(): ProgramStats {
53-
const allPrograms = this._programService.getAll()
54-
const activeProgramsResult = this._programService.getFilteredPrograms({})
55-
if (activeProgramsResult.isErr) {
56-
Monitor.error('Error generating program statistics', { error: activeProgramsResult.error })
57-
throw activeProgramsResult.error
13+
const response = await fetch(METABASE_URL)
14+
if (!response.ok) {
15+
throw new Error('Stats API: Failed to fetch Metabase data')
5816
}
59-
return {
60-
countProgramsTotal: allPrograms.length,
61-
countProgramsNow: activeProgramsResult.value.length
62-
}
63-
}
6417

65-
convertDatesToCumulativeTimeSeries(opportunitiesDate: Date[]): DemandsAtTime[] {
66-
const timeSeries: DemandsAtTime[] = []
18+
const rawData: { week_start_date: string; total_2_3: number }[] = await response.json()
19+
const filtered = rawData
20+
.map((row) => ({
21+
date: this.toLocalDateOnly(new Date(row.week_start_date)), // week_start_date is a metabase variable that is poorly named and that is simply the statistic date
22+
value: row.total_2_3
23+
}))
24+
.filter((row) => row.date >= since && row.date <= to)
6725

68-
for (const date of opportunitiesDate) {
69-
const year = date.getFullYear()
70-
const month = date.getMonth() + 1
26+
const grouped: Record<string, { date: Date; value: number }> = {}
7127

72-
const existingEntryIndex = timeSeries.findIndex((entry) => entry.year === year && entry.month === month)
73-
if (existingEntryIndex !== -1) {
74-
const entry = timeSeries[existingEntryIndex] as DemandsAtTime
75-
entry.nDemands++
76-
} else {
77-
timeSeries.push({ year: year, month: month, nDemands: 1 })
78-
}
79-
}
28+
filtered.forEach(({ date, value }) => {
29+
const d = new Date(date)
30+
if (d < since || d > to) return
8031

81-
return this.convertToCumulativeTimeSeries(timeSeries)
82-
}
32+
let keyStatDate = today
8333

84-
convertToCumulativeTimeSeries(timeSeries: DemandsAtTime[]): DemandsAtTime[] {
85-
// Sort the array by year and month
86-
timeSeries.sort((a, b) => {
87-
if (a.year !== b.year) {
88-
return a.year - b.year
34+
switch (periodicity) {
35+
case 'day':
36+
keyStatDate = new Date(d.getFullYear(), d.getMonth(), d.getDate())
37+
break
38+
case 'week': {
39+
const weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate() - ((d.getDay() + 6) % 7))
40+
keyStatDate = weekStart
41+
break
42+
}
43+
case 'month':
44+
keyStatDate = new Date(d.getFullYear(), d.getMonth(), 1)
45+
break
46+
case 'year':
47+
keyStatDate = new Date(d.getFullYear(), 0, 1)
48+
break
8949
}
90-
return a.month - b.month
91-
})
92-
// compute the cumulative sum
93-
let cumulativeTotal = 0
94-
return timeSeries.map((entry) => {
95-
cumulativeTotal += entry.nDemands
9650

97-
return {
98-
year: entry.year,
99-
month: entry.month,
100-
nDemands: cumulativeTotal
51+
keyStatDate.setHours(keyStatDate.getHours() + 15) // To compensate for UTC hours and locales that can cause issues
52+
const key = keyStatDate.toISOString().slice(0, 10)
53+
if (!grouped[key]) {
54+
grouped[key] = { date: keyStatDate, value: 0 }
10155
}
56+
grouped[key].value += value
10257
})
103-
}
10458

105-
async getOpportunitiesCreated(): Promise<Date[] | null> {
106-
const opportunitiesDates = await this._opportunityRepository.readDates()
59+
const stats: Stat[] = Object.values(grouped)
60+
.sort((a, b) => a.date.getTime() - b.date.getTime())
61+
.map((stat) => ({
62+
...stat,
63+
date: stat.date.toISOString().split('T')[0]
64+
}))
10765

108-
if (opportunitiesDates.isOk) {
109-
return opportunitiesDates.value
66+
return {
67+
description: `Entreprises bénéficiaires`,
68+
stats
11069
}
111-
Monitor.error('Error generating Opportunities dates ', { error: opportunitiesDates.error })
112-
return null
70+
}
71+
72+
toLocalDateOnly(date: Date): Date {
73+
return new Date(date.getFullYear(), date.getMonth(), date.getDate())
11374
}
11475
}

libs/common/src/stats/types.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
export type DemandsAtTime = {
2-
year: number
3-
month: number
4-
nDemands: number
5-
}
1+
export type StatsPeriodicity = 'day' | 'week' | 'month' | 'year'
62

7-
export interface ProgramStats {
8-
countProgramsTotal: number | null
9-
countProgramsNow: number | null
3+
export interface StatQueryParams {
4+
periodicity?: StatsPeriodicity
5+
since?: string
6+
to?: string
107
}
118

12-
export interface OpportunityStats {
13-
countOpportunitiesTotal: number | null
14-
countOpportunities30Days: number | null
15-
demandsTimeSeries: DemandsAtTime[]
9+
export interface Stat {
10+
value: number
11+
date: string
1612
}
1713

18-
export type StatsData = ProgramStats & OpportunityStats
14+
export interface StatOutput {
15+
description?: string
16+
stats: Stat[]
17+
}

0 commit comments

Comments
 (0)