Skip to content

Commit 4ad22c3

Browse files
authored
✨ poll stats the good way (NGC-2329) (#357)
1 parent fc13a9e commit 4ad22c3

File tree

7 files changed

+144
-46
lines changed

7 files changed

+144
-46
lines changed

src/core/deep-merge-sum.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/core/deep-merge.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
type RecordToDeepMerge = {
2+
[key: string]: number | RecordToDeepMerge | undefined
3+
}
4+
5+
const deepMerge = (
6+
record1: RecordToDeepMerge,
7+
record2: RecordToDeepMerge,
8+
operation: (value1: number, value2: number) => number
9+
) =>
10+
Object.entries(record2).reduce(
11+
(acc, [key, value]): RecordToDeepMerge => {
12+
if (typeof value === 'number') {
13+
acc[key] = acc[key] || 0
14+
if (typeof acc[key] === 'number') {
15+
acc[key] = operation(acc[key], value)
16+
}
17+
} else if (typeof value === 'object') {
18+
acc[key] = acc[key] || {}
19+
if (typeof acc[key] === 'object') {
20+
acc[key] = deepMerge(acc[key], value, operation)
21+
}
22+
}
23+
24+
return acc
25+
},
26+
{ ...record1 }
27+
)
28+
29+
export const deepMergeSum = (
30+
record1: RecordToDeepMerge,
31+
record2: RecordToDeepMerge
32+
) => deepMerge(record1, record2, (a, b) => +(a + b).toPrecision(12))
33+
34+
export const deepMergeSubstract = (
35+
record1: RecordToDeepMerge,
36+
record2: RecordToDeepMerge
37+
) => deepMerge(record1, record2, (a, b) => +(a - b).toPrecision(12))

src/features/organisations/__tests__/fetch-organisation-public-poll.spec.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
66
import { prisma } from '../../../adapters/prisma/client.js'
77
import * as prismaTransactionAdapter from '../../../adapters/prisma/transaction.js'
88
import app from '../../../app.js'
9+
import { deepMergeSubstract, deepMergeSum } from '../../../core/deep-merge.js'
910
import logger from '../../../logger.js'
1011
import { login } from '../../authentication/__tests__/fixtures/login.fixture.js'
1112
import { COOKIE_NAME } from '../../authentication/authentication.service.js'
13+
import type { ComputedResultSchema } from '../../simulations/simulations.validator.js'
1214
import {
1315
createOrganisation,
1416
createOrganisationPoll,
@@ -111,10 +113,12 @@ describe('Given a NGC user', () => {
111113
})
112114

113115
describe('And he did participate to the poll', () => {
116+
let computedResults: ComputedResultSchema
114117
let userId: string
115118

116119
beforeEach(async () => {
117120
;({
121+
computedResults,
118122
user: { id: userId },
119123
} = await createOrganisationPollSimulation({
120124
agent,
@@ -141,7 +145,12 @@ describe('Given a NGC user', () => {
141145
finished: 1,
142146
hasParticipated: true,
143147
},
144-
computedResults: expect.any(Object),
148+
computedResults,
149+
userComputedResults: computedResults,
150+
otherComputedResults: deepMergeSubstract(
151+
computedResults,
152+
computedResults
153+
),
145154
funFacts: Object.fromEntries(
146155
Object.entries(modelFunFacts).map(([k]) => [
147156
k,
@@ -174,7 +183,12 @@ describe('Given a NGC user', () => {
174183
finished: 1,
175184
hasParticipated: true,
176185
},
177-
computedResults: expect.any(Object),
186+
computedResults,
187+
userComputedResults: computedResults,
188+
otherComputedResults: deepMergeSubstract(
189+
computedResults,
190+
computedResults
191+
),
178192
funFacts: Object.fromEntries(
179193
Object.entries(modelFunFacts).map(([k]) => [
180194
k,
@@ -395,7 +409,11 @@ describe('Given a NGC user', () => {
395409
finished: 3,
396410
hasParticipated: false,
397411
},
398-
computedResults: expect.any(Object),
412+
computedResults: simulations.reduce(
413+
(acc, { computedResults }) =>
414+
deepMergeSum(acc, computedResults),
415+
{}
416+
),
399417
funFacts: Object.fromEntries(
400418
Object.entries(modelFunFacts).map(([k]) => [
401419
k,

src/features/organisations/organisations.repository.ts

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { FunFacts } from '@incubateur-ademe/nosgestesclimat'
22
import type { Prisma } from '@prisma/client'
3+
import type { JsonValue } from '@prisma/client/runtime/library'
34
import type { Request } from 'express'
45
import slugify from 'slugify'
56
import {
@@ -9,10 +10,8 @@ import {
910
defaultVerifiedUserSelection,
1011
} from '../../adapters/prisma/selection.js'
1112
import type { Session } from '../../adapters/prisma/transaction.js'
12-
import type {
13-
ComputedResultSchema,
14-
SimulationParams,
15-
} from '../simulations/simulations.validator.js'
13+
import type { SimulationParams } from '../simulations/simulations.validator.js'
14+
import { ComputedResultSchema } from '../simulations/simulations.validator.js'
1615
import type {
1716
OrganisationCreateDto,
1817
OrganisationParams,
@@ -329,14 +328,27 @@ export const fetchUserOrganisation = (
329328

330329
const findUniquePollSlug = findModelUniqueSlug('poll')
331330

331+
type SimulationsInfo = {
332+
count: number
333+
finished: number
334+
} & (
335+
| {
336+
hasParticipated: false
337+
}
338+
| {
339+
hasParticipated: true
340+
userComputedResults: ComputedResultSchema
341+
}
342+
)
343+
332344
const fetchPollSimulationsInfo = async (
333345
{
334346
poll: { id },
335347
user: { userId },
336348
}: { poll: { id: string }; user: { userId: string } },
337349
{ session }: { session: Session }
338-
) => {
339-
const [count, finished, userCount] = await Promise.all([
350+
): Promise<SimulationsInfo> => {
351+
const [count, finished, userSimulation] = await Promise.all([
340352
session.simulationPoll.count({
341353
where: {
342354
pollId: id,
@@ -350,7 +362,7 @@ const fetchPollSimulationsInfo = async (
350362
},
351363
},
352364
}),
353-
session.simulationPoll.count({
365+
session.simulationPoll.findFirst({
354366
where: {
355367
pollId: id,
356368
simulation: {
@@ -359,13 +371,51 @@ const fetchPollSimulationsInfo = async (
359371
},
360372
},
361373
},
374+
select: {
375+
simulation: {
376+
select: {
377+
computedResults: true,
378+
},
379+
},
380+
},
381+
orderBy: {
382+
simulation: {
383+
createdAt: 'desc',
384+
},
385+
},
362386
}),
363387
])
364388

389+
const userComputedResults = ComputedResultSchema.safeParse(
390+
userSimulation?.simulation.computedResults
391+
)
392+
365393
return {
366394
count,
367395
finished,
368-
hasParticipated: !!userCount,
396+
...(userComputedResults.success
397+
? {
398+
hasParticipated: true,
399+
userComputedResults: userComputedResults.data,
400+
}
401+
: {
402+
hasParticipated: false,
403+
}),
404+
}
405+
}
406+
407+
const sanitizePollComputedResults = <T extends { computedResults: JsonValue }>({
408+
computedResults: rawComputedResults,
409+
...poll
410+
}: T): Omit<T, 'computedResults'> & {
411+
computedResults: ComputedResultSchema | null
412+
} => {
413+
const computedResults = ComputedResultSchema.safeParse(rawComputedResults)
414+
return {
415+
...poll,
416+
...(computedResults.success
417+
? { computedResults: computedResults.data }
418+
: { computedResults: null }),
369419
}
370420
}
371421

@@ -428,9 +478,9 @@ export const createOrganisationPoll = async (
428478
)
429479

430480
return {
481+
poll: sanitizePollComputedResults(poll),
431482
simulationsInfos,
432483
organisation,
433-
poll,
434484
}
435485
}
436486

@@ -519,9 +569,9 @@ export const updateOrganisationPoll = async (
519569
)
520570

521571
return {
572+
poll: sanitizePollComputedResults(poll),
522573
simulationsInfos,
523574
organisation,
524-
poll,
525575
}
526576
}
527577

@@ -564,7 +614,7 @@ export const fetchOrganisationPolls = async (
564614
organisation,
565615
polls: await Promise.all(
566616
polls.map(async (poll) => ({
567-
poll,
617+
poll: sanitizePollComputedResults(poll),
568618
simulationsInfos: await fetchPollSimulationsInfo(
569619
{ poll, user },
570620
{ session }
@@ -604,7 +654,7 @@ export const fetchOrganisationPoll = async (
604654
return {
605655
simulationsInfos,
606656
organisation,
607-
poll,
657+
poll: sanitizePollComputedResults(poll),
608658
}
609659
}
610660

@@ -644,9 +694,9 @@ export const fetchOrganisationPublicPoll = async (
644694
)
645695

646696
return {
697+
poll: sanitizePollComputedResults(poll),
647698
simulationsInfos,
648699
organisation,
649-
poll,
650700
}
651701
}
652702

src/features/organisations/organisations.service.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { Session } from '../../adapters/prisma/transaction.js'
1212
import { transaction } from '../../adapters/prisma/transaction.js'
1313
import { client } from '../../adapters/scaleway/client.js'
1414
import { config } from '../../config.js'
15+
import { deepMergeSubstract } from '../../core/deep-merge.js'
1516
import { EntityNotFoundException } from '../../core/errors/EntityNotFoundException.js'
1617
import { ForbiddenException } from '../../core/errors/ForbiddenException.js'
1718
import { EventBus } from '../../core/event-bus/event-bus.js'
@@ -269,7 +270,8 @@ const isOrganisationAdmin = (
269270

270271
const pollToDto = ({
271272
poll: { organisationId: _1, computeRealTimeStats: _2, ...poll },
272-
simulationsInfos: simulations,
273+
simulationsInfos: { count, finished, hasParticipated },
274+
simulationsInfos,
273275
organisation,
274276
user,
275277
}: {
@@ -293,7 +295,24 @@ const pollToDto = ({
293295
defaultAdditionalQuestions: poll.defaultAdditionalQuestions?.map(
294296
({ type }) => type
295297
),
296-
simulations,
298+
simulations: {
299+
count,
300+
finished,
301+
hasParticipated,
302+
},
303+
...(simulationsInfos.hasParticipated
304+
? {
305+
userComputedResults: simulationsInfos.userComputedResults,
306+
...(poll.computedResults
307+
? {
308+
otherComputedResults: deepMergeSubstract(
309+
poll.computedResults,
310+
simulationsInfos.userComputedResults
311+
),
312+
}
313+
: {}),
314+
}
315+
: {}),
297316
})
298317

299318
export const createPoll = async ({

src/features/simulations/__tests__/compute-poll-stats.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ describe('Given a poll participation', () => {
127127
const cache = JSON.parse(rawCache!)
128128

129129
expect(cache).toEqual({
130-
computedResults: expect.any(Object),
130+
computedResults: event.attributes.simulation.computedResults,
131131
simulationCount: 1,
132132
funFactValues: Object.fromEntries(
133133
Object.entries(modelFunFacts).map(([_, v]) => [v, expect.any(Number)])

src/features/simulations/simulations.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type { Session } from '../../adapters/prisma/transaction.js'
1414
import { transaction } from '../../adapters/prisma/transaction.js'
1515
import { redis } from '../../adapters/redis/client.js'
1616
import { KEYS } from '../../adapters/redis/constant.js'
17-
import { deepMergeSum } from '../../core/deep-merge-sum.js'
17+
import { deepMergeSum } from '../../core/deep-merge.js'
1818
import { EntityNotFoundException } from '../../core/errors/EntityNotFoundException.js'
1919
import { ForbiddenException } from '../../core/errors/ForbiddenException.js'
2020
import { EventBus } from '../../core/event-bus/event-bus.js'

0 commit comments

Comments
 (0)