|
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' |
7 | 2 |
|
8 | 3 | 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' |
12 | 6 |
|
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' |
23 | 8 |
|
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) |
51 | 12 |
|
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') |
58 | 16 | }
|
59 |
| - return { |
60 |
| - countProgramsTotal: allPrograms.length, |
61 |
| - countProgramsNow: activeProgramsResult.value.length |
62 |
| - } |
63 |
| - } |
64 | 17 |
|
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) |
67 | 25 |
|
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 }> = {} |
71 | 27 |
|
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 |
80 | 31 |
|
81 |
| - return this.convertToCumulativeTimeSeries(timeSeries) |
82 |
| - } |
| 32 | + let keyStatDate = today |
83 | 33 |
|
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 |
89 | 49 | }
|
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 |
96 | 50 |
|
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 } |
101 | 55 | }
|
| 56 | + grouped[key].value += value |
102 | 57 | })
|
103 |
| - } |
104 | 58 |
|
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 | + })) |
107 | 65 |
|
108 |
| - if (opportunitiesDates.isOk) { |
109 |
| - return opportunitiesDates.value |
| 66 | + return { |
| 67 | + description: `Entreprises bénéficiaires`, |
| 68 | + stats |
110 | 69 | }
|
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()) |
113 | 74 | }
|
114 | 75 | }
|
0 commit comments