@kenyaemr/esm-pcs-app 5.4.5-pre.1092
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +6 -0
- package/dist/1072.js +1 -0
- package/dist/1072.js.map +1 -0
- package/dist/1102.js +1 -0
- package/dist/1102.js.map +1 -0
- package/dist/1105.js +1 -0
- package/dist/1105.js.map +1 -0
- package/dist/1148.js +1 -0
- package/dist/1148.js.map +1 -0
- package/dist/1278.js +1 -0
- package/dist/1278.js.map +1 -0
- package/dist/1442.js +1 -0
- package/dist/1442.js.map +1 -0
- package/dist/15.js +1 -0
- package/dist/15.js.map +1 -0
- package/dist/1620.js +1 -0
- package/dist/1620.js.map +1 -0
- package/dist/1789.js +1 -0
- package/dist/1789.js.map +1 -0
- package/dist/1818.js +1 -0
- package/dist/1818.js.map +1 -0
- package/dist/1831.js +1 -0
- package/dist/1831.js.map +1 -0
- package/dist/1844.js +1 -0
- package/dist/1844.js.map +1 -0
- package/dist/1854.js +1 -0
- package/dist/1854.js.map +1 -0
- package/dist/1946.js +1 -0
- package/dist/1946.js.map +1 -0
- package/dist/2007.js +1 -0
- package/dist/2007.js.map +1 -0
- package/dist/2115.js +1 -0
- package/dist/2115.js.map +1 -0
- package/dist/2152.js +1 -0
- package/dist/2152.js.map +1 -0
- package/dist/2202.js +1 -0
- package/dist/2202.js.map +1 -0
- package/dist/2394.js +1 -0
- package/dist/2394.js.map +1 -0
- package/dist/2530.js +1 -0
- package/dist/2530.js.map +1 -0
- package/dist/2534.js +1 -0
- package/dist/2534.js.map +1 -0
- package/dist/2537.js +1 -0
- package/dist/2537.js.map +1 -0
- package/dist/2556.js +1 -0
- package/dist/2556.js.map +1 -0
- package/dist/2648.js +1 -0
- package/dist/2648.js.map +1 -0
- package/dist/2795.js +1 -0
- package/dist/2795.js.map +1 -0
- package/dist/2827.js +1 -0
- package/dist/2827.js.map +1 -0
- package/dist/2916.js +1 -0
- package/dist/2916.js.map +1 -0
- package/dist/2924.js +1 -0
- package/dist/2924.js.map +1 -0
- package/dist/2947.js +1 -0
- package/dist/2947.js.map +1 -0
- package/dist/3022.js +1 -0
- package/dist/3022.js.map +1 -0
- package/dist/3285.js +1 -0
- package/dist/3285.js.map +1 -0
- package/dist/3335.js +1 -0
- package/dist/3335.js.map +1 -0
- package/dist/3378.js +1 -0
- package/dist/3378.js.map +1 -0
- package/dist/3381.js +1 -0
- package/dist/3381.js.map +1 -0
- package/dist/3504.js +1 -0
- package/dist/3504.js.map +1 -0
- package/dist/3530.js +1 -0
- package/dist/3530.js.map +1 -0
- package/dist/356.js +1 -0
- package/dist/356.js.map +1 -0
- package/dist/3569.js +1 -0
- package/dist/3569.js.map +1 -0
- package/dist/3723.js +1 -0
- package/dist/3723.js.map +1 -0
- package/dist/3745.js +1 -0
- package/dist/3745.js.map +1 -0
- package/dist/3813.js +1 -0
- package/dist/3813.js.map +1 -0
- package/dist/393.js +6 -0
- package/dist/393.js.map +1 -0
- package/dist/3953.js +1 -0
- package/dist/3953.js.map +1 -0
- package/dist/3989.js +1 -0
- package/dist/3989.js.map +1 -0
- package/dist/412.js +1 -0
- package/dist/412.js.map +1 -0
- package/dist/4178.js +1 -0
- package/dist/4178.js.map +1 -0
- package/dist/4413.js +1 -0
- package/dist/4413.js.map +1 -0
- package/dist/4487.js +11 -0
- package/dist/4487.js.map +1 -0
- package/dist/4537.js +1 -0
- package/dist/4537.js.map +1 -0
- package/dist/4588.js +1 -0
- package/dist/4588.js.map +1 -0
- package/dist/4637.js +1 -0
- package/dist/4637.js.map +1 -0
- package/dist/4650.js +1 -0
- package/dist/4650.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/4817.js +1 -0
- package/dist/4817.js.map +1 -0
- package/dist/4818.js +1 -0
- package/dist/4818.js.map +1 -0
- package/dist/500.js +1 -0
- package/dist/500.js.map +1 -0
- package/dist/5086.js +1 -0
- package/dist/5086.js.map +1 -0
- package/dist/5117.js +1 -0
- package/dist/5117.js.map +1 -0
- package/dist/5279.js +1 -0
- package/dist/5279.js.map +1 -0
- package/dist/5366.js +1 -0
- package/dist/5366.js.map +1 -0
- package/dist/5598.js +1 -0
- package/dist/5598.js.map +1 -0
- package/dist/5676.js +1 -0
- package/dist/5676.js.map +1 -0
- package/dist/5731.js +5 -0
- package/dist/5731.js.map +1 -0
- package/dist/5792.js +1 -0
- package/dist/5792.js.map +1 -0
- package/dist/5825.js +1 -0
- package/dist/5825.js.map +1 -0
- package/dist/5932.js +1 -0
- package/dist/5932.js.map +1 -0
- package/dist/5941.js +1 -0
- package/dist/5941.js.map +1 -0
- package/dist/5967.js +1 -0
- package/dist/5967.js.map +1 -0
- package/dist/6104.js +1 -0
- package/dist/6104.js.map +1 -0
- package/dist/624.js +1 -0
- package/dist/624.js.map +1 -0
- package/dist/6259.js +1 -0
- package/dist/6259.js.map +1 -0
- package/dist/6273.js +1 -0
- package/dist/6273.js.map +1 -0
- package/dist/6337.js +1 -0
- package/dist/6337.js.map +1 -0
- package/dist/6427.js +1 -0
- package/dist/6427.js.map +1 -0
- package/dist/6478.js +7 -0
- package/dist/6478.js.map +1 -0
- package/dist/6508.js +1 -0
- package/dist/6508.js.map +1 -0
- package/dist/6561.js +1 -0
- package/dist/6561.js.map +1 -0
- package/dist/6566.js +1 -0
- package/dist/6566.js.map +1 -0
- package/dist/6567.js +1 -0
- package/dist/6567.js.map +1 -0
- package/dist/6634.js +1 -0
- package/dist/6634.js.map +1 -0
- package/dist/7022.js +1 -0
- package/dist/7022.js.map +1 -0
- package/dist/7085.js +1 -0
- package/dist/7085.js.map +1 -0
- package/dist/7209.js +1 -0
- package/dist/7209.js.map +1 -0
- package/dist/7291.js +1 -0
- package/dist/7291.js.map +1 -0
- package/dist/7360.js +1 -0
- package/dist/7360.js.map +1 -0
- package/dist/7453.js +1 -0
- package/dist/7453.js.map +1 -0
- package/dist/7454.js +1 -0
- package/dist/7454.js.map +1 -0
- package/dist/7478.js +1 -0
- package/dist/7478.js.map +1 -0
- package/dist/7778.js +1 -0
- package/dist/7778.js.map +1 -0
- package/dist/7782.js +1 -0
- package/dist/7782.js.map +1 -0
- package/dist/8028.js +1 -0
- package/dist/8028.js.map +1 -0
- package/dist/8031.js +1 -0
- package/dist/8031.js.map +1 -0
- package/dist/8196.js +1 -0
- package/dist/8196.js.map +1 -0
- package/dist/820.js +1 -0
- package/dist/820.js.map +1 -0
- package/dist/8240.js +1 -0
- package/dist/8240.js.map +1 -0
- package/dist/8472.js +1 -0
- package/dist/8472.js.map +1 -0
- package/dist/8607.js +1 -0
- package/dist/8607.js.map +1 -0
- package/dist/8793.js +1 -0
- package/dist/8793.js.map +1 -0
- package/dist/8924.js +1 -0
- package/dist/8924.js.map +1 -0
- package/dist/8990.js +1 -0
- package/dist/8990.js.map +1 -0
- package/dist/9060.js +1 -0
- package/dist/9060.js.map +1 -0
- package/dist/9061.js +1 -0
- package/dist/9061.js.map +1 -0
- package/dist/9087.js +1 -0
- package/dist/9087.js.map +1 -0
- package/dist/9165.js +1 -0
- package/dist/9165.js.map +1 -0
- package/dist/9274.js +1 -0
- package/dist/9274.js.map +1 -0
- package/dist/9425.js +1 -0
- package/dist/9520.js +1 -0
- package/dist/9520.js.map +1 -0
- package/dist/9547.js +1 -0
- package/dist/9547.js.map +1 -0
- package/dist/9612.js +1 -0
- package/dist/9612.js.map +1 -0
- package/dist/9771.js +1 -0
- package/dist/9771.js.map +1 -0
- package/dist/9774.js +1 -0
- package/dist/9774.js.map +1 -0
- package/dist/9806.js +1 -0
- package/dist/9806.js.map +1 -0
- package/dist/9885.js +1 -0
- package/dist/9885.js.map +1 -0
- package/dist/992.js +1 -0
- package/dist/992.js.map +1 -0
- package/dist/9941.js +1 -0
- package/dist/9941.js.map +1 -0
- package/dist/997.js +1 -0
- package/dist/997.js.map +1 -0
- package/dist/kenyaemr-esm-pcs-app.js +3 -0
- package/dist/kenyaemr-esm-pcs-app.js.buildmanifest.json +3173 -0
- package/dist/kenyaemr-esm-pcs-app.js.map +1 -0
- package/dist/main.js +3 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/package.json +50 -0
- package/rspack.config.js +1 -0
- package/src/add-dependant/add-dependant.scss +29 -0
- package/src/add-dependant/add-dependant.workspace.test.ts +65 -0
- package/src/add-dependant/add-dependant.workspace.tsx +249 -0
- package/src/config-schema.ts +67 -0
- package/src/declarations.d.ts +2 -0
- package/src/empty-svg.component.tsx +565 -0
- package/src/index.ts +34 -0
- package/src/link-hie-dependant/link-hie-dependant.scss +55 -0
- package/src/link-hie-dependant/link-hie-dependant.workspace.tsx +207 -0
- package/src/linked/dependant-row.component.test.tsx +134 -0
- package/src/linked/dependant-row.component.tsx +122 -0
- package/src/linked/dependants-list.component.tsx +60 -0
- package/src/linked/linked-participant-body.component.tsx +82 -0
- package/src/linked/linked-participant.component.test.tsx +134 -0
- package/src/linked/linked-participant.component.tsx +142 -0
- package/src/linked/participant-details.component.tsx +89 -0
- package/src/linked/use-study-sync-snackbars.ts +41 -0
- package/src/modals/delink-participant.modal.tsx +96 -0
- package/src/modals/link-dependant.modal.tsx +244 -0
- package/src/modals/link-participant.modal.tsx +143 -0
- package/src/modals/link-participant.scss +48 -0
- package/src/pcs.scss +241 -0
- package/src/resources/hie-contacts.ts +61 -0
- package/src/resources/identifiers.ts +52 -0
- package/src/resources/link-dependant.resource.test.ts +445 -0
- package/src/resources/link-dependant.resource.ts +268 -0
- package/src/resources/link-participant.resource.test.ts +336 -0
- package/src/resources/link-participant.resource.ts +452 -0
- package/src/resources/patient-create.resource.ts +296 -0
- package/src/resources/patient-search.resource.ts +106 -0
- package/src/resources/pcs.resource.test.ts +36 -0
- package/src/resources/pcs.resource.ts +182 -0
- package/src/resources/use-identifier-type-uuids.ts +38 -0
- package/src/routes.json +54 -0
- package/src/search/participant-results.component.tsx +96 -0
- package/src/search/participant-tile.component.tsx +133 -0
- package/src/search/pcs-empty-state.component.tsx +31 -0
- package/src/search/pcs-results-skeleton.component.tsx +21 -0
- package/src/search/pcs-search.component.test.tsx +169 -0
- package/src/search/pcs-search.component.tsx +228 -0
- package/src/types/index.ts +140 -0
- package/translations/am.json +120 -0
- package/translations/en.json +120 -0
- package/translations/fr.json +122 -0
- package/translations/sw.json +120 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import { getLocalIdentifierValue } from './identifiers';
|
|
3
|
+
import { createDependantPatient, createPatientFromDemographics } from './patient-create.resource';
|
|
4
|
+
import { findExistingLocalPatient } from './patient-search.resource';
|
|
5
|
+
import useSWR from 'swr';
|
|
6
|
+
import { getPrimaryContact } from './pcs.resource';
|
|
7
|
+
import { launchCheckInWorkspace, readLocalPatient, stampAndCheckIn } from './link-participant.resource';
|
|
8
|
+
import {
|
|
9
|
+
type IdentifierTypeUuids,
|
|
10
|
+
type PcsDependantDemographics,
|
|
11
|
+
type PcsHieDependant,
|
|
12
|
+
type PcsParticipant,
|
|
13
|
+
} from '../types';
|
|
14
|
+
|
|
15
|
+
interface LinkDependantOptions {
|
|
16
|
+
/** A row from `getDependentsFromContacts` — an HIE contact, not necessarily a patient here yet. */
|
|
17
|
+
dependant: any;
|
|
18
|
+
parentPhoneNumber?: string;
|
|
19
|
+
participant: PcsParticipant;
|
|
20
|
+
studyParticipantIdentifierType: string;
|
|
21
|
+
pbidsEnrollmentAttributeType: string;
|
|
22
|
+
cardseEnrollmentAttributeType: string;
|
|
23
|
+
/** The identifier types a created patient's HIE identifiers are written against. */
|
|
24
|
+
uuids: IdentifierTypeUuids;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolves a dependant to a local record, creating them when they are not registered here.
|
|
29
|
+
*
|
|
30
|
+
* `isDependent: true` matters — it makes `extractPatientIdentifiers` skip `household-number`,
|
|
31
|
+
* which belongs to the parent, so a miss here doesn't silently match the parent's own record.
|
|
32
|
+
*
|
|
33
|
+
* Deliberately calls `createPatient` rather than `createDependentPatient`: the latter launches
|
|
34
|
+
* the visit workspace inside itself, which would fire before the study data is written.
|
|
35
|
+
*/
|
|
36
|
+
async function resolveLocalDependant(
|
|
37
|
+
dependant: PcsHieDependant,
|
|
38
|
+
parentPhoneNumber: string | undefined,
|
|
39
|
+
uuids: IdentifierTypeUuids,
|
|
40
|
+
) {
|
|
41
|
+
const existing = await findExistingLocalPatient(dependant.contactData, true);
|
|
42
|
+
if (existing) {
|
|
43
|
+
return existing;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return createDependantPatient(dependant, parentPhoneNumber ? { ...uuids } : uuids, parentPhoneNumber);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Links a dependant in the HIE dependants table to a participant from their mother's PCS
|
|
51
|
+
* record: create if needed, stamp the study identifier and both enrolment attributes, check in.
|
|
52
|
+
*/
|
|
53
|
+
export async function linkDependantToParticipant({
|
|
54
|
+
dependant,
|
|
55
|
+
parentPhoneNumber,
|
|
56
|
+
participant,
|
|
57
|
+
studyParticipantIdentifierType,
|
|
58
|
+
pbidsEnrollmentAttributeType,
|
|
59
|
+
cardseEnrollmentAttributeType,
|
|
60
|
+
uuids,
|
|
61
|
+
}: LinkDependantOptions) {
|
|
62
|
+
const localPatient = await resolveLocalDependant(dependant, parentPhoneNumber, uuids);
|
|
63
|
+
|
|
64
|
+
return stampAndCheckIn({
|
|
65
|
+
localPatient,
|
|
66
|
+
participant,
|
|
67
|
+
studyParticipantIdentifierType,
|
|
68
|
+
pbidsEnrollmentAttributeType,
|
|
69
|
+
cardseEnrollmentAttributeType,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Which of the mother's HIE dependants are already linked to a PCS participant, keyed by
|
|
75
|
+
* candidate id, with the study ID each one holds.
|
|
76
|
+
*
|
|
77
|
+
* One SWR key over all candidates rather than a hook per row: Carbon's `RadioButtonGroup`
|
|
78
|
+
* clones its children expecting `RadioButton` elements, so a per-row wrapper component would
|
|
79
|
+
* break its `valueSelected` / `onChange` wiring.
|
|
80
|
+
*/
|
|
81
|
+
export function useHieDependantLinkState(candidates: Array<any>, identifierTypes: Array<string>) {
|
|
82
|
+
const key = candidates.length
|
|
83
|
+
? `pcs-hie-dependant-link-state/${candidates.map((candidate) => candidate.id).join(',')}`
|
|
84
|
+
: null;
|
|
85
|
+
|
|
86
|
+
const { data, isLoading } = useSWR(key, async () => {
|
|
87
|
+
const resolved = await Promise.all(
|
|
88
|
+
candidates.map(async (candidate) => {
|
|
89
|
+
const localPatient = await findExistingLocalPatient(candidate.contactData, true).catch(() => null);
|
|
90
|
+
const studyId = identifierTypes
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.map((identifierType) => getLocalIdentifierValue(localPatient, identifierType))
|
|
93
|
+
.find(Boolean);
|
|
94
|
+
|
|
95
|
+
return [candidate.id, studyId] as const;
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return Object.fromEntries(resolved) as Record<string, string | undefined>;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return { linkedById: data ?? {}, isChecking: isLoading };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface CreateAndLinkOptions {
|
|
106
|
+
participant: PcsParticipant;
|
|
107
|
+
nationalIdUUID: string;
|
|
108
|
+
phoneAttributeTypeUUID: string;
|
|
109
|
+
studyParticipantIdentifierType: string;
|
|
110
|
+
pbidsEnrollmentAttributeType: string;
|
|
111
|
+
cardseEnrollmentAttributeType: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A PCS participant mapped onto the same create. */
|
|
115
|
+
function createPatientFromParticipant(
|
|
116
|
+
participant: PcsParticipant,
|
|
117
|
+
nationalIdUUID: string,
|
|
118
|
+
phoneAttributeTypeUUID: string,
|
|
119
|
+
) {
|
|
120
|
+
const contact = getPrimaryContact(participant);
|
|
121
|
+
|
|
122
|
+
return createPatientFromDemographics(
|
|
123
|
+
{
|
|
124
|
+
firstName: participant.firstName,
|
|
125
|
+
middleName: participant.middleName,
|
|
126
|
+
lastName: participant.lastName,
|
|
127
|
+
sex: participant.sex,
|
|
128
|
+
dateOfBirth: participant.dateOfBirth,
|
|
129
|
+
nationalId: contact?.nationalId,
|
|
130
|
+
phone: contact?.phone,
|
|
131
|
+
},
|
|
132
|
+
nationalIdUUID,
|
|
133
|
+
phoneAttributeTypeUUID,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* For a PCS dependant the HIE has no record of: build the patient from PCS's own demographics,
|
|
139
|
+
* then stamp **this participant's** identifier — they already hold a permanent one, so nothing
|
|
140
|
+
* temporary is minted and PCS keeps a single record for the child.
|
|
141
|
+
*/
|
|
142
|
+
export async function createAndLinkFromParticipant({
|
|
143
|
+
participant,
|
|
144
|
+
nationalIdUUID,
|
|
145
|
+
phoneAttributeTypeUUID,
|
|
146
|
+
studyParticipantIdentifierType,
|
|
147
|
+
pbidsEnrollmentAttributeType,
|
|
148
|
+
cardseEnrollmentAttributeType,
|
|
149
|
+
}: CreateAndLinkOptions) {
|
|
150
|
+
const localPatient = await createPatientFromParticipant(participant, nationalIdUUID, phoneAttributeTypeUUID);
|
|
151
|
+
|
|
152
|
+
return stampAndCheckIn({
|
|
153
|
+
localPatient,
|
|
154
|
+
participant,
|
|
155
|
+
studyParticipantIdentifierType,
|
|
156
|
+
pbidsEnrollmentAttributeType,
|
|
157
|
+
cardseEnrollmentAttributeType,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface CreatePcsParticipantOptions {
|
|
162
|
+
patientUuid: string;
|
|
163
|
+
motherId: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The request as the module documents it. Kept real and separately testable so the path and
|
|
168
|
+
* both field names are pinned rather than discovered at integration time — the same split the
|
|
169
|
+
* search side uses with `buildParticipantSearchUrl`.
|
|
170
|
+
*/
|
|
171
|
+
export function buildCreateParticipantRequest({ patientUuid, motherId }: CreatePcsParticipantOptions) {
|
|
172
|
+
return {
|
|
173
|
+
url: `${restBaseUrl}/pbids-participants`,
|
|
174
|
+
options: {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
177
|
+
body: { patientUuid, motherId },
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Documented failures, all of which surface through the form's notification rather than
|
|
184
|
+
* indicating a bug here: `409` when the patient already holds a study or temporary identifier,
|
|
185
|
+
* `400` for a missing field or an unknown patient or mother, and `503` when the PCS database is
|
|
186
|
+
* unreachable.
|
|
187
|
+
*/
|
|
188
|
+
async function createPcsParticipant({ patientUuid, motherId }: CreatePcsParticipantOptions): Promise<PcsParticipant> {
|
|
189
|
+
const request = buildCreateParticipantRequest({ patientUuid, motherId });
|
|
190
|
+
const { data } = await openmrsFetch<PcsParticipant>(request.url, request.options);
|
|
191
|
+
return data;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface CreateDependantWithTemporaryIdOptions {
|
|
195
|
+
demographics: PcsDependantDemographics;
|
|
196
|
+
motherIndividualId: string;
|
|
197
|
+
nationalIdUUID: string;
|
|
198
|
+
phoneAttributeTypeUUID: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The tail both temporary-ID flows share: have the module create a participant against the
|
|
203
|
+
* mother's and mint the study id, then start the visit.
|
|
204
|
+
*
|
|
205
|
+
* Nothing is written client-side. The module owns both sides — it assigns the identifier and
|
|
206
|
+
* sets the `PBIDS Enrolled` / `CARDSE Enrolled` attributes to match the PCS row, which the docs
|
|
207
|
+
* are explicit must stay in step. The re-read is how we see what it wrote; the record we hold
|
|
208
|
+
* could not carry it.
|
|
209
|
+
*/
|
|
210
|
+
async function mintTemporaryIdAndCheckIn(localPatient: any, motherIndividualId: string) {
|
|
211
|
+
if (!localPatient?.uuid) {
|
|
212
|
+
throw new Error('The patient record could not be created');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const participant = await createPcsParticipant({ patientUuid: localPatient.uuid, motherId: motherIndividualId });
|
|
216
|
+
|
|
217
|
+
const updated = (await readLocalPatient(localPatient.uuid).catch(() => null)) ?? localPatient;
|
|
218
|
+
|
|
219
|
+
await launchCheckInWorkspace(updated, updated.uuid);
|
|
220
|
+
|
|
221
|
+
return { localPatient: updated, participant };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* For an infant in neither PCS nor the HIE: register them here from typed demographics, then
|
|
226
|
+
* mint a temporary study id against their mother's participant.
|
|
227
|
+
*/
|
|
228
|
+
export async function createDependantWithTemporaryId({
|
|
229
|
+
demographics,
|
|
230
|
+
motherIndividualId,
|
|
231
|
+
nationalIdUUID,
|
|
232
|
+
phoneAttributeTypeUUID,
|
|
233
|
+
}: CreateDependantWithTemporaryIdOptions) {
|
|
234
|
+
const localPatient = await createPatientFromDemographics(demographics, nationalIdUUID, phoneAttributeTypeUUID);
|
|
235
|
+
|
|
236
|
+
return mintTemporaryIdAndCheckIn(localPatient, motherIndividualId);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface LinkHieDependantOptions {
|
|
240
|
+
/** A row from `getDependentsFromContacts` — an HIE contact, not necessarily a patient here yet. */
|
|
241
|
+
dependant: any;
|
|
242
|
+
parentPhoneNumber?: string;
|
|
243
|
+
motherIndividualId: string;
|
|
244
|
+
/** The identifier types a created patient's HIE identifiers are written against. */
|
|
245
|
+
uuids: IdentifierTypeUuids;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* For a child the HIE lists under her mother but PCS has no row for: reuse her local record when
|
|
250
|
+
* she is already registered here, create it from the HIE contact when she isn't, then mint a
|
|
251
|
+
* temporary study id.
|
|
252
|
+
*
|
|
253
|
+
* Differs from `createDependantWithTemporaryId` only at the head — resolve-or-create from an HIE
|
|
254
|
+
* contact, rather than create from typed demographics.
|
|
255
|
+
*
|
|
256
|
+
* A child linked elsewhere between the list rendering and this call comes back as the documented
|
|
257
|
+
* `409`; that race belongs to the server, not to a second client-side check.
|
|
258
|
+
*/
|
|
259
|
+
export async function linkHieDependantWithTemporaryId({
|
|
260
|
+
dependant,
|
|
261
|
+
parentPhoneNumber,
|
|
262
|
+
motherIndividualId,
|
|
263
|
+
uuids,
|
|
264
|
+
}: LinkHieDependantOptions) {
|
|
265
|
+
const localPatient = await resolveLocalDependant(dependant, parentPhoneNumber, uuids);
|
|
266
|
+
|
|
267
|
+
return mintTemporaryIdAndCheckIn(localPatient, motherIndividualId);
|
|
268
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getSessionLocation, launchWorkspace2, launchWorkspaceGroup2, openmrsFetch } from '@openmrs/esm-framework';
|
|
3
|
+
import { createDependantPatient, createPatientFromHiePatient } from './patient-create.resource';
|
|
4
|
+
import { findExistingLocalPatient } from './patient-search.resource';
|
|
5
|
+
import { delinkParticipant, linkParticipantToPatient, syncStudyAttributes } from './link-participant.resource';
|
|
6
|
+
import { type PcsParticipant, type PcsSearchSubject } from '../types';
|
|
7
|
+
|
|
8
|
+
vi.mock('@openmrs/esm-framework', async (importOriginal) => ({
|
|
9
|
+
...(await importOriginal<typeof import('@openmrs/esm-framework')>()),
|
|
10
|
+
openmrsFetch: vi.fn(),
|
|
11
|
+
getSessionLocation: vi.fn(),
|
|
12
|
+
launchWorkspace2: vi.fn(),
|
|
13
|
+
launchWorkspaceGroup2: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('./patient-create.resource', async (importOriginal) => ({
|
|
17
|
+
...(await importOriginal<typeof import('./patient-create.resource')>()),
|
|
18
|
+
createDependantPatient: vi.fn(),
|
|
19
|
+
createPatientFromHiePatient: vi.fn(),
|
|
20
|
+
}));
|
|
21
|
+
vi.mock('./patient-search.resource', () => ({ findExistingLocalPatient: vi.fn() }));
|
|
22
|
+
|
|
23
|
+
const mockOpenmrsFetch = vi.mocked(openmrsFetch);
|
|
24
|
+
const mockGetSessionLocation = vi.mocked(getSessionLocation);
|
|
25
|
+
const mockCreateDependantPatient = vi.mocked(createDependantPatient);
|
|
26
|
+
const mockCreatePatientFromHie = vi.mocked(createPatientFromHiePatient);
|
|
27
|
+
const mockFindExistingLocalPatient = vi.mocked(findExistingLocalPatient);
|
|
28
|
+
|
|
29
|
+
const uuids = {
|
|
30
|
+
nationalIdUUID: 'national-id-type',
|
|
31
|
+
shaNumberUUID: 'sha-number-type',
|
|
32
|
+
passportUUID: 'passport-type',
|
|
33
|
+
birthCertificateUUID: 'birth-certificate-type',
|
|
34
|
+
crIdentificationNumberUUID: 'cr-number-type',
|
|
35
|
+
phoneAttributeTypeUUID: 'phone-attr-type',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const STUDY_ID_TYPE = 'study-id-type-uuid';
|
|
39
|
+
const PBIDS_ENROLLMENT_TYPE = 'pbids-enrollment-type-uuid';
|
|
40
|
+
const CARDSE_ENROLLMENT_TYPE = 'cardse-enrollment-type-uuid';
|
|
41
|
+
|
|
42
|
+
const participant = {
|
|
43
|
+
individualId: '901-1-1-3',
|
|
44
|
+
firstName: 'DENNIS',
|
|
45
|
+
lastName: 'ODONGO',
|
|
46
|
+
sex: 'M',
|
|
47
|
+
pbidsEnrolled: true,
|
|
48
|
+
cardse: false,
|
|
49
|
+
mother: null,
|
|
50
|
+
compound: { compoundId: '901-1', headIndividualId: '901-1-1-1', headFirstName: 'JOHN', headLastName: 'ODONGO' },
|
|
51
|
+
village: { code: '901', name: 'TEST ABUYA' },
|
|
52
|
+
contacts: [],
|
|
53
|
+
matchedOn: 'name',
|
|
54
|
+
matchType: 'EXACT',
|
|
55
|
+
} as PcsParticipant;
|
|
56
|
+
|
|
57
|
+
const localSubject: PcsSearchSubject = {
|
|
58
|
+
id: 'local-uuid',
|
|
59
|
+
source: 'local',
|
|
60
|
+
patient: { resourceType: 'Patient', id: 'local-uuid' },
|
|
61
|
+
name: 'Dennis Odongo',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const hieSubject: PcsSearchSubject = {
|
|
65
|
+
id: 'cr-number',
|
|
66
|
+
source: 'hie',
|
|
67
|
+
patient: { resourceType: 'Patient', id: 'cr-number' },
|
|
68
|
+
name: 'Dennis Odongo',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const link = (subject: PcsSearchSubject) =>
|
|
72
|
+
linkParticipantToPatient({
|
|
73
|
+
subject,
|
|
74
|
+
participant,
|
|
75
|
+
studyParticipantIdentifierType: STUDY_ID_TYPE,
|
|
76
|
+
pbidsEnrollmentAttributeType: PBIDS_ENROLLMENT_TYPE,
|
|
77
|
+
cardseEnrollmentAttributeType: CARDSE_ENROLLMENT_TYPE,
|
|
78
|
+
uuids,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/** Routes the mocked fetch by URL so each test only states what it cares about. */
|
|
82
|
+
const respondWith = ({ localPatient, attributes = [] }: { localPatient?: any; attributes?: Array<any> }) => {
|
|
83
|
+
let customReads = 0;
|
|
84
|
+
|
|
85
|
+
mockOpenmrsFetch.mockImplementation((url: string) => {
|
|
86
|
+
if (url.includes('/attribute?v=default')) {
|
|
87
|
+
return Promise.resolve({ data: { results: attributes } } as any);
|
|
88
|
+
}
|
|
89
|
+
if (url.includes('?v=custom')) {
|
|
90
|
+
// A local subject is read twice: once to resolve it, once again after the writes. Only
|
|
91
|
+
// the second is tagged, so a test asserting `reread` genuinely proves the caller got the
|
|
92
|
+
// refreshed record — returning the first one is what left the row showing "PCS Link".
|
|
93
|
+
customReads += 1;
|
|
94
|
+
return Promise.resolve({ data: { ...localPatient, reread: customReads > 1 } } as any);
|
|
95
|
+
}
|
|
96
|
+
return Promise.resolve({ data: {} } as any);
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const writes = () =>
|
|
101
|
+
mockOpenmrsFetch.mock.calls.filter(([, options]) => (options as any)?.method === 'POST').map(([url]) => url);
|
|
102
|
+
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
vi.clearAllMocks();
|
|
105
|
+
mockGetSessionLocation.mockResolvedValue({ uuid: 'location-uuid' } as any);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('linkParticipantToPatient', () => {
|
|
109
|
+
it('uses the existing record for a local patient and never creates one', async () => {
|
|
110
|
+
respondWith({ localPatient: { uuid: 'local-uuid', identifiers: [] } });
|
|
111
|
+
|
|
112
|
+
const result = await link(localSubject);
|
|
113
|
+
|
|
114
|
+
expect(mockCreatePatientFromHie).not.toHaveBeenCalled();
|
|
115
|
+
expect(writes()).toContain('/ws/rest/v1/patient/local-uuid/identifier');
|
|
116
|
+
// Returning the pre-write object is what made the dependants row keep offering to link.
|
|
117
|
+
expect(result.reread).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('creates the patient for an HIE match, then writes the study data, then checks in once', async () => {
|
|
121
|
+
mockFindExistingLocalPatient.mockResolvedValue(null);
|
|
122
|
+
mockCreatePatientFromHie.mockResolvedValue({ uuid: 'new-uuid', identifiers: [] } as any);
|
|
123
|
+
respondWith({});
|
|
124
|
+
|
|
125
|
+
await link(hieSubject);
|
|
126
|
+
|
|
127
|
+
expect(mockCreatePatientFromHie).toHaveBeenCalledOnce();
|
|
128
|
+
expect(writes()).toEqual([
|
|
129
|
+
'/ws/rest/v1/patient/new-uuid/identifier',
|
|
130
|
+
'/ws/rest/v1/person/new-uuid/attribute',
|
|
131
|
+
'/ws/rest/v1/person/new-uuid/attribute',
|
|
132
|
+
]);
|
|
133
|
+
// The workspace must open last, and exactly once — the Check In path double-launches
|
|
134
|
+
// because createHIEPatient launches internally; this must not reproduce that.
|
|
135
|
+
expect(mockLaunchedAfterWrites()).toBe(true);
|
|
136
|
+
expect(launchWorkspaceGroup2).toHaveBeenCalledOnce();
|
|
137
|
+
expect(launchWorkspace2).toHaveBeenCalledOnce();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('refuses to re-point a patient already linked to another participant', async () => {
|
|
141
|
+
respondWith({
|
|
142
|
+
localPatient: {
|
|
143
|
+
uuid: 'local-uuid',
|
|
144
|
+
identifiers: [{ uuid: 'identifier-uuid', identifier: '901-9-9-9', identifierType: { uuid: STUDY_ID_TYPE } }],
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// This used to overwrite the identifier in place, silently moving the child from one
|
|
149
|
+
// participant to another and destroying the previous link.
|
|
150
|
+
await expect(link(localSubject)).rejects.toThrow('already linked to PCS participant 901-9-9-9');
|
|
151
|
+
expect(writes()).toEqual([]);
|
|
152
|
+
expect(launchWorkspace2).not.toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('is a no-op when the patient already holds this same participant', async () => {
|
|
156
|
+
respondWith({
|
|
157
|
+
localPatient: {
|
|
158
|
+
uuid: 'local-uuid',
|
|
159
|
+
identifiers: [{ uuid: 'identifier-uuid', identifier: '901-1-1-3', identifierType: { uuid: STUDY_ID_TYPE } }],
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await link(localSubject);
|
|
164
|
+
|
|
165
|
+
// Re-linking to the same participant stays idempotent — no identifier write at all.
|
|
166
|
+
expect(writes().filter((url) => url.includes('/identifier'))).toEqual([]);
|
|
167
|
+
expect(launchWorkspace2).toHaveBeenCalledOnce();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('leaves an attribute alone when it already holds the right value', async () => {
|
|
171
|
+
respondWith({
|
|
172
|
+
localPatient: { uuid: 'local-uuid', identifiers: [] },
|
|
173
|
+
attributes: [{ uuid: 'attribute-uuid', value: 'true', attributeType: { uuid: PBIDS_ENROLLMENT_TYPE } }],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await link(localSubject);
|
|
177
|
+
|
|
178
|
+
// PBIDS enrollment already reads 'true'; only the CARDSE attribute is written.
|
|
179
|
+
expect(writes().filter((url) => url.includes('/attribute'))).toEqual(['/ws/rest/v1/person/local-uuid/attribute']);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('writes both enrolment flags as boolean strings', async () => {
|
|
183
|
+
respondWith({ localPatient: { uuid: 'local-uuid', identifiers: [] } });
|
|
184
|
+
|
|
185
|
+
await link(localSubject);
|
|
186
|
+
|
|
187
|
+
const attributePosts = mockOpenmrsFetch.mock.calls
|
|
188
|
+
.filter(([url, options]) => url.includes('/attribute') && (options as any)?.method === 'POST')
|
|
189
|
+
.map(([, options]) => (options as any).body);
|
|
190
|
+
|
|
191
|
+
// The fixture is pbidsEnrolled: true, cardse: false — `false` is written, not skipped.
|
|
192
|
+
expect(attributePosts).toEqual([
|
|
193
|
+
{ attributeType: PBIDS_ENROLLMENT_TYPE, value: 'true' },
|
|
194
|
+
{ attributeType: CARDSE_ENROLLMENT_TYPE, value: 'false' },
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('throws without checking in when a write fails', async () => {
|
|
199
|
+
respondWith({ localPatient: { uuid: 'local-uuid', identifiers: [] } });
|
|
200
|
+
mockOpenmrsFetch.mockImplementation((url: string, options?: any) => {
|
|
201
|
+
if (options?.method === 'POST') {
|
|
202
|
+
return Promise.reject(new Error('Identifier already in use'));
|
|
203
|
+
}
|
|
204
|
+
return Promise.resolve({ data: { uuid: 'local-uuid', identifiers: [] } } as any);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await expect(link(localSubject)).rejects.toThrow('Identifier already in use');
|
|
208
|
+
expect(launchWorkspace2).not.toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
/** True when every POST was issued before the workspace was launched. */
|
|
213
|
+
function mockLaunchedAfterWrites() {
|
|
214
|
+
return (
|
|
215
|
+
vi.mocked(launchWorkspaceGroup2).mock.invocationCallOrder[0] >
|
|
216
|
+
Math.max(...mockOpenmrsFetch.mock.invocationCallOrder)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
describe('delinkParticipant', () => {
|
|
221
|
+
const delink = (localPatient: any) =>
|
|
222
|
+
delinkParticipant({
|
|
223
|
+
localPatient,
|
|
224
|
+
studyParticipantIdentifierType: STUDY_ID_TYPE,
|
|
225
|
+
pbidsEnrollmentAttributeType: PBIDS_ENROLLMENT_TYPE,
|
|
226
|
+
cardseEnrollmentAttributeType: CARDSE_ENROLLMENT_TYPE,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const deletes = () =>
|
|
230
|
+
mockOpenmrsFetch.mock.calls.filter(([, options]) => (options as any)?.method === 'DELETE').map(([url]) => url);
|
|
231
|
+
|
|
232
|
+
it('voids the identifier and both attributes', async () => {
|
|
233
|
+
mockOpenmrsFetch.mockResolvedValue({
|
|
234
|
+
data: {
|
|
235
|
+
results: [
|
|
236
|
+
{ uuid: 'status-attr', attributeType: { uuid: PBIDS_ENROLLMENT_TYPE } },
|
|
237
|
+
{ uuid: 'category-attr', attributeType: { uuid: CARDSE_ENROLLMENT_TYPE } },
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
} as any);
|
|
241
|
+
|
|
242
|
+
await delink({
|
|
243
|
+
uuid: 'local-uuid',
|
|
244
|
+
identifiers: [{ uuid: 'identifier-uuid', identifierType: { uuid: STUDY_ID_TYPE } }],
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(deletes()).toEqual([
|
|
248
|
+
'/ws/rest/v1/patient/local-uuid/identifier/identifier-uuid',
|
|
249
|
+
'/ws/rest/v1/person/local-uuid/attribute/status-attr',
|
|
250
|
+
'/ws/rest/v1/person/local-uuid/attribute/category-attr',
|
|
251
|
+
]);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('still succeeds when the study data was only partly written', async () => {
|
|
255
|
+
mockOpenmrsFetch.mockResolvedValue({ data: { results: [] } } as any);
|
|
256
|
+
|
|
257
|
+
await expect(delink({ uuid: 'local-uuid', identifiers: [] })).resolves.toBeUndefined();
|
|
258
|
+
expect(deletes()).toEqual([]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('leaves an already-voided attribute alone', async () => {
|
|
262
|
+
mockOpenmrsFetch.mockResolvedValue({
|
|
263
|
+
data: {
|
|
264
|
+
results: [{ uuid: 'status-attr', voided: true, attributeType: { uuid: PBIDS_ENROLLMENT_TYPE } }],
|
|
265
|
+
},
|
|
266
|
+
} as any);
|
|
267
|
+
|
|
268
|
+
await delink({ uuid: 'local-uuid', identifiers: [] });
|
|
269
|
+
|
|
270
|
+
expect(deletes()).toEqual([]);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('syncStudyAttributes', () => {
|
|
275
|
+
const sync = () =>
|
|
276
|
+
syncStudyAttributes({
|
|
277
|
+
personUuid: 'local-uuid',
|
|
278
|
+
participant,
|
|
279
|
+
pbidsEnrollmentAttributeType: PBIDS_ENROLLMENT_TYPE,
|
|
280
|
+
cardseEnrollmentAttributeType: CARDSE_ENROLLMENT_TYPE,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const posts = () =>
|
|
284
|
+
mockOpenmrsFetch.mock.calls
|
|
285
|
+
.filter(([, options]) => (options as any)?.method === 'POST')
|
|
286
|
+
.map(([url, options]) => [url, (options as any).body]);
|
|
287
|
+
|
|
288
|
+
/** The fixture is pbidsEnrolled: true, cardse: false. */
|
|
289
|
+
const withAttributes = (attributes: Array<any>) =>
|
|
290
|
+
mockOpenmrsFetch.mockImplementation((url: string, options?: any) => {
|
|
291
|
+
if (options?.method === 'POST') {
|
|
292
|
+
return Promise.resolve({ data: {} } as any);
|
|
293
|
+
}
|
|
294
|
+
return Promise.resolve({ data: { results: attributes } } as any);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('writes only the flag that drifted and reports just that one', async () => {
|
|
298
|
+
withAttributes([
|
|
299
|
+
{ uuid: 'pbids-attr', value: 'true', attributeType: { uuid: PBIDS_ENROLLMENT_TYPE } },
|
|
300
|
+
{ uuid: 'cardse-attr', value: 'true', attributeType: { uuid: CARDSE_ENROLLMENT_TYPE } },
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
await expect(sync()).resolves.toEqual(['cardse']);
|
|
304
|
+
expect(posts()).toEqual([['/ws/rest/v1/person/local-uuid/attribute/cardse-attr', { value: 'false' }]]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('writes nothing when the patient already matches PCS', async () => {
|
|
308
|
+
withAttributes([
|
|
309
|
+
{ uuid: 'pbids-attr', value: 'true', attributeType: { uuid: PBIDS_ENROLLMENT_TYPE } },
|
|
310
|
+
{ uuid: 'cardse-attr', value: 'false', attributeType: { uuid: CARDSE_ENROLLMENT_TYPE } },
|
|
311
|
+
]);
|
|
312
|
+
|
|
313
|
+
await expect(sync()).resolves.toEqual([]);
|
|
314
|
+
expect(posts()).toEqual([]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('creates both attributes when the patient has none', async () => {
|
|
318
|
+
withAttributes([]);
|
|
319
|
+
|
|
320
|
+
await expect(sync()).resolves.toEqual(['pbids', 'cardse']);
|
|
321
|
+
expect(posts()).toEqual([
|
|
322
|
+
['/ws/rest/v1/person/local-uuid/attribute', { attributeType: PBIDS_ENROLLMENT_TYPE, value: 'true' }],
|
|
323
|
+
['/ws/rest/v1/person/local-uuid/attribute', { attributeType: CARDSE_ENROLLMENT_TYPE, value: 'false' }],
|
|
324
|
+
]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('propagates a write failure so the caller can report it', async () => {
|
|
328
|
+
mockOpenmrsFetch.mockImplementation((_url: string, options?: any) =>
|
|
329
|
+
options?.method === 'POST'
|
|
330
|
+
? Promise.reject(new Error('Attribute type not found'))
|
|
331
|
+
: Promise.resolve({ data: { results: [] } } as any),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
await expect(sync()).rejects.toThrow('Attribute type not found');
|
|
335
|
+
});
|
|
336
|
+
});
|