• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

inclusion-numerique / coop-mediation-numerique / 39cc4768-bcb1-433f-a4a6-d76e5937751a

01 Apr 2026 04:06PM UTC coverage: 7.472% (+0.5%) from 6.94%
39cc4768-bcb1-433f-a4a6-d76e5937751a

push

circleci

web-flow
Merge pull request #469 from inclusion-numerique/dev

MEP 2026-01-01

500 of 10542 branches covered (4.74%)

Branch coverage included in aggregate %.

145 of 414 new or added lines in 38 files covered. (35.02%)

13 existing lines in 10 files now uncovered.

1500 of 16224 relevant lines covered (9.25%)

36.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/apps/web/src/features/rdvsp/webhook/handleUserModelWebhook.ts
1
import { softDeleteBeneficiaires } from '@app/web/beneficiaire/softDeleteBeneficiaires'
2
import { prismaClient } from '@app/web/prismaClient'
3
import { ServerWebAppConfig } from '@app/web/ServerWebAppConfig'
4
import { userPrismaDataFromOAuthApiUser } from '../sync/syncRdv'
5
import type { RdvspWebhookEvent, RdvspWebhookUserData } from './rdvWebhook'
6

7
const logDebug = ServerWebAppConfig.RdvServicePublic.log.webhook.debug
×
8

9
/**
10
 * Converts webhook user data to the format expected by userPrismaDataFromOAuthApiUser
11
 */
12
const webhookUserToOAuthApiUser = (
×
13
  data: RdvspWebhookUserData,
14
): Parameters<typeof userPrismaDataFromOAuthApiUser>[0] => {
15
  return {
×
16
    id: data.id,
17
    first_name: data.first_name,
18
    last_name: data.last_name,
19
    email: data.email,
20
    notification_email: data.notification_email,
21
    notify_by_email: data.notify_by_email,
22
    notify_by_sms: data.notify_by_sms,
23
    phone_number: data.phone_number,
24
    phone_number_formatted: data.phone_number_formatted,
25
    address: data.address,
26
    address_details: data.address_details,
27
    affiliation_number: data.affiliation_number,
28
    birth_date: data.birth_date,
29
    birth_name: data.birth_name,
30
    caisse_affiliation: null, // Not available in webhook data
31
    created_at: data.created_at,
32
    invitation_accepted_at: data.invitation_accepted_at,
33
    invitation_created_at: data.invitation_created_at,
34
    responsible_id: data.responsible_id,
35
    responsible: null, // Not needed for upsert
36
    user_profiles: null, // Not needed for upsert
37
  }
38
}
39

40
export const handleUserModelWebhook = async ({
×
41
  data,
42
  event,
43
}: {
44
  data: RdvspWebhookUserData
45
  event: RdvspWebhookEvent
46
}) => {
47
  if (logDebug) {
×
48
    // biome-ignore lint/suspicious/noConsole: we log this until feature is not in production
49
    console.log(
×
50
      `[rdvsp webhook] Processing User ${event} for User id ${data.id}`,
51
    )
52
  }
53

54
  try {
×
55
    switch (event) {
×
56
      case 'created': {
57
        /**
58
         * We skip creating new RdvUser records from webhooks because:
59
         * - We can't determine which mediateur(s) should have access to this user
60
         * - RdvUsers are created during RDV sync when we have the full context
61
         * - A user without any RDV appointments is not relevant to our system yet
62
         */
63
        if (logDebug) {
×
64
          // biome-ignore lint/suspicious/noConsole: we log this until feature is not in production
65
          console.log(
×
66
            `[rdvsp webhook] Skipping User creation for id ${data.id} (users are created during RDV sync)`,
67
          )
68
        }
69
        break
×
70
      }
71

72
      case 'updated': {
73
        // Only update if this RdvUser is already linked to at least one Beneficiaire
74
        const linkedBeneficiaire = await prismaClient.beneficiaire.findFirst({
×
75
          where: { rdvUserId: data.id },
76
          select: { id: true },
77
        })
78

79
        if (linkedBeneficiaire) {
×
80
          // Update the RdvUser with the latest data
81
          const userData = userPrismaDataFromOAuthApiUser(
×
82
            webhookUserToOAuthApiUser(data),
83
          )
84
          await prismaClient.rdvUser.update({
×
85
            where: { id: data.id },
86
            data: userData,
87
          })
88
          if (logDebug) {
×
89
            // biome-ignore lint/suspicious/noConsole: we log this until feature is not in production
90
            console.log(`[rdvsp webhook] Updated RdvUser ${data.id}`)
×
91
          }
92
        } else if (logDebug) {
×
93
          // biome-ignore lint/suspicious/noConsole: we log this until feature is not in production
94
          console.log(
×
95
            `[rdvsp webhook] Skipping User update for id ${data.id} (not linked to any beneficiaire)`,
96
          )
97
        }
98
        break
×
99
      }
100

101
      case 'destroyed': {
102
        // Soft-delete/anonymise all beneficiaires linked to this RdvUser
NEW
103
        const beneficiairesToDelete = await prismaClient.beneficiaire.findMany({
×
104
          where: { rdvUserId: data.id, suppression: null },
105
          select: { id: true, mediateurId: true },
106
        })
107

NEW
108
        if (beneficiairesToDelete.length > 0) {
×
109
          // Group by mediateurId to decrement each mediateur's count
NEW
110
          const countByMediateur = new Map<string, number>()
×
NEW
111
          for (const b of beneficiairesToDelete) {
×
NEW
112
            countByMediateur.set(
×
113
              b.mediateurId,
114
              (countByMediateur.get(b.mediateurId) ?? 0) + 1,
×
115
            )
116
          }
117

NEW
118
          await prismaClient.$transaction(async (tx) => {
×
NEW
119
            await softDeleteBeneficiaires(
×
120
              tx,
NEW
121
              beneficiairesToDelete.map((b) => b.id),
×
122
            )
123

NEW
124
            for (const [mediateurId, count] of countByMediateur) {
×
NEW
125
              await tx.mediateur.update({
×
126
                where: { id: mediateurId },
127
                data: { beneficiairesCount: { decrement: count } },
128
              })
129
            }
130
          })
131
        }
132

133
        // Unlink any remaining beneficiaires (already soft-deleted ones)
UNCOV
134
        await prismaClient.beneficiaire.updateMany({
×
135
          where: { rdvUserId: data.id },
136
          data: { rdvUserId: null },
137
        })
138

139
        // Delete the RdvUser (cascade will delete RdvUserProfile)
140
        await prismaClient.rdvUser.delete({
×
141
          where: { id: data.id },
142
        })
143
        if (logDebug) {
×
144
          // biome-ignore lint/suspicious/noConsole: we log this until feature is not in production
145
          console.log(
×
146
            `[rdvsp webhook] Deleted RdvUser ${data.id} and soft-deleted ${beneficiairesToDelete.length} beneficiaire(s)`,
147
          )
148
        }
149
        break
×
150
      }
151

152
      default: {
153
        const exhaustiveCheck: never = event
×
154
        // biome-ignore lint/suspicious/noConsole: we log this until feature is not in production
155
        console.warn(
×
156
          `[rdvsp webhook] Unknown event type: ${String(exhaustiveCheck)}`,
157
        )
158
      }
159
    }
160
  } catch (error) {
161
    // biome-ignore lint/suspicious/noConsole: we log this until feature is not in production
162
    console.error(
×
163
      `[rdvsp webhook] Error processing User ${event} for User id ${data.id}:`,
164
      error,
165
    )
166
    throw error
×
167
  }
168
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc