Project: workspace-doctolib · Branch: HEAD · Mode: ` · **Model:**claude-opus-4-7`
Stats: 1 user messages, 28 tool calls, tools used: Bash, Read.
Conversation
Turn 1 — User
Review the following GitHub PR for adherence to project guidelines, style, and best practices:
PR URL: https://github.com/doctolib/doctolib/pull/241133 Repo: doctolib/doctolib
Use the GitHub MCP tools to read the PR diff and files. If you need to browse local code for context, the local worktree is at /Users/USER/workspace/doctolib/worktrees/doctolib/ (check subdirectories for the relevant branch).
Focus on: - Code correctness and logic - Security vulnerabilities - Style guide adherence - Error handling - Naming conventions
Return a structured list of findings with file paths and line numbers.
Turn 1 — Assistant
I'll review this PR. Let me start by fetching the PR details using the GitHub CLI.
Tools used:
Bash:gh pr view 241133 --repo doctolib/doctolib --json title,body,author,state,baseRefName,headRefName,files,additions,deletions
Tool results:
→ result (ok): {"additions":228,"author":{"id":"U_kgDOBl0CeQ","is_bot":false,"login":"sushangag","name":"Sushan Gagneja"},"baseRefName":"master","body":"## Why\n\nhttps://doctolib.atlassian.net/browse/FHIR-2224\n\nupdateDocumentStatus in DmpApi previously returned Promise<void> and threw on failure. Callers had no way to access structured DMP error information (error codes, descriptions) — they could only catch a plain Error with a message string. This PR makes the API return structured results, consis
…(truncated, 3671 more chars)
Let me fetch the actual diff content to review.
Tools used:
Bash:gh pr diff 241133 --repo doctolib/doctolib
Tool results:
→ result (ok): diff --git a/packages/@doctolib/dmp/src/business/DmpApi.test.ts b/packages/@doctolib/dmp/src/business/DmpApi.test.ts index 2b146a28faf65..99769412b8646 100644 --- a/packages/@doctolib/dmp/src/business/DmpApi.test.ts +++ b/packages/@doctolib/dmp/src/business/DmpApi.test.ts @@ -1609,33 +1609,78 @@ describe('DmpApi', () => { }) })
- it('should throw when no cached session exists', async () => {
- it('should return success: true on success', async () => {
- getCachedDmpSessi …(truncated, 18145 more chars)
Now let me read the existing source files for context, especially around DocumentStatusUpdateResult declaration vs PR description, and the DmpApiError/error semantics.
Tools used:
Bash:find /Users/USER/workspace/doctolib/worktrees/doctolib -maxdepth 2 -type d 2>/dev/null | head -30
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/worktrees/doctolib /Users/USER/workspace/doctolib/worktrees/doctolib/fhir-2221_display-last-dmp-access-date /Users/USER/workspace/doctolib/worktrees/doctolib/fhir-2223_fix-column-widths-dmp /Users/USER/workspace/doctolib/worktrees/doctolib/fhir-2209_fix-patient-ins-env-transformation /Users/USER/workspace/doctolib/worktrees/doctolib/fhir-2190_improve-filter-panel-ui /Users/USER/workspace/doctolib/worktrees/doctolib/fhir-2089 /Users/USER/workspace/doctolib/worktrees …(truncated, 2126 more chars)
Tools used:
Bash:ls /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/types/ 2>/dev/null && ls /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@docto …(truncated, 33 more chars)
Tool results:
→ result (ok): DmpApi.ts FrDmpTypes.ts CallDmpDelegate.test.ts CallDmpDelegate.ts DmpApi.test.ts DmpApi.ts DmpI18nMessage.ts DmpSharingSettings.test.ts DmpSharingSettings.ts FrDmpCommunication.test.ts FrDmpCommunication.ts FrDmpConfig.ts FrDmpConstants.ts frDmpPayloadBuilders.ts icanopee mapCpsToPscPayload.test.ts mapCpsToPscPayload.ts
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/types/DmpApi.ts
Tool results:
→ result (ok): 1 import type { Patient } from 'monolith_DEPRECATED/types/doctor/api/Patient'
2 import type { ClinicalDocument } from 'engines/cdar2/types/ClinicalDocument'
3 import type { DmpDocumentToAdd, DmpDocumentToDelete } from '../business/FrDmpConfig'
4 import type { DmpWorkflow, BillingSituation, GetDocumentsContentParams, PracticeLocationOverride } from './FrDmpTypes'
5
6 export type PatientConsent = 'error' | 'authorized' | 'expired' | 'forbidden' | 'noAuthorization'
7
8 export type PatientConsentU
…(truncated, 5382 more chars)
Now I notice an important discrepancy. The PR description says DocumentStatusUpdateResult is { error?: DmpApiErrorInfo }, and the actual file confirms this. But the diff shows test code using { success: true } and { success: false; error: ... }. Let me check what's actually in the worktree on the FHIR-2224 branch.
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/business/DmpApi.ts(340–420)
Tool results:
→ result (ok): 340
341 const { sessionId } = cachedSession
342 const response = await getDocumentsContent({ sessionId, ...props })
343 return response.Documents.map(this.toDocumentContent)
344 }
345
346 async updateDocumentStatus(props: DocumentStatusUpdateProps): Promise<DocumentStatusUpdateResult> {
347 const { ins, documentUniqueId: uniqueId, uuid, healthCareSettings, getTransactionFrames } = props
348
349 if (this.dmpService) {
350 const payload = buildUpdateDocumentStatusP
…(truncated, 2336 more chars)
Now I have a clearer picture. The local worktree has different code than the PR diff. The PR diff modifies the API differently than what's in the worktree. Looking at the diff in the PR, the new code introduces { success: true } / { success: false, error: ... } but the type definition is { error?: DmpApiErrorInfo }. Let me re-read the diff carefully.
Looking at the diff more carefully:
Type definition diff (packages/@doctolib/dmp/src/types/DmpApi.ts):
export type DocumentStatusUpdateResult = { success: true } | { success: false; error: DmpApiErrorInfo }
This is the type added. Good - it's a discriminated union.
Implementation (DmpApi.ts):
return { success: true }
return { success: false, error: { status: 'ERROR', apiErrorDescription: message } }
So the implementation matches.
Adapter (DmpNhrAdapter.ts):
if (!result.success) {
return { ..., message: new DmpApiError(result.error).message }
}
return { id, uuid, propertyName, status: 'success' }
This is consistent.
Now let me carefully look at the issues. Let me check the adapter changes more carefully—specifically the removal of healthcareSettings/healthCareSettings:
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.ts
Tool results:
→ result (ok): 1 import type { 2 DmpApi, 3 DmpAuthor, 4 DmpDocument, 5 DmpDocumentContent, 6 SearchProps, 7 SearchResult as DmpSearchResult, 8 } from '@doctolib/dmp' 9 import { DmpApiError, DOCUMENT_STATUS, DOCUMENT_VISIBILITY, getPatientInsForDMP } from '@doctolib/dmp' 10 import { isUnstructured } from '@doctolib/cda/types/UnstructuredBody' 11 import { isStructured } from '@doctolib/cda/types/StructuredBody' 12 import type { Patient } from 'monolith_DEPRECATED/types/doctor/api/Patient' 13 import t …(truncated, 9948 more chars)
The local worktree is clearly already on a previous version of the code that uses result.error. The PR diff shows two divergent paradigms — the diff itself uses success: true / success: false. Let me re-examine the diff carefully.
Looking again at the type definition diff:
+export type DocumentStatusUpdateResult = { success: true } | { success: false; error: DmpApiErrorInfo }
And the adapter diff:
+if (!result.success) {
+ return { ..., message: new DmpApiError(result.error).message }
+}
These are consistent — discriminated union with success property.
Now let me dig into specific concerns:
- The legacy path for
updateDocumentStatusCommanddoesn't have a try/catch in the worktree but in the PR diff it adds try/catch, but the test for "no cached session" expectssuccess: falserather than throwing. Let me re-verify the diff.
Looking at the PR diff for DmpApi.ts:
if (!cachedSession) {
- throw new Error('No DMP session found')
+ return { success: false, error: { status: 'ERROR', apiErrorDescription: 'No DMP session found' } }
}
OK, this changes from throwing to returning the structured error. Good.
For the catch block on the legacy path:
try {
const response = await updateDocumentStatusCommand({...})
if (response.s_status !== STATUS_OK) {
return { success: false, error: {...} }
}
return { success: true }
} catch (e) {
const message = e instanceof Error ? e.message : 'Unknown error'
return { success: false, error: { status: 'ERROR', apiErrorDescription: message } }
}
For the dmpService path:
try {
await this.dmpService.call(DmpAction.UpdateDocumentStatus, payload)
return { success: true }
} catch (e) {
const message = e instanceof Error ? e.message : 'Unknown error'
return { success: false, error: { status: 'ERROR', apiErrorDescription: message } }
}
Now let me check the test file for the BusinessError handling:
Looking at the test diff for BusinessError:
it('should return error result when dmpService throws BusinessError', async () => {
mockDmpService.call.mockRejectedValue(new BusinessError('DMP returned error'))
...
expect(result).toEqual({
success: false,
error: expect.objectContaining({
status: 'ERROR',
apiErrorDescription: 'The teleservice returned a business error',
}),
})
})
Wait — the BusinessError has the message 'DMP returned error' but the expected apiErrorDescription is 'The teleservice returned a business error'. There's a discrepancy! This implies BusinessError's .message is NOT 'DMP returned error' but rather 'The teleservice returned a business error'. Let me find BusinessError.
Tools used:
Bash:grep -r "class BusinessError" /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/ 2>/dev/null | head -5
Tool results:
→ result (ok): (Bash completed with no output)
Tools used:
Bash:grep -rn "BusinessError" /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/ 2>/dev/null | head -20
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/business/CallDmpDelegate.test.ts:2:import { DmpAction, BusinessError } from '@doctolib/tpf-french-teleservices' /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/business/CallDmpDelegate.test.ts:150: it('should throw BusinessError when s_status is not OK', () => { /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/business/CallDmpDeleg …(truncated, 2509 more chars)
Tools used:
Bash:grep -rn "class BusinessError\|BusinessError extends\|teleservice returned a business error" /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/tpf-french-teleservices …(truncated, 24 more chars)
Tool results:
→ result (ok): (Bash completed with no output)
Tools used:
Bash:find /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224 -name "BusinessError*" -type f 2>/dev/null | head -5; grep -rn "teleservice returned a business error" /Users/USER/workspace/ …(truncated, 69 more chars)
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/business/DmpApi.test.ts:2058: expect.objectContaining({ status: 'ERROR', apiErrorDescription: 'The teleservice returned a business error' })
Tools used:
Bash:find /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224 -path "*tpf-french-teleservices*" -name "*.ts" 2>/dev/null | head -10
Tool results:
→ result (ok): (Bash completed with no output)
Tools used:
Bash:find /Users/USER/workspace/doctolib -name "tpf-french-teleservices*" -maxdepth 6 -type d 2>/dev/null | head -5
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-french-teleservices /Users/USER/workspace/doctolib/treatment-plan/build/packages/tpf-french-teleservices /Users/USER/workspace/doctolib/treatment-plan/.claude/worktrees/friendly-lalande/packages/tpf-french-teleservices /Users/USER/workspace/doctolib/treatment-plan/.claude/worktrees/stupefied-lewin/packages/tpf-french-teleservices /Users/USER/workspace/doctolib/worktrees/treatment-plan/fhir-2222_add-visible-to-all-filter/packages/tpf-fren …(truncated, 15 more chars)
Tools used:
Bash:grep -rn "class BusinessError\|teleservice returned a business" /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-french-teleservices/ 2>/dev/null | head -5
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-french-teleservices/src/core/auth-errors.ts:24:export class BusinessError extends FrenchTeleserviceError { /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-french-teleservices/src/core/auth-errors.ts:26: super(message ?? 'The teleservice returned a business error', 'BUSINESS_ERROR', serviceName) /Users/USER/workspace/doctolib/treatment-plan/packages/tpf-french-teleservices/src/core/auth-errors.test.ts:49: expect(error.mes …(truncated, 55 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/treatment-plan/packages/tpf-french-teleservices/src/core/auth-errors.ts
Tool results:
→ result (ok): 1 import { FrenchTeleserviceError } from './errors'
2
3 export class TransportError extends FrenchTeleserviceError {
4 constructor(serviceName: string, message?: string) {
5 super(message ?? 'A transport error occurred', 'TRANSPORT_ERROR', serviceName)
6 this.name = 'TransportError'
7 }
8 }
9
10 export class CpsAuthError extends FrenchTeleserviceError {
11 constructor(serviceName: string, message?: string) {
12 super(message ?? 'CPS card authentication failed', 'CPS_AUTH_ERRO
…(truncated, 685 more chars)
Now I have the full picture. The BusinessError constructor takes (serviceName, message?) and the message defaults to 'The teleservice returned a business error'. The test calls new BusinessError('DMP returned error') — so 'DMP returned error' is being passed as serviceName, NOT message. So e.message would be the default 'The teleservice returned a business error'. That makes sense with the test.
Let me also re-examine the diff in detail for the type definition. Looking at it more carefully, the PR description says the diff for type definition shows DocumentStatusUpdateResult = { error?: DmpApiErrorInfo }, but the actual implementation/tests use { success: true | false; error: ... }. This is the discrepancy that I noticed in my first reading. Let me re-verify the diff output for DmpApi.ts types.
From the diff:
+export type DocumentStatusUpdateResult = { success: true } | { success: false; error: DmpApiErrorInfo }
OK so the diff shows the discriminated union, but the PR description says { error?: DmpApiErrorInfo }. The PR description is out of sync with the code. Let me verify by reading the actual file content again at the type level — I read the worktree version above which shows { error?: DmpApiErrorInfo }. This means the worktree has an older version of the code. The PR diff is what's actually being reviewed.
Now let me verify a few more concerns from the diff:
1. Inconsistent approach: success discriminator vs error? optional
The diff modifies the type to use a discriminated union ({ success: true } | { success: false; error: ... }), but SearchResult (referenced in the PR description as the pattern to mirror) uses { documents, error?: ... } — an optional error. The PR description claims to "mirror the existing SearchResult pattern" but the actual implementation uses a different pattern.
2. PR description mentions BusinessError synthesizing error info
The PR description says: "catches BusinessError and returns { error: { status: 'ERROR', apiErrorDescription: e.message } } instead of throwing. Non-BusinessError still propagates."
But the actual diff catches all errors, not just BusinessError:
} catch (e) {
const message = e instanceof Error ? e.message : 'Unknown error'
return { success: false, error: { status: 'ERROR', apiErrorDescription: message } }
}
This is a behavior change — the diff swallows all exceptions instead of just BusinessError. This is a regression from documented intent.
3. Legacy path also catches all errors
In the legacy path, the diff wraps the whole updateDocumentStatusCommand block in try/catch. This silences all errors including TypeError/network errors that previously propagated.
4. healthCareSettings removed from updateDocumentStatus call
The adapter previously passed healthCareSettings: healthcareSettings. The diff removes it. But the type DocumentStatusUpdateProps (still in worktree) has healthCareSettings: string as required. Does the diff change DocumentStatusUpdateProps to make it optional? Let me check.
Looking at the diff for types/DmpApi.ts — only the result type was added, no change to DocumentStatusUpdateProps. So healthCareSettings remains required in the type but the call site no longer passes it. This would be a TypeScript error.
Wait, let me check whether the diff showed healthCareSettings in DmpApi.ts impl. The updateDocumentStatus method in DmpApi.ts uses props.frDmpWorkflow.healthcareSettings rather than props.healthCareSettings. Let me look:
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/business/DmpApi.ts(346–361)
Tool results:
→ result (ok): 346 async updateDocumentStatus(props: DocumentStatusUpdateProps): Promise<DocumentStatusUpdateResult> { 347 const { ins, documentUniqueId: uniqueId, uuid, healthCareSettings, getTransactionFrames } = props 348 349 if (this.dmpService) { 350 const payload = buildUpdateDocumentStatusPayload({ 351 ins, 352 uniqueId, 353 uuid, 354 healthCareSettings, 355 getTransactionFrames, 356 }) 357 358 try { 359 await this.dmpService. …(truncated, 67 more chars)
Looking at line 347, healthCareSettings is destructured from props. So the diff shows that healthCareSettings is read from props.
The pre-PR state in the diff:
async updateDocumentStatus(props: DocumentStatusUpdateProps): Promise<void> {
const { ins, documentUniqueId: uniqueId, uuid, getTransactionFrames } = props
if (!this.props.frDmpWorkflow) throw new Error('frDmpWorkflow is required to update document status')
const healthCareSettings = this.props.frDmpWorkflow.healthcareSettings
So in the pre-PR state (before this PR), healthCareSettings was sourced from this.props.frDmpWorkflow.healthcareSettings, not from props. The diff shows the new code grabs it from props.healthCareSettings. Since the PR description doesn't claim to make this change, but the diff shows it, this is the actual code state after the change.
Now the adapter test expects expect(...).toHaveBeenCalledWith({ ins: ..., documentUniqueId: ..., uuid: ... }) — without healthCareSettings. And the call site doesn't pass healthCareSettings either. So if DocumentStatusUpdateProps still requires healthCareSettings: string, this would be a TypeScript error.
Let me look at the diff carefully again:
diff --git a/packages/@doctolib/dmp/src/business/DmpApi.ts b/packages/@doctolib/dmp/src/business/DmpApi.ts
@@ -341,7 +343,7 @@ export class DmpApiImpl implements DmpApi {
return response.Documents.map(this.toDocumentContent)
}
- async updateDocumentStatus(props: DocumentStatusUpdateProps): Promise<void> {
+ async updateDocumentStatus(props: DocumentStatusUpdateProps): Promise<DocumentStatusUpdateResult> {
const { ins, documentUniqueId: uniqueId, uuid, getTransactionFrames } = props
if (!this.props.frDmpWorkflow) throw new Error('frDmpWorkflow is required to update document status')
const healthCareSettings = this.props.frDmpWorkflow.healthcareSettings
OK so the diff still shows:
- healthCareSettings sourced from this.props.frDmpWorkflow.healthcareSettings
- NOT removed from props destructuring (still ins, documentUniqueId, uuid, getTransactionFrames)
That confirms healthCareSettings is NOT in the props destructure, so the type DocumentStatusUpdateProps may not require it any more. Actually checking adapter call site in diff:
const result = await this.dmpApi.updateDocumentStatus({
ins,
documentUniqueId: id,
uuid: uuid ?? '',
})
It only passes ins, documentUniqueId, uuid. The diff doesn't change the type of DocumentStatusUpdateProps, so if healthCareSettings is still required there, this is a TypeScript error. Let me check if the diff in types/DmpApi.ts includes a removal of healthCareSettings.
Looking at the type diff again:
diff --git a/packages/@doctolib/dmp/src/types/DmpApi.ts b/packages/@doctolib/dmp/src/types/DmpApi.ts
@@ -109,7 +109,7 @@ export interface DmpApi {
getPatientConsent: () => Promise<PatientConsent>
updatePatientConsent: (consent: PatientConsentUpdate) => Promise<boolean>
getDocumentsContentByUniqueId: (props: GetDocumentsContentProps) => Promise<DmpDocumentContent[]>
- updateDocumentStatus: (props: DocumentStatusUpdateProps) => Promise<void>
+ updateDocumentStatus: (props: DocumentStatusUpdateProps) => Promise<DocumentStatusUpdateResult>
}
export type DmpSetUpProps = {
@@ -183,6 +183,8 @@ export interface SearchResult {
error?: DmpApiErrorInfo
}
+export type DocumentStatusUpdateResult = { success: true } | { success: false; error: DmpApiErrorInfo }
+
export type GetDocumentsContentProps = Omit<GetDocumentsContentParams, 'sessionId'>
The diff for types only adds DocumentStatusUpdateResult and changes the return type. DocumentStatusUpdateProps is NOT changed. So if DocumentStatusUpdateProps includes healthCareSettings: string (looking at the worktree, it does), then the adapter's call without healthCareSettings is a TypeScript type error.
Wait — let me check the test file. The test file in the diff shows { healthcareSettings: '' } in additionalInfo (changed from 'SA07' to empty string). This strongly suggests the field is being deprecated/dropped from being passed in the API call. And in the test the assertion is:
expect(updateDocumentStatusMock).toHaveBeenCalledWith({
ins: 'INS-123T',
documentUniqueId: 'doc-1',
uuid: 'uuid-1',
// healthCareSettings: 'SA07', <-- removed
})
But this test won't compile if DocumentStatusUpdateProps still requires healthCareSettings: string. Unless... the diff also modifies DocumentStatusUpdateProps. Let me look at the file's full diff one more time.
The PR diff I have shows ONLY the changes I quoted. So there is NO change to DocumentStatusUpdateProps. This means:
- Either the
DocumentStatusUpdatePropsinterface hashealthCareSettingsas optional (not visible in the diff but the worktree shows it as requiredstring) - Or there's a TypeScript type error
Let me verify by looking at the actual current file to confirm if the worktree diverged:
Tools used:
Bash:cd /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224 && git log --oneline -10 2>/dev/null
Tool results:
→ result (ok): 1cec77f7f3d2 FHIR-2224: Handle errors
Tools used:
Bash:cd /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224 && git diff HEAD~1 HEAD -- packages/@doctolib/dmp/src/types/DmpApi.ts 2>/dev/null | head -50
Tool results:
→ result (ok): (Bash completed with no output)
Tools used:
Bash:cd /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224 && git log --all --oneline -20 2>/dev/null
Tool results:
→ result (ok): d370831bba18 Merge branch 'master' into APE-3895 04ff8aba49a1 lint fix 7cbb571247c8 Merge last green master (39dfa1e18230) into TIGAH-2846-async-job 657a8083465b chore(UpsellModalToConsultationAssistant): remove german_variation_for_ca_prospect_modal feature switch cb9b400e7741 update bootsnap on next b94ff8305948 Merge branch 'master' into PUG-3889-feature-switch f9387dcea3f0 Merge branch 'master' into MDP-3587-trackings af57561c415e Merge master into update_billeo 04c7ff9376c7 update 29c1e51aa …(truncated, 752 more chars)
The worktree appears to have a single commit on top of master that's older than this PR. The PR (#241133) is not what's in this worktree. Let me get the actual PR's branch to verify by directly fetching the PR's tree state:
Tools used:
Bash:gh api repos/doctolib/doctolib/pulls/241133/files --paginate -q '.[] | "\(.filename) [\(.status)] +\(.additions)/-\(.deletions)"' 2>&1 | head -20
Tool results:
→ result (ok): packages/@doctolib/dmp/src/business/DmpApi.test.ts [modified] +104/-25 packages/@doctolib/dmp/src/business/DmpApi.ts [modified] +35/-19 packages/@doctolib/dmp/src/index.ts [modified] +1/-0 packages/@doctolib/dmp/src/types/DmpApi.ts [modified] +3/-1 packages/@doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.test.ts [modified] +60/-7 packages/@doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.ts [modified] +25/-27
Tools used:
Bash:gh api repos/doctolib/doctolib/contents/packages/@doctolib/dmp/src/types/DmpApi.ts?ref=FHIR-2224-monolith-return-doc-status-update-results-from-dmp-api -q '.content' 2>&1 | base64 -d 2>/dev/null | hea …(truncated, 6 more chars)
Tool results:
→ result (ok): (eval):1: no matches found: repos/doctolib/doctolib/contents/packages/@doctolib/dmp/src/types/DmpApi.ts?ref=FHIR-2224-monolith-return-doc-status-update-results-from-dmp-api
Tools used:
Bash:gh api 'repos/doctolib/doctolib/contents/packages/%40doctolib/dmp/src/types/DmpApi.ts?ref=FHIR-2224-monolith-return-doc-status-update-results-from-dmp-api' --jq '.content' 2>&1 | base64 -d 2>/dev/null …(truncated, 11 more chars)
Tool results:
→ result (ok): onDocumentsSharingFinished: () => void sharingSessionId: string }
export type SearchProps = { ins: string categories?: Record<string, string> formats?: Record<string, string> practices?: Record<string, string> creationDateBottom?: string creationDateTop?: string serviceStartDateBottom?: string serviceStartDateTop?: string serviceStopDateBottom?: string serviceStopDateTop?: string submissionDateBottom?: string submissionDateTop?: string status?: number visibility?: …(truncated, 785 more chars)
Now I have the actual PR file contents. DocumentStatusUpdateProps has been changed — healthCareSettings was REMOVED. But this change is NOT in the diff! Let me search the diff more thoroughly:
Tools used:
Bash:gh pr diff 241133 --repo doctolib/doctolib | grep -A5 -B2 "healthCareSettings\|healthcareSettings"
Tool results:
→ result (ok): - it('should throw when no cached session exists', async () => { + it('should return success: true on success', async () => { + getCachedDmpSessionMocked.mockResolvedValue({ sessionId: 'session-abc', healthcareSettings: 'SA07' }) + updateDocumentStatusCommandMocked.mockResolvedValue({ s_status: STATUS_OK }) + + const dmpApi = new DmpApi(mockProps) + const result = await dmpApi.updateDocumentStatus({ + ins: '123456789012345678901', -- - it('should throw when iC …(truncated, 3255 more chars)
Good. Now I see that the diff does NOT contain a removal of healthCareSettings: string from DocumentStatusUpdateProps type. But the actual file on the PR branch shows it removed. There's an inconsistency. Let me re-fetch the type file diff specifically:
Tools used:
Bash:gh api repos/doctolib/doctolib/pulls/241133/files --paginate --jq '.[] | select(.filename | endswith("types/DmpApi.ts")) | .patch' 2>&1
Tool results:
→ result (ok): @@ -109,7 +109,7 @@ export interface DmpApi { getPatientConsent: () => Promise<PatientConsent> updatePatientConsent: (consent: PatientConsentUpdate) => Promise<boolean> getDocumentsContentByUniqueId: (props: GetDocumentsContentProps) => Promise<DmpDocumentContent[]> - updateDocumentStatus: (props: DocumentStatusUpdateProps) => Promise<void> + updateDocumentStatus: (props: DocumentStatusUpdateProps) => Promise<DocumentStatusUpdateResult> }
export type DmpSetUpProps = { @@ -183,6 +1 …(truncated, 308 more chars)
OK, so the diff for types/DmpApi.ts indeed does NOT show removal of healthCareSettings. The base branch must already have DocumentStatusUpdateProps without healthCareSettings. The local worktree just had stale code. Good — no type error.
Let me also fetch the DmpApi.ts file from the PR branch to check the implementation in full:
Tools used:
Bash:gh api 'repos/doctolib/doctolib/contents/packages/%40doctolib/dmp/src/business/DmpApi.ts?ref=FHIR-2224-monolith-return-doc-status-update-results-from-dmp-api' --jq '.content' 2>&1 | base64 -d 2>/dev/n …(truncated, 23 more chars)
Tool results:
→ result (ok): DmpAction.GetDocumentsContentByUniqueId, payload ) return response.Documents.map(this.toDocumentContent) } catch (e) { if (e instanceof BusinessError) { // Legacy does not throw on GetDocumentsContent errors — it returns // the response as-is and toDocumentContent handles empty Documents. return [] }
throw e
}
}
const cachedSession = await getCachedDmpSession()
if (!cachedSession) {
t
…(truncated, 2930 more chars)
Now let me also fetch the adapter file:
Tools used:
Bash:gh api 'repos/doctolib/doctolib/contents/packages/%40doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.ts?ref=FHIR-2224-monolith-return-doc-status-update-results-from-dmp-api' --jq …(truncated, 59 more chars)
Tool results:
→ result (ok): ins: toEnvIns(patient.nationalHealthIdentifier), uniqueIds: params.ids, getCdaContent: true, getAuthors: true, }) return result.map((doc) => { const typeCode = doc.cdaContent?.code?._code const facet = findFacetForCDAType(typeCode, this.documentCategorizationFacets) return toNhrDocumentContentResult(doc, facet) }) }
async updateDocumentMetadata(props: NhrDocumentMetadataUpdateProps): Promise<NhrDocumentMetadataUpdateResult> { if (!props.pa …(truncated, 1573 more chars)
Now I have full picture. Let me also check whether DmpApiError exists and how it handles missing apiErrorDescription:
Tools used:
Bash:grep -rn "class DmpApiError\|DmpApiError =" /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/ 2>/dev/null | head -5
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api/DmpApiError.ts:3:export class DmpApiError extends Error {
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api/DmpApiError.ts
Tool results:
→ result (ok): 1 import type { DmpApiErrorInfo } from '../types/DmpApi'
2
3 export class DmpApiError extends Error {
4 readonly apiErrorCode: string | undefined
5
6 readonly apiErrorType: string | undefined
7
8 readonly apiErrorContext: string | undefined
9
10 readonly apiErrorDescription: string | undefined
11
12 readonly apiErrorExtendedInformations: string | undefined
13
14 readonly apiErrorTypeDescription: string | undefined
15
16 readonly status: string
17
18 constructor(info: DmpA
…(truncated, 730 more chars)
Good. The DmpApiError constructs the message from DmpApiErrorInfo correctly with fallback to 'DMP API error'. The adapter's use of new DmpApiError(result.error).message is sound.
Now I have enough context. Let me also verify the dmpService path test for BusinessError. Looking at the diff:
it('should return error result when dmpService throws BusinessError', async () => {
mockDmpService.call.mockRejectedValue(new BusinessError('DMP returned error'))
...
expect(result).toEqual({
success: false,
error: expect.objectContaining({
status: 'ERROR',
apiErrorDescription: 'The teleservice returned a business error',
}),
})
})
Constructor: new BusinessError(serviceName, message?). So 'DMP returned error' is being passed as serviceName, with message undefined → message defaults to 'The teleservice returned a business error'. So e.message = 'The teleservice returned a business error'. The test expectation matches what the implementation produces.
But wait — looking more carefully at the test diff line 1997 from grep: it('should return empty array when dmpService throws BusinessError', async () => { mockDmpService.call.mockRejectedValue(new BusinessError('error')) — this is for a different method (likely getDocumentsContent). So BusinessError('DMP returned error') is passing 'DMP returned error' as serviceName. That's a poor test setup but the assertion still holds.
Now my findings — let me organize them:
Summary
I reviewed PR #241133 (feat(FHIR-2224): return doc status update results from DmpApi). The PR refactors DmpApi.updateDocumentStatus from a throw-on-error to a discriminated-union return ({ success: true } | { success: false; error: DmpApiErrorInfo }) and updates the NHR adapter accordingly.
Files reviewed:
- packages/@doctolib/dmp/src/business/DmpApi.ts
- packages/@doctolib/dmp/src/business/DmpApi.test.ts
- packages/@doctolib/dmp/src/index.ts
- packages/@doctolib/dmp/src/types/DmpApi.ts
- packages/@doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.ts
- packages/@doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.test.ts
Findings
Important (80-89)
1. Behavior diverges from PR description on dmpService path: catches all errors, not just BusinessError (confidence: 88)
- File: packages/@doctolib/dmp/src/business/DmpApi.ts (lines ~358-367 of new version)
- The PR description states: "catches BusinessError and returns { error: { status: 'ERROR', apiErrorDescription: e.message } } instead of throwing. Non-BusinessError still propagates."
- The actual implementation catches every thrown value (} catch (e) { ... return { success: false, error: ... } }) and never re-throws. This swallows transport errors, programming bugs (TypeError, ReferenceError), and any non-business exceptions, masking them as "DMP API errors". Callers (and Sentry/monitoring) lose the ability to distinguish real infrastructure failures from business outcomes.
- Suggested fix: keep the previous shape and only convert BusinessError:
```ts
} catch (e) {
if (e instanceof BusinessError) {
return { success: false, error: { status: 'ERROR', apiErrorDescription: e.message } }
}
throw e
}
**2. Same issue on the legacy (`updateDocumentStatusCommand`) path** (confidence: 86)
- File: `packages/@doctolib/dmp/src/business/DmpApi.ts` (the new outer try/catch added around `updateDocumentStatusCommand`)
- Previously this path threw on transport/network failure; now any thrown value is caught and converted into a `DmpApiErrorInfo`-shaped result with `status: 'ERROR'`. The PR description does not mention this scope expansion, and there is no test asserting that genuinely unexpected errors should be swallowed silently — they should propagate so they can be reported. The added test "`should return error result when updateDocumentStatusCommand throws`" effectively codifies the over-broad catch but doesn't justify it.
- Suggested fix: do not wrap the entire legacy block in a catch-all. If you want to convert errors, do it narrowly (e.g. only the `s_status !== STATUS_OK` branch already handles structured non-OK responses), and let unexpected throws bubble up.
**3. PR description out of sync with the actual API shape** (confidence: 85)
- File: `packages/@doctolib/dmp/src/types/DmpApi.ts` (line 186 of the file on the PR branch)
- The PR body claims the result type is `{ error?: DmpApiErrorInfo }` and that the API "mirrors the existing `SearchResult` pattern". The actual code introduces a discriminated union: `type DocumentStatusUpdateResult = { success: true } | { success: false; error: DmpApiErrorInfo }`. This is a different pattern from `SearchResult` (`{ documents, error?: ... }`) and from the documented intent.
- This is not a bug per se — discriminated unions are arguably cleaner — but reviewers and future readers will be confused. Either:
- Update the PR description to reflect the actual shape and explicitly justify deviating from `SearchResult`, OR
- Change the type to match `SearchResult` (`interface DocumentStatusUpdateResult { error?: DmpApiErrorInfo }`) for cross-method consistency.
- The inconsistency between sibling APIs (`search` returns `{ documents, error? }`, `updateDocumentStatus` returns `{ success } | { success, error }`) is exactly the kind of style drift the PR description's "Consistency with the pattern established in PR #240721" section says it cares about.
**4. `result.error` accessed without narrowing on `success` discriminant** (confidence: 80)
- File: `packages/@doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.ts` (line ~233 in the PR branch)
- The adapter writes:
```ts
if (!result.success) {
return { ..., message: new DmpApiError(result.error).message }
}
This works because TypeScript narrows the union via !result.success. It is fine, but TypeScript-flagged because DmpApiError constructor requires DmpApiErrorInfo (not undefined). Since the union guarantees error: DmpApiErrorInfo whenever success: false, the call is type-safe today. However, if the type definition is later relaxed to error?: DmpApiErrorInfo (per the PR description), this call site silently constructs new DmpApiError(undefined) and crashes at runtime. Add a defensive check or — better — settle the type design first (see finding 3) before merging. If you keep the discriminated union, this finding becomes a non-issue.
Nits / non-blocking observations (below 80, listed for context but not actionable)
- The test
mockRejectedValue(new BusinessError('DMP returned error'))passes'DMP returned error'as the serviceName parameter, not as the message. The expectedapiErrorDescriptionis'The teleservice returned a business error'(the default), so the assertion is correct, but the test reads as if'DMP returned error'is the message. Considernew BusinessError('dmp', 'DMP returned error')for clarity. (confidence ~60, not reported in main findings). additionalInfo: { healthcareSettings: '' }inDmpNhrAdapter.test.ts— sincehealthcareSettingsis no longer used byupdateDocumentMetadata, consider removing the field from the adapter input contract or leaving the test value as'SA07'to avoid signaling that empty string is meaningful. (confidence ~55).
Recommendation
The refactor is structurally sound and the tests cover the new return shape, but the over-broad catch (e) in both code paths (findings 1 and 2) is the main concern — it changes observability of unexpected failures, contradicts the PR description, and should be tightened to only translate BusinessError to a structured result. Reconciling the type shape with SearchResult (or updating the PR description) would also reduce reviewer churn.
Relevant absolute paths: - /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/business/DmpApi.ts - …(truncated, 354 more chars)