@choiceapps/communityservices-sdk 0.8.0 → 0.9.1
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/API.md +250 -0
- package/CHANGELOG.md +20 -0
- package/browser/enrollment-workflow.d.mts +8 -5
- package/browser/enrollment-workflow.mjs +54 -25
- package/browser/household-enrollment-discovery.mjs +11 -2
- package/browser/index.d.mts +3 -1
- package/browser/playwright-session.mjs +5 -3
- package/browser/workflow-error-codes.mjs +3 -0
- package/docs/assessments.md +4 -2
- package/docs/capabilities.md +7 -1
- package/docs/enrollments.md +11 -4
- package/docs/reference.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/docs/vendor-issues/enrollment-household-identity.md +13 -0
- package/docs/vendor-issues/yes-no-answers.md +11 -3
- package/examples/enrollment-discovery-and-correction.mts +1 -1
- package/package.json +1 -1
package/API.md
CHANGED
|
@@ -84,3 +84,253 @@ Both versions have the methods below. Import from `@choiceapps/communityservices
|
|
|
84
84
|
| `tenants.update` | PUT | `/tenants/UpdateTenantRecord` | |
|
|
85
85
|
| `tenants.updateByCode` | PUT | `/tenants/UpdateTenantRecord/{tenant_code}` | |
|
|
86
86
|
| `tenants.delete` | DELETE | `/tenants/DeleteTenantRecord` | |
|
|
87
|
+
|
|
88
|
+
## Enrollment workflow errors
|
|
89
|
+
|
|
90
|
+
This SDK-owned code inventory is generated from the workflow guards and stable reason map. These codes are separate from the REST Swagger operations above. Classify errors by `code`, never by message text. Existing codes remain stable when wording changes; later releases may add codes. Application-supplied constructor codes are outside this inventory.
|
|
91
|
+
|
|
92
|
+
`EnrollmentWorkflowError.operation` identifies the SDK operation. `committed: false` means that operation has not attempted a WellSky record write; it does not promise that no local journal exists or that another operation wrote nothing. `committed: "unknown"` preserves an attempted or unreadable prior outcome. Never choose a new key solely from an HTTP status or unchanged read.
|
|
93
|
+
|
|
94
|
+
A preflight refusal after enrollment creation can identify `step: "scalars"` while the enrollment is already saved. Keep the same key and reconcile the existing journal. A failed `resolveEntryCorrection` can have `committed: false` for resolution itself and `originalCommitted: "unknown"` for the original correction. See [recovery guidance](docs/troubleshooting.md#workflow-error-codes).
|
|
95
|
+
|
|
96
|
+
| Code | Reason or meaning |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `ACTIVE_CLIENT_REQUIRED` | active client required |
|
|
99
|
+
| `ACTIVE_HOUSEHOLD_REQUIRED` | active household required |
|
|
100
|
+
| `ACTIVE_HOUSEHOLD_REQUIRED_FOR_ADOPTION` | active household required for adoption |
|
|
101
|
+
| `ACTIVE_OPERATIONAL_PROGRAM_REQUIRED` | active operational program required |
|
|
102
|
+
| `ADDED_ENROLLMENT_IDENTITY_CHANGED` | added enrollment identity changed |
|
|
103
|
+
| `ADDITION_REQUIRES_NEW_HOUSEHOLD_MEMBERS` | addition requires new household members |
|
|
104
|
+
| `ADDITIONAL_MEMBER_ALREADY_HAS_AN_INDEPENDENT_ENROLLMENT` | additional member already has an independent enrollment |
|
|
105
|
+
| `ADDITIONAL_MEMBER_ASSESSMENT_CONFIGURATION_CHANGED` | additional member assessment configuration changed |
|
|
106
|
+
| `ADDITIONAL_MEMBER_ENTRY_MUST_FOLLOW_ORIGINAL_ENTRY` | additional member entry must follow original entry |
|
|
107
|
+
| `ADOPTED_ENROLLMENT_IDENTITY_OR_CONFIGURATION_CHANGED` | adopted enrollment identity or configuration changed |
|
|
108
|
+
| `ADOPTION_COMPLETE_COHORT_REQUIRED` | adoption requires the complete linked enrollment cohort |
|
|
109
|
+
| `ADOPTION_ENROLLMENT_CONTEXT_MISMATCH` | adoption enrollment context mismatch |
|
|
110
|
+
| `ADOPTION_ENROLLMENT_GROUP_MISMATCH` | adoption enrollment group changed<br>adoption enrollment group mismatch |
|
|
111
|
+
| `ADOPTION_ENROLLMENT_HOUSEHOLD_MISMATCH` | adoption enrollment household mismatch<br>adoption household differs from enrollment association |
|
|
112
|
+
| `ADOPTION_EXIT_PRECEDES_ENTRY` | adoption exit precedes entry |
|
|
113
|
+
| `ADOPTION_HEAD_EVIDENCE_REQUIRED` | adoption head evidence required<br>native adoption requires original entry head relationship evidence |
|
|
114
|
+
| `ADOPTION_HOUSEHOLD_HEAD_MISMATCH` | adoption household head mismatch |
|
|
115
|
+
| `ADOPTION_HOUSEHOLD_MEMBERSHIP_MISMATCH` | adoption household membership mismatch |
|
|
116
|
+
| `ADOPTION_HOUSEHOLD_REQUIRED` | linked household enrollment requires adoptHouseholdEnrollment |
|
|
117
|
+
| `ADOPTION_KEY_CONFLICTS_WITH_PREVIOUS_INTENT` | adoption key conflicts with previous intent |
|
|
118
|
+
| `ADOPTION_REQUIRES_THE_ORIGINAL_ENROLLMENT_HEAD_RELATIONSHIP` | adoption requires the original enrollment head relationship |
|
|
119
|
+
| `AMBIGUOUS_ASSESSMENT_READ` | ambiguous assessment read |
|
|
120
|
+
| `AMBIGUOUS_CLOSURE_SNAPSHOT` | ambiguous closure snapshot |
|
|
121
|
+
| `AMBIGUOUS_CORRECTION_ASSESSMENT_READ` | ambiguous correction assessment read |
|
|
122
|
+
| `AMBIGUOUS_CORRECTION_ROW` | ambiguous correction row |
|
|
123
|
+
| `AMBIGUOUS_CORRECTION_ROW_CELL` | ambiguous correction row cell |
|
|
124
|
+
| `AMBIGUOUS_HOUSEHOLD_MEMBERSHIP` | ambiguous household membership |
|
|
125
|
+
| `ANOTHER_UNFINISHED_HOUSEHOLD_WORKFLOW_MUST_BE_RECONCILED_FIRST` | another unfinished household workflow must be reconciled first |
|
|
126
|
+
| `ANOTHER_UNFINISHED_WORKFLOW_MUST_BE_RECONCILED_FIRST` | another unfinished workflow must be reconciled first |
|
|
127
|
+
| `ANSWER_ENCODING_UNAVAILABLE` | answer encoding unavailable |
|
|
128
|
+
| `ASSESSMENT_FIELD_UNCOVERED` | assessment field uncovered |
|
|
129
|
+
| `ASSESSMENT_LOCATIONS_MUST_BE_A_MAP_BY_ENTRY_EXIT_TYPE` | assessment locations must be a map by Entry/Exit type |
|
|
130
|
+
| `ASSESSMENT_LOCATIONS_REQUIRED` | assessment locations required |
|
|
131
|
+
| `ASSESSMENT_METADATA_INCOMPLETE` | assessment metadata incomplete |
|
|
132
|
+
| `ASSESSMENT_PLAN_REQUIRED` | assessment plan required |
|
|
133
|
+
| `ATTEMPTED_CORRECTION_REQUIRED_FOR_RESOLUTION` | attempted correction required for resolution |
|
|
134
|
+
| `AUTHORITATIVE_ADOPTION_EXIT_DATE_REQUIRED` | authoritative adoption exit date required |
|
|
135
|
+
| `AUTHORITATIVE_ENROLLMENT_SESSION_REQUIRED_FOR_ADOPTION` | authoritative enrollment session required for adoption |
|
|
136
|
+
| `AUTHORITATIVE_EXIT_DETAILS_READER_REQUIRED` | authoritative exit details reader required |
|
|
137
|
+
| `AUTHORITATIVE_EXIT_READ_REQUIRED` | authoritative exit read required |
|
|
138
|
+
| `AUTHORITATIVE_HEAD_OF_HOUSEHOLD_EXIT_DATES_REQUIRED` | authoritative head of household exit dates required |
|
|
139
|
+
| `AUTHORITATIVE_INDIVIDUAL_EXIT_READ_REQUIRED` | authoritative individual exit read required |
|
|
140
|
+
| `CARRY_FORWARD_BIRTH_DATE_CHANGED_SINCE_ENROLLMENT` | carry-forward birth date changed since enrollment |
|
|
141
|
+
| `CARRY_FORWARD_BIRTH_DATE_IS_EMPTY_OR_FOLLOWS_ASSESSMENT_DATE` | carry-forward birth date is empty or follows assessment date |
|
|
142
|
+
| `CARRY_FORWARD_FIELD_IS_EMPTY` | carry-forward field is empty |
|
|
143
|
+
| `CLIENT_ID_REQUIRED` | client ID required |
|
|
144
|
+
| `CLIENT_PROVIDER_AND_SUPPORTED_ENTRY_EXIT_TYPE_REQUIRED` | client, provider and supported Entry/Exit type required |
|
|
145
|
+
| `CONFLICTING_FIELD_COVERAGE` | conflicting field coverage |
|
|
146
|
+
| `CORRECTION_ANSWER_COULD_NOT_BE_ENCODED` | correction answer could not be encoded |
|
|
147
|
+
| `CORRECTION_KEY_CONFLICTS_WITH_PREVIOUS_INTENT` | correction key conflicts with previous intent |
|
|
148
|
+
| `CORRECTION_LOOKUP_COULD_NOT_BE_RESOLVED` | correction lookup could not be resolved |
|
|
149
|
+
| `CORRECTION_REQUIRES_AN_EXISTING_ENTRY_ROW_KEY_AND_MATCHING_PARENT` | correction requires an existing entry row key and matching parent |
|
|
150
|
+
| `CORRECTION_ROW_METADATA_INCOMPLETE` | correction row metadata incomplete |
|
|
151
|
+
| `CORRECTION_SCALAR_READ_MISSING` | correction scalar read missing |
|
|
152
|
+
| `CORRECTION_STATE_CHANGED_DURING_RESOLUTION` | correction state changed during resolution |
|
|
153
|
+
| `CORRECTION_STATE_DIFFERS_FROM_ITS_PRE_WRITE_SNAPSHOT_NON_APPLICATION_CANNOT_BE_RESOLVED` | correction state differs from its pre-write snapshot; non-application cannot be resolved |
|
|
154
|
+
| `CORRECTION_SUBASSESSMENT_READ_MISSING` | correction subassessment read missing |
|
|
155
|
+
| `CORRECTION_WAS_RESOLVED_AS_NOT_APPLIED_USE_A_NEW_KEY_FOR_A_CORRECTED_INTENT` | correction was resolved as not applied; use a new key for a corrected intent |
|
|
156
|
+
| `DESTINATION_AND_REASON_FOR_LEAVING_REQUIRED` | destination and reason for leaving required |
|
|
157
|
+
| `DOB_AND_HOUSEHOLD_HEAD_CORRECTIONS_REQUIRE_THEIR_DEDICATED_WORKFLOWS` | DOB and household head corrections require their dedicated workflows |
|
|
158
|
+
| `DOB_HELPER_REQUIRED_FOR_BIRTH_DATE_ANSWERS_AND_CARRY_FORWARD` | DOB helper required for birth-date answers and carry-forward |
|
|
159
|
+
| `DOB_QUESTION_TYPE_CHANGED` | DOB question type changed |
|
|
160
|
+
| `DUPLICATE_ADOPTION_TARGETS` | duplicate adoption targets |
|
|
161
|
+
| `DUPLICATE_CORRECTION_JOURNAL_KEYS` | duplicate correction journal keys |
|
|
162
|
+
| `DUPLICATE_HOUSEHOLD_JOURNAL_KEYS` | duplicate household journal keys |
|
|
163
|
+
| `DUPLICATE_HOUSEHOLD_MEMBER_ENROLLMENT` | duplicate household member enrollment |
|
|
164
|
+
| `DUPLICATE_HOUSEHOLD_REVIEW_IDS` | duplicate household review IDs |
|
|
165
|
+
| `DUPLICATE_JOURNAL_KEYS` | duplicate journal keys |
|
|
166
|
+
| `DUPLICATE_MATCHING_ENROLLMENTS` | duplicate matching enrollments |
|
|
167
|
+
| `DUPLICATE_MATCHING_SUBASSESSMENT_ROWS` | duplicate matching subassessment rows |
|
|
168
|
+
| `DUPLICATE_SEMANTIC_SUBASSESSMENT_ROWS` | duplicate semantic subassessment rows |
|
|
169
|
+
| `DUPLICATE_SUBASSESSMENT_KEY` | duplicate subassessment key |
|
|
170
|
+
| `EMPTY_ENTRY_CORRECTION` | empty entry correction |
|
|
171
|
+
| `EMPTY_ROW_CORRECTION` | empty row correction |
|
|
172
|
+
| `END_DATE_PRECEDES_RECORD_START` | end date precedes record start |
|
|
173
|
+
| `END_DATE_TARGET_INTERVAL_MISSING` | end-date target interval missing |
|
|
174
|
+
| `END_DATE_TARGET_MISSING` | end-date target missing |
|
|
175
|
+
| `ENROLLMENT_ALREADY_JOURNALED_USE_ITS_EXISTING_ENTRY_KEY` | enrollment already journaled; use its existing entry key |
|
|
176
|
+
| `ENROLLMENT_COUNT_UNAVAILABLE_OR_CHANGED` | enrollment count unavailable or changed |
|
|
177
|
+
| `ENROLLMENT_LIST_CONTEXT_MISMATCH` | enrollment list context mismatch |
|
|
178
|
+
| `ENROLLMENT_LIST_EXCEEDS_LIMIT` | enrollment list exceeds limit |
|
|
179
|
+
| `ENROLLMENT_NOT_GROUPED` | standalone enrollment uses adoptEnrollment |
|
|
180
|
+
| `ENROLLMENT_READ_FAILED` | enrollment read failed |
|
|
181
|
+
| `ENTRY_AND_INTERIM_ASSESSMENT_LOCATIONS_REQUIRED_EXIT_LOCATION_MUST_BE_NONEMPTY_WHEN_SUPPLIED` | entry and interim assessment locations required; exit location must be nonempty when supplied |
|
|
182
|
+
| `ENTRY_CHANGED_SINCE_CORRECTION_PREFLIGHT` | entry changed since correction preflight |
|
|
183
|
+
| `ENTRY_CORRECTION_DID_NOT_VERIFY` | entry correction did not verify |
|
|
184
|
+
| `ENTRY_CORRECTION_ENROLLMENT_CONTEXT_MISMATCH` | entry correction enrollment context mismatch |
|
|
185
|
+
| `ENTRY_CORRECTION_MUST_PRECEDE_INTERIM_OR_EXIT_WORKFLOWS` | entry correction must precede interim or exit workflows |
|
|
186
|
+
| `ENTRY_CORRECTION_REJECTED` | correction was resolved as not applied; use a new key for a corrected intent<br>entry correction rejected |
|
|
187
|
+
| `ENTRY_CORRECTION_REQUIRES_AN_OPEN_ENROLLMENT_WITHOUT_REVIEWS` | entry correction requires an open enrollment without reviews |
|
|
188
|
+
| `ENTRY_CORRECTION_UNCERTAIN` | entry correction uncertain<br>uncertain entry correction requires reconciliation |
|
|
189
|
+
| `EXISTING_EXIT_CONFLICTS_WITH_REQUESTED_EXIT` | existing exit conflicts with requested exit |
|
|
190
|
+
| `EXISTING_HOUSEHOLD_REVIEW_CONFLICT` | existing household review conflict |
|
|
191
|
+
| `EXIT_ASSESSMENT_CONFIGURATION_CHANGED` | exit assessment configuration changed |
|
|
192
|
+
| `EXIT_IDENTITY_CHANGED` | exit identity changed |
|
|
193
|
+
| `EXIT_IDENTITY_MISSING` | exit identity missing |
|
|
194
|
+
| `EXIT_PRECEDES_AN_EXISTING_REVIEW` | exit precedes an existing review |
|
|
195
|
+
| `EXIT_SUBGROUP_IDENTITY_CHANGED` | exit subgroup identity changed |
|
|
196
|
+
| `EXIT_SUBGROUP_MEMBERSHIP_MISMATCH` | exit subgroup membership mismatch |
|
|
197
|
+
| `EXIT_UI_AND_AUTHORITATIVE_ENROLLMENT_SESSION_REQUIRED` | exit UI and authoritative enrollment session required |
|
|
198
|
+
| `EXIT_UI_DESTINATION_AND_REASON_FOR_LEAVING_REQUIRED` | exit UI, destination and reason for leaving required |
|
|
199
|
+
| `EXPLICIT_ENROLLMENT_TARGETS_REQUIRED` | explicit enrollment targets required |
|
|
200
|
+
| `HEAD_AND_HOUSEHOLD_MEMBERS_REQUIRED` | head and household members required |
|
|
201
|
+
| `HEAD_CLIENT_ID_REQUIRED` | head client ID required |
|
|
202
|
+
| `HEAD_OF_HOUSEHOLD_ASSESSMENT_OPTION_UNRESOLVED` | head of household assessment option unresolved |
|
|
203
|
+
| `HEAD_OF_HOUSEHOLD_CHANGE_IS_A_RED_FLAG_USE_A_SEPARATE_RETROACTIVE_CORRECTION_FOR_A_MEMBER_WITH` | head of household change is a red flag; use a separate retroactive correction for a member with the same first entry date, or exit all enrollments and create a fresh enrollment |
|
|
204
|
+
| `HOUSEHOLD_API_AND_AUTHORITATIVE_ENROLLMENT_SESSION_REQUIRED` | household API and authoritative enrollment session required |
|
|
205
|
+
| `HOUSEHOLD_DISCOVERY_AMBIGUOUS` | a unique current household matching the full cohort, head and dates is required |
|
|
206
|
+
| `HOUSEHOLD_DISCOVERY_COHORT_CHANGED` | household membership changed during discovery<br>linked enrollment cohort changed during discovery<br>native household or enrollment cohort changed during discovery<br>native household or enrollment group changed during discovery |
|
|
207
|
+
| `HOUSEHOLD_DISCOVERY_DETAIL_INVALID` | complete linked enrollment cohort required<br>group flag conflicts with linked cohort |
|
|
208
|
+
| `HOUSEHOLD_DISCOVERY_INPUT_INVALID` | head client and enrollment IDs required |
|
|
209
|
+
| `HOUSEHOLD_DISCOVERY_LIST_INCOMPLETE` | household list count unavailable or changed<br>incomplete household list<br>invalid household list |
|
|
210
|
+
| `HOUSEHOLD_DISCOVERY_NATIVE_LINK_REQUIRED` | native household and enrollment group IDs required |
|
|
211
|
+
| `HOUSEHOLD_DISCOVERY_NOT_FOUND` | a unique current household matching the full cohort, head and dates is required |
|
|
212
|
+
| `HOUSEHOLD_DISCOVERY_READERS_REQUIRED` | authoritative enrollment reader required<br>legacy readers require complete household readers |
|
|
213
|
+
| `HOUSEHOLD_ENROLLMENT_CONTEXT_MISMATCH` | household enrollment context mismatch |
|
|
214
|
+
| `HOUSEHOLD_ENROLLMENT_IDENTITY_CHANGED` | household enrollment identity changed |
|
|
215
|
+
| `HOUSEHOLD_ENROLLMENT_MEMBERS_CHANGED` | household enrollment members changed |
|
|
216
|
+
| `HOUSEHOLD_EXITS_ARE_NOT_LINKED` | household exits are not linked |
|
|
217
|
+
| `HOUSEHOLD_HEAD_MISMATCH_HEAD_CHANGES_ARE_A_RED_FLAG_AND_REQUIRE_SEPARATE_RETROACTIVE` | household head mismatch; head changes are a red flag and require separate retroactive correction or a fresh enrollment |
|
|
218
|
+
| `HOUSEHOLD_KEY_CONFLICTS_WITH_PREVIOUS_INTENT` | household key conflicts with previous intent |
|
|
219
|
+
| `HOUSEHOLD_MEMBERS_UNAVAILABLE` | household members unavailable |
|
|
220
|
+
| `HOUSEHOLD_MEMBERSHIP_CHANGED_WHILE_ACQUIRING_LOCKS_RETRY` | household membership changed while acquiring locks; retry |
|
|
221
|
+
| `HOUSEHOLD_NATIVE_LINK_CHANGED` | household native link changed<br>native enrollment household or group changed |
|
|
222
|
+
| `HOUSEHOLD_ORIGINAL_HEAD_AND_COMPLETE_ENROLLMENT_COHORT_REQUIRED` | household, original head and complete enrollment cohort required |
|
|
223
|
+
| `HOUSEHOLD_PROGRAM_AND_SUPPORTED_ENTRY_EXIT_TYPE_REQUIRED` | household, program and supported Entry/Exit type required |
|
|
224
|
+
| `HOUSEHOLD_REVIEW_IDENTITY_CHANGED` | household review identity changed |
|
|
225
|
+
| `HOUSEHOLD_REVIEW_MEMBERSHIP_MISMATCH` | household review membership mismatch |
|
|
226
|
+
| `HOUSEHOLD_REVIEWS_UNAVAILABLE` | household reviews unavailable |
|
|
227
|
+
| `INCOMPLETE_ENROLLMENT_LIST` | incomplete enrollment list |
|
|
228
|
+
| `INCOMPLETE_EXISTING_EXIT_METADATA` | incomplete existing exit metadata |
|
|
229
|
+
| `INDEPENDENT_CONFIRMATION_REQUIRED` | independent non-application confirmation required |
|
|
230
|
+
| `INDIVIDUAL_ENROLLMENT_CONTEXT_MISMATCH` | individual enrollment context mismatch |
|
|
231
|
+
| `INDIVIDUAL_EXIT_MEMBERSHIP_MISMATCH` | individual exit membership mismatch |
|
|
232
|
+
| `INTERIM_DID_NOT_VERIFY` | interim did not verify |
|
|
233
|
+
| `INVALID_ASSESSMENT_PLAN` | invalid assessment plan |
|
|
234
|
+
| `INVALID_CORRECTION_CLIENT_OR_ROWS` | invalid correction client or rows |
|
|
235
|
+
| `INVALID_CORRECTION_DATE` | invalid correction date |
|
|
236
|
+
| `INVALID_CORRECTION_EFFECTIVE_DATE` | invalid correction effective date |
|
|
237
|
+
| `INVALID_ENROLLMENT_DATA_ADOPTED_MEMBERS_REQUIRE_MATCHING_PROGRAM_AND_HEAD_COVERAGE` | invalid enrollment data: adopted members require matching program and head coverage |
|
|
238
|
+
| `INVALID_ENROLLMENT_DATA_ENROLLMENT_HAS_NO_HEAD_OF_HOUSEHOLD` | invalid enrollment data: enrollment has no head of household |
|
|
239
|
+
| `INVALID_ENROLLMENT_DATA_HEAD_OF_HOUSEHOLD_RELATIONSHIP_IS_MISSING_OR_CLEARED` | invalid enrollment data: head of household relationship is missing or cleared |
|
|
240
|
+
| `INVALID_ENROLLMENT_DATA_HOUSEHOLD_HAS_NO_ACTIVE_HEAD_OF_HOUSEHOLD` | invalid enrollment data: household has no active head of household |
|
|
241
|
+
| `INVALID_ENROLLMENT_DATA_MEMBER_ENROLLMENT_WOULD_EXIST_WITHOUT_AN_ENROLLED_HEAD_OF_HOUSEHOLD` | invalid enrollment data: member enrollment would exist without an enrolled head of household; the original head must remain enrolled through every member exit; reconcile the data or exit all members and create a fresh enrollment |
|
|
242
|
+
| `INVALID_ENROLLMENT_LIST` | invalid enrollment list |
|
|
243
|
+
| `INVALID_HOUSEHOLD_ENROLLMENT_MEMBERS` | invalid household enrollment members |
|
|
244
|
+
| `INVALID_HOUSEHOLD_JOURNAL` | invalid household journal |
|
|
245
|
+
| `INVALID_OR_CONFLICTING_CORRECTION_ANSWERS_AND_CLEARS` | invalid or conflicting correction answers and clears |
|
|
246
|
+
| `INVALID_OR_DUPLICATE_END_DATE_TARGET` | invalid or duplicate end-date target |
|
|
247
|
+
| `INVALID_OR_REUSED_SUBASSESSMENT_ID` | invalid or reused subassessment ID |
|
|
248
|
+
| `INVALID_STORED_CORRECTION_DATE` | invalid stored correction date |
|
|
249
|
+
| `INVALID_STORED_CORRECTION_NUMBER` | invalid stored correction number |
|
|
250
|
+
| `INVALID_SUBASSESSMENT_PLAN` | invalid subassessment plan |
|
|
251
|
+
| `INVALID_WORKFLOW_JOURNAL` | invalid workflow journal |
|
|
252
|
+
| `KEY_CONFLICTS_WITH_PREVIOUS_INTENT` | key conflicts with previous intent |
|
|
253
|
+
| `LINKED_HOUSEHOLD_ENROLLMENT_REQUIRES_EXITHOUSEHOLD` | linked household enrollment requires exitHousehold |
|
|
254
|
+
| `LOOKUP_WRITE_LABEL_UNAVAILABLE` | lookup write label unavailable |
|
|
255
|
+
| `MANDATORY_CLEAR_CONFLICT` | mandatory clear conflict |
|
|
256
|
+
| `MANDATORY_CORRECTION_FIELD_COULD_NOT_BE_ENCODED` | mandatory correction field could not be encoded |
|
|
257
|
+
| `MANDATORY_CORRECTION_FIELD_REQUIRES_AN_EXPLICIT_ANSWER_OR_CLEAR` | mandatory correction field requires an explicit answer or clear |
|
|
258
|
+
| `MANDATORY_CORRECTION_LOOKUP_LABEL_UNAVAILABLE` | mandatory correction lookup label unavailable |
|
|
259
|
+
| `MATCHING_CORRECTION_JOURNAL_REQUIRED` | matching correction journal required |
|
|
260
|
+
| `MEMBER_ALREADY_HAS_AN_INDEPENDENT_MATCHING_ENROLLMENT` | member already has an independent matching enrollment |
|
|
261
|
+
| `MEMBER_CLIENT_AND_ASSESSMENT_REQUIRED` | member client and assessment required |
|
|
262
|
+
| `MEMBER_ENROLLMENT_HAS_NOT_BEEN_ATTACHED` | member enrollment has not been attached |
|
|
263
|
+
| `MEMBER_ENTRY_DATE_PORT_REQUIRED` | member entry date port required |
|
|
264
|
+
| `MEMBER_EXIT_IDENTITY_MISSING` | member exit identity missing |
|
|
265
|
+
| `MEMBER_IS_NOT_ACTIVE_IN_HOUSEHOLD_AT_ENTRY` | member is not active in household at entry |
|
|
266
|
+
| `MOBILE_ANSWER_EXISTS` | mobile answer exists<br>mobile scalar answer already exists at the effective date |
|
|
267
|
+
| `MOBILE_PREFLIGHT_UNAVAILABLE` | mobile preflight unavailable<br>mobile scalar preflight unavailable |
|
|
268
|
+
| `MULTIPLE_END_DATE_OVERRIDES` | multiple end-date overrides |
|
|
269
|
+
| `NEW_MEMBER_MUST_REMAIN_ENROLLED` | new member must remain enrolled |
|
|
270
|
+
| `NOT_APPLICABLE_EXPLANATION_REQUIRED` | not-applicable explanation required |
|
|
271
|
+
| `OPEN_HOUSEHOLD_ENROLLMENT_REQUIRED_TO_ADD_A_MEMBER` | open household enrollment required to add a member |
|
|
272
|
+
| `PARTIAL_EXISTING_HOUSEHOLD_EXIT_REQUIRES_RECONCILIATION` | partial existing household exit requires reconciliation |
|
|
273
|
+
| `PARTIAL_HOUSEHOLD_REVIEW_REQUIRES_RECONCILIATION` | partial household review requires reconciliation |
|
|
274
|
+
| `PICKLIST_CATALOG_CONTEXT_MISMATCH` | picklist catalog client mismatch<br>picklist catalog context mismatch |
|
|
275
|
+
| `PROGRAM_AND_SUPPORTED_ENTRY_EXIT_TYPE_REQUIRED` | program and supported Entry/Exit type required |
|
|
276
|
+
| `PROGRAM_ASSESSMENT_CONFIGURATION_CHANGED` | program assessment configuration changed |
|
|
277
|
+
| `PROGRAM_ASSESSMENT_DEFAULT_UNRESOLVED` | program assessment default unresolved |
|
|
278
|
+
| `PROVIDER_DISCOVERY_MISMATCH` | provider discovery mismatch |
|
|
279
|
+
| `RECORD_ALREADY_HAS_A_DIFFERENT_END_DATE` | record already has a different end date |
|
|
280
|
+
| `RESOLUTION_EVIDENCE_REFERENCE_CHANGED` | resolution evidence reference changed |
|
|
281
|
+
| `REST_API_DISCOVERY_ENROLLMENT_HELPERS_AND_DURABLE_STORE_REQUIRED` | REST API, discovery, enrollment helpers and durable store required |
|
|
282
|
+
| `REVIEW_FOLLOWS_PROJECT_EXIT` | review follows project exit |
|
|
283
|
+
| `REVIEW_PRECEDES_ENTRY` | review precedes entry |
|
|
284
|
+
| `ROW_CLOSURE_MUST_FALL_BETWEEN_ENTRY_AND_ASSESSMENT` | row closure must fall between entry and assessment |
|
|
285
|
+
| `ROW_REGISTRATION_BINDING_CONFLICT` | row key or recordset is already bound differently |
|
|
286
|
+
| `ROW_REGISTRATION_CONTEXT_MISMATCH` | enrollment identity changed during row registration |
|
|
287
|
+
| `ROW_REGISTRATION_ENTRY_REQUIRED` | verified enrollment workflow required |
|
|
288
|
+
| `ROW_REGISTRATION_INPUT_INVALID` | unique row keys, parents and positive recordset IDs required<br>valid registration key, entry key, client and explicit rows required |
|
|
289
|
+
| `ROW_REGISTRATION_INTENT_CONFLICT` | registration key conflicts with previous intent |
|
|
290
|
+
| `ROW_REGISTRATION_PARENT_INVALID` | subassessment parent not in entry definition |
|
|
291
|
+
| `ROW_REGISTRATION_READ_UNAVAILABLE` | complete row registration read required |
|
|
292
|
+
| `ROW_REGISTRATION_READER_REQUIRED` | authoritative enrollment reader required |
|
|
293
|
+
| `ROW_REGISTRATION_ROW_MISSING` | selected row is missing or ambiguous at the entry date or current read |
|
|
294
|
+
| `SAVED_ENROLLMENT_MISSING` | saved enrollment missing |
|
|
295
|
+
| `SAVED_ENROLLMENT_MISSING_OR_CHANGED` | saved enrollment missing or changed |
|
|
296
|
+
| `SESSION_INTERRUPTED` | session interrupted<br>upstream workflow operation failed |
|
|
297
|
+
| `STABLE_CORRECTION_KEY_REQUIRED` | stable correction key required |
|
|
298
|
+
| `STABLE_KEY_REQUIRED` | stable key required |
|
|
299
|
+
| `STANDARD_ASSESSMENT_WRITES_AND_AUTHORITATIVE_ENROLLMENT_READER_REQUIRED_FOR_CORRECTIONS` | standard assessment writes and authoritative enrollment reader required for corrections |
|
|
300
|
+
| `STORED_EXIT_DETAILS_CONFLICT_WITH_REQUESTED_EXIT` | stored exit details conflict with requested exit |
|
|
301
|
+
| `SUBASSESSMENT_READ_MISSING` | subassessment read missing |
|
|
302
|
+
| `TIMEZONE_QUALIFIED_WHOLE_SECOND_DATE_REQUIRED` | timezone-qualified whole-second date required |
|
|
303
|
+
| `UNCERTAIN_ENTRY_CORRECTION_REQUIRES_RECONCILIATION` | uncertain entry correction requires reconciliation |
|
|
304
|
+
| `UNCERTAIN_HOUSEHOLD_EXIT_REQUIRES_RECONCILIATION` | uncertain household exit requires reconciliation |
|
|
305
|
+
| `UNCERTAIN_MEMBER_ENTRY_DATE_REQUIRES_RECONCILIATION` | uncertain member entry date requires reconciliation |
|
|
306
|
+
| `UNCERTAIN_WRITE_REQUIRES_RECONCILIATION` | uncertain write requires reconciliation |
|
|
307
|
+
| `UNFINISHED_HOUSEHOLD_WORKFLOW_MUST_BE_RECONCILED_FIRST` | unfinished household workflow must be reconciled first |
|
|
308
|
+
| `UNIQUE_MEMBERS_INCLUDING_HEAD_REQUIRED` | unique members including head required |
|
|
309
|
+
| `UNKNOWN_CARRY_FORWARD_FIELD` | unknown carry-forward field |
|
|
310
|
+
| `UNKNOWN_OR_INVALID_CORRECTION_FIELDS` | unknown or invalid correction fields |
|
|
311
|
+
| `UNKNOWN_OR_INVALID_INPUT_FIELDS` | unknown or invalid input fields |
|
|
312
|
+
| `UNSELECTED_HOUSEHOLD_MEMBER_CHANGED` | unselected household member changed |
|
|
313
|
+
| `UNSELECTED_MEMBER_EXIT_IDENTITY_MISSING` | unselected member exit identity missing |
|
|
314
|
+
| `UNSUPPORTED_CORRECTION_FIELD_TYPE` | unsupported correction field type |
|
|
315
|
+
| `UNSUPPORTED_ENTRY_EXIT_TYPE_IN_ASSESSMENT_LOCATIONS` | unsupported Entry/Exit type in assessment locations |
|
|
316
|
+
| `UNSUPPORTED_MANDATORY_CORRECTION_FIELD` | unsupported mandatory correction field |
|
|
317
|
+
| `UNSUPPORTED_MANDATORY_FIELD_TYPE` | unsupported mandatory field type |
|
|
318
|
+
| `UNSUPPORTED_YES_NO_CLEAR` | unsupported yes no clear<br>yes/no answers cannot be cleared |
|
|
319
|
+
| `VERIFIED_CORRECTION_CANNOT_BE_REJECTED` | verified correction cannot be rejected |
|
|
320
|
+
| `VERIFIED_ENROLLMENT_WORKFLOW_REQUIRED` | verified enrollment workflow required |
|
|
321
|
+
| `VERIFIED_ENTRY_CORRECTION_CHANGED` | verified entry correction changed |
|
|
322
|
+
| `VERIFIED_EXIT_MISSING` | verified exit missing |
|
|
323
|
+
| `VERIFIED_HOUSEHOLD_ENTRY_REQUIRED` | verified household entry required |
|
|
324
|
+
| `VERIFIED_HOUSEHOLD_ENTRY_WITH_IDENTICAL_MEMBERS_REQUIRED_UNKNOWN_SELECTED_MEMBER` | verified household entry with identical members required; unknown selected member |
|
|
325
|
+
| `VERIFIED_ROW_CORRECTION_CHANGED` | verified row correction changed |
|
|
326
|
+
| `VERIFIED_STEP_CHANGED` | verified step changed |
|
|
327
|
+
| `WORKFLOW_CONFIGURATION_INVALID` | invalid workflow configuration<br>workflow configuration invalid |
|
|
328
|
+
| `WORKFLOW_FAILED` | Unclassified workflow failure; inspect cause and commit markers. |
|
|
329
|
+
| `WORKFLOW_JOURNAL_INVALID` | invalid workflow journal |
|
|
330
|
+
| `WORKFLOW_PENDING` | unfinished household workflow must be reconciled first<br>unfinished workflow must be reconciled first |
|
|
331
|
+
| `WORKFLOW_SCHEMA_CHANGED` | program assessment configuration changed |
|
|
332
|
+
| `WORKFLOW_UPSTREAM_FAILURE` | upstream workflow operation failed<br>workflow upstream failure |
|
|
333
|
+
| `WORKFLOW_VALIDATION_FAILED` | upstream workflow operation failed<br>workflow validation failed |
|
|
334
|
+
| `YES_NO_ANSWERS_CANNOT_BE_CLEARED` | yes/no answers cannot be cleared |
|
|
335
|
+
| `YES_NO_WRITE_MODE_CHANGED_SINCE_THIS_WORKFLOW_STARTED` | yes/no write mode changed since this workflow started |
|
|
336
|
+
| `YESNOWRITES_MUST_BE_MOBILE_OR_STANDARD_WITH_WRITELABEL_AND_ASSESSMENTS_CREATE` | yesNoWrites must be mobile, or standard with writeLabel and assessments.create |
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.1 - 2026-11-11
|
|
4
|
+
|
|
5
|
+
- Keep the pinned Chromium download and full browser checks on GitHub Ubuntu 24.04 runners while using their preinstalled runtime libraries. Avoid the redundant APT/font installation that blocked 0.9.0 public publication when the Ubuntu mirror failed DNS resolution. SDK runtime behavior is unchanged from 0.9.0.
|
|
6
|
+
|
|
7
|
+
## 0.9.0 - 2026-11-11
|
|
8
|
+
|
|
9
|
+
- Expose native `householdId` on managed GWT enrollment list/detail reads, separately from `groupId`.
|
|
10
|
+
- Discover and adopt linked enrollments from the head's IDs using the recorded household association, with cohort-wide consistency and original-head checks. Explicit household IDs must match the enrollment record.
|
|
11
|
+
- Use pinned native household/group identity for existing-group interims and exits, allowing changed current membership while preserving original enrollment-head coverage. Creation and additions still validate current membership.
|
|
12
|
+
- Preserve existing adoption keys and legacy custom-reader behavior; discovery reports whether evidence is `native-enrollment` or `verified-membership`.
|
|
13
|
+
- Verify native reads on existing training Coordinated Entry groups and read-only adoption of open/exited groups with no-write retries. No live enrollment or assessment records were changed.
|
|
14
|
+
|
|
15
|
+
## 0.8.1 - 2026-11-11
|
|
16
|
+
|
|
17
|
+
- Recheck mobile scalar collisions immediately before the first scalar-stage attempt, including answers that appeared after enrollment or review creation. Refuse known collisions and unavailable evidence without sending the batch or marking that stage attempted; preserve earlier lifecycle writes and prior-attempt reconciliation.
|
|
18
|
+
- Give complete-cohort adoption, standalone adoption of grouped entries, and changed verified steps specific stable codes instead of `WORKFLOW_FAILED`.
|
|
19
|
+
- Generate the complete enrollment workflow error-code reference in `API.md`; fail generation for new constant refusal reasons without codes, including conditional branches.
|
|
20
|
+
- Clarify that household discovery verifies current membership, not the original historical association, and explicit IDs do not bypass active-membership validation.
|
|
21
|
+
- Record the additional reported adoption/interim/correction acceptance from GitLab issues #5–#7, and distinguish the issue #2 report from the successful follow-up batch controls. The upstream partial-commit cause remains unresolved; no score-specific batch splitting is introduced.
|
|
22
|
+
|
|
3
23
|
## 0.8.0 - 2026-11-11
|
|
4
24
|
|
|
5
25
|
- Resolve full lookup write labels without existing answers using `enrollmentSession.readPicklistOptions` and `createCatalogWriteLabelResolver`.
|
|
@@ -110,10 +110,9 @@ export interface AdoptHouseholdEnrollmentInput {
|
|
|
110
110
|
members:Array<{clientId:number;enrollmentId:number}>;
|
|
111
111
|
}
|
|
112
112
|
export interface HouseholdEnrollmentDiscoveryInput {headClientId:number;headEnrollmentId:number;}
|
|
113
|
-
export
|
|
114
|
-
householdId:number;
|
|
115
|
-
|
|
116
|
-
}
|
|
113
|
+
export type HouseholdEnrollmentDiscoveryResult = HouseholdEnrollmentDiscoveryInput & {
|
|
114
|
+
householdId:number;providerId:number;type:string;members:Array<{clientId:number;enrollmentId:number}>;
|
|
115
|
+
} & ({householdSource:'native-enrollment';groupId:number|string}|{householdSource:'verified-membership';groupId?:never});
|
|
117
116
|
export interface AdoptHouseholdEnrollmentFromHeadInput extends HouseholdEnrollmentDiscoveryInput {key:string;}
|
|
118
117
|
export interface RegisterEntryRowsInput {
|
|
119
118
|
key:string;enrollmentKey:string;clientId:number;
|
|
@@ -220,6 +219,8 @@ export interface HouseholdExitInput extends HouseholdExitDetailInput {
|
|
|
220
219
|
members:HouseholdMemberPlan[];
|
|
221
220
|
}
|
|
222
221
|
export interface HouseholdEnrollmentResult {
|
|
222
|
+
groupId?:number|string;
|
|
223
|
+
householdSource?:'native-enrollment';
|
|
223
224
|
key:string;
|
|
224
225
|
outcome:'verified'|'already-present';
|
|
225
226
|
householdId:number;
|
|
@@ -229,6 +230,8 @@ export interface HouseholdEnrollmentResult {
|
|
|
229
230
|
export interface HouseholdInterimResult extends Omit<HouseholdEnrollmentResult,'members'> {members:InterimWorkflowResult[];}
|
|
230
231
|
export interface HouseholdExitResult extends Omit<HouseholdEnrollmentResult,'members'> {members:ExitWorkflowResult[];}
|
|
231
232
|
export interface HouseholdWorkflowStatus {
|
|
233
|
+
groupId?:number|string;
|
|
234
|
+
householdSource?:'native-enrollment';
|
|
232
235
|
key:string;
|
|
233
236
|
kind:'entry'|'addition'|'interim'|'exit';
|
|
234
237
|
stage:'pending'|'verified';
|
|
@@ -246,7 +249,7 @@ export interface EnrollmentWorkflowOptions {
|
|
|
246
249
|
store:UiMutationStore;
|
|
247
250
|
/** Required for SVPPROFDOB answers or carry-forward. Share this workflow's durable store. */
|
|
248
251
|
birthDates?:DateOfBirth;
|
|
249
|
-
/** Required for exits
|
|
252
|
+
/** Required for exits and household operations. Household creation/additions also need api.households and helpers.addHouseholdMembers; native-linked adoption/reviews/exits do not. */
|
|
250
253
|
enrollmentSession?:EnrollmentReader;
|
|
251
254
|
/** Required for exitEnrollment and exitHousehold; the workflow owns its durable write journal. */
|
|
252
255
|
exitUi?:HouseholdExitUi;
|
|
@@ -10,6 +10,7 @@ import {readProgramCatalog,flattenQuestions,createCatalogWriteLabelResolver,enco
|
|
|
10
10
|
|
|
11
11
|
const object=v=>v!==null&&typeof v==='object'&&!Array.isArray(v);
|
|
12
12
|
const positive=v=>Number.isSafeInteger(v)&&v>0;
|
|
13
|
+
const validGroupId=v=>positive(v)||typeof v==='string'&&/^[a-zA-Z0-9_-]+$/.test(v);
|
|
13
14
|
const entryExitTypes=['HUD','Basic','Standard','RHY','VA','Basic Center Program Entry/Exit','PATH','Quick Call','Transitional Living Program Entry/Exit'];
|
|
14
15
|
const canonical=v=>JSON.stringify(v,(_,x)=>object(x)?Object.fromEntries(Object.keys(x).sort().map(k=>[k,x[k]])):x);
|
|
15
16
|
const hash=v=>createHash('sha256').update(canonical(v)).digest('hex');
|
|
@@ -251,20 +252,20 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
251
252
|
// assessment's own rows. Read at the later of now and the assessment instant.
|
|
252
253
|
const readCurrent=async()=>questions(await api.assessments.get({path:{clientId:input.clientId,assessmentCode:code},query:{dateEffective:new Date(Math.max(Date.now(),Date.parse(at))).toISOString().slice(0,19)+'Z'}}));
|
|
253
254
|
// A generic read at exactly the effective instant returns null for yes/no answers saved at
|
|
254
|
-
// that instant; one second later returns them.
|
|
255
|
+
// that instant; one second later returns them. Scalar preflight and verification use this.
|
|
255
256
|
const readSaved=async()=>questions(await api.assessments.get({path:{clientId:input.clientId,assessmentCode:code},query:{dateEffective:new Date(Date.parse(at)+1000).toISOString()}}));
|
|
256
|
-
const
|
|
257
|
-
if(!record&&plan.scalar.length){
|
|
258
|
-
const saved=await readSaved();
|
|
257
|
+
const assertScalarInsertable=saved=>{
|
|
259
258
|
const existing=plan.scalar.map(q=>({q,row:saved.find(r=>r.virt_field_name===q.virt_field_name)}));
|
|
260
|
-
const complete=existing.every(({q,row})=>row&&sameAnswer(q,row.answer_value)&&restDate(row.date_effective)===restDate(at));
|
|
259
|
+
const complete=existing.every(({q,row})=>row&&row.question_type===q.question_type&&sameAnswer(q,row.answer_value)&&restDate(row.date_effective)===restDate(at));
|
|
261
260
|
if(!complete){
|
|
262
261
|
const unavailable=existing.filter(({q,row})=>!row||row.question_type!==q.question_type||row.date_effective!=null&&(!Number.isFinite(restDate(row.date_effective))||restDate(row.date_effective)>restDate(at))||row.date_effective==null&&row.answer_value!=null&&row.answer_value!=='').map(({q})=>q.virt_field_name);
|
|
263
|
-
if(unavailable.length)fail('mobile scalar preflight unavailable',{code:'MOBILE_PREFLIGHT_UNAVAILABLE',fields:unavailable});
|
|
262
|
+
if(unavailable.length)fail('mobile scalar preflight unavailable',{code:'MOBILE_PREFLIGHT_UNAVAILABLE',step:'scalars',fields:unavailable});
|
|
264
263
|
const collisions=existing.filter(({row})=>restDate(row.date_effective)===restDate(at)).map(({q})=>q.virt_field_name);
|
|
265
|
-
if(collisions.length)fail('mobile scalar answer already exists at the effective date',{code:'MOBILE_ANSWER_EXISTS',fields:collisions});
|
|
264
|
+
if(collisions.length)fail('mobile scalar answer already exists at the effective date',{code:'MOBILE_ANSWER_EXISTS',step:'scalars',fields:collisions});
|
|
266
265
|
}
|
|
267
|
-
}
|
|
266
|
+
};
|
|
267
|
+
const initial=await read();
|
|
268
|
+
if(!record&&plan.scalar.length)assertScalarInsertable(await readSaved());
|
|
268
269
|
if(expectedHead===input.clientId&&headQuestion)checkHeadAnswer(proposedHead?.answer_value??initial.find(q=>q.virt_field_name==='HUD_RELATIONTOHOH')?.answer_value);
|
|
269
270
|
for(const field of plan.carry.filter(field=>field!=='SVPPROFDOB')){if(kind==='entry'&&record?.correctedScalars?.[field])continue;const q=initial.find(q=>q.virt_field_name===field);if(!q||(q.question_type==='sub_assessment'?!q.sub_assessment_answer_list?.length:!q.answer_value))fail('carry-forward field is empty');}
|
|
270
271
|
// Resend mandatory answers with the standard yes/no write. A blank write must not hide a recorded value.
|
|
@@ -370,7 +371,13 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
370
371
|
},()=>api.subassessments.setEndDate({clientId:input.clientId,assessmentCode:code,effectiveDate:at,parentField:end.parentField,recordsetId:end.recordsetId,endDateField:end.endDateField,endDate:end.endDate}));
|
|
371
372
|
}
|
|
372
373
|
if(plan.scalar.length)await step('scalars',async()=>{
|
|
373
|
-
const source=
|
|
374
|
+
const source=await readSaved();
|
|
375
|
+
const found=plan.scalar.every(original=>{const q=kind==='entry'?record.correctedScalars?.[original.virt_field_name]??original:original,r=source.find(r=>r.virt_field_name===q.virt_field_name);return r&&r.question_type===q.question_type&&sameAnswer(q,r.answer_value)&&(q.answer_value===''||restDate(r.date_effective)===restDate(at));});
|
|
376
|
+
// Enrollment/review creation or another writer can populate shared questions after
|
|
377
|
+
// initial preflight. Refuse a known collision before journaling this first attempt.
|
|
378
|
+
// Prior attempts remain reconciliation-only, even when only part of a batch saved.
|
|
379
|
+
if(!found&&!record.steps.scalars)assertScalarInsertable(source);
|
|
380
|
+
return {found,value:plan.scalar.map(q=>q.virt_field_name)};
|
|
374
381
|
},()=>api.assessmentsMobile.addAnswer({path:{clientId:input.clientId,assessmentCode:code},query:{dateEffective:at},body:plan.scalar}));
|
|
375
382
|
// Standard writes update existing dated rows and change only the answers sent.
|
|
376
383
|
if(plan.yesNo.length){const body=[...plan.yesNo,...mandatory.map(({expected,...q})=>q)],expected=[...plan.yesNo,...mandatory.map(({expected,...q})=>({...q,answer_value:expected}))];
|
|
@@ -427,7 +434,7 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
427
434
|
for(const target of targets){fields(target,['clientId','enrollmentId']);if(!positive(target.clientId)||!positive(target.enrollmentId))fail('explicit enrollment targets required');}
|
|
428
435
|
targets.sort((a,b)=>a.clientId-b.clientId);const ids=targets.map(t=>t.clientId);
|
|
429
436
|
if(new Set(ids).size!==ids.length||new Set(targets.map(t=>t.enrollmentId)).size!==targets.length)fail('duplicate adoption targets');
|
|
430
|
-
if(household&&(!positive(input.householdId)||!positive(input.headClientId)||!ids.includes(input.headClientId)
|
|
437
|
+
if(household&&(!positive(input.householdId)||!positive(input.headClientId)||!ids.includes(input.headClientId)))fail('household, original head and complete enrollment cohort required');
|
|
431
438
|
const owner=hash({adoption:true,input}),intentHash=hash(input);
|
|
432
439
|
return withClients(ids,async()=>{
|
|
433
440
|
const locked=household?work=>store.withLock(householdScope(input.headClientId),work):work=>work();
|
|
@@ -460,7 +467,13 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
460
467
|
}
|
|
461
468
|
const head=snapshots.find(r=>r.target.clientId===(household?input.headClientId:input.clientId));
|
|
462
469
|
if(snapshots.some(r=>r.s.providerId!==head.s.providerId||r.s.enrollmentType!==head.s.enrollmentType||r.start<head.start||r.end>head.end))fail('invalid enrollment data: adopted members require matching program and head coverage');
|
|
463
|
-
|
|
470
|
+
const native=household&&snapshots.some(r=>r.s.householdId!==undefined);
|
|
471
|
+
if(native){
|
|
472
|
+
if(snapshots.some(r=>r.s.householdId!==input.householdId))fail('adoption household differs from enrollment association',{code:'ADOPTION_ENROLLMENT_HOUSEHOLD_MISMATCH'});
|
|
473
|
+
if(!validGroupId(head.s.groupId)||snapshots.some(r=>r.s.groupId!==head.s.groupId||r.s.grouped===false)||group?.context.groupId!==undefined&&group.context.groupId!==head.s.groupId)fail('adoption enrollment group changed',{code:'ADOPTION_ENROLLMENT_GROUP_MISMATCH'});
|
|
474
|
+
}
|
|
475
|
+
if(household&&!native){
|
|
476
|
+
if(typeof api.households?.get!=='function')fail('household, original head and complete enrollment cohort required');
|
|
464
477
|
const found=toItems(await api.households.get({path:{householdId:input.householdId}}));
|
|
465
478
|
if(found.length!==1||found[0].householdId!==input.householdId||found[0].isActive===false||!Array.isArray(found[0].clients))fail('active household required for adoption');
|
|
466
479
|
const members=found[0].clients,heads=members.filter(m=>m.headofhouseholdStatus&&m.isActive!==false&&!m.dateLeft);
|
|
@@ -472,10 +485,11 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
472
485
|
const context={clientId:r.target.clientId,providerId:r.s.providerId,type:r.s.enrollmentType,entryDate:new Date(r.start).toISOString()};
|
|
473
486
|
const discovered=await discover(context),schemaHash=hash({providerId:discovered.provider.providerId,entry:{assessment:discovered.entry.assessment,questions:discovered.entry.definition.questions},interim:{assessment:discovered.interim.assessment,questions:discovered.interim.definition.questions}});
|
|
474
487
|
const headQuestion=discovered.entry.definition.questions.find(q=>q.fieldName==='HUD_RELATIONTOHOH');
|
|
488
|
+
if(native&&!headQuestion&&group?.stage!=='verified')fail('native adoption requires original entry head relationship evidence',{code:'ADOPTION_HEAD_EVIDENCE_REQUIRED'});
|
|
475
489
|
if(headQuestion){
|
|
476
490
|
const self=headQuestion.options.filter(o=>o.code==='RELATIONHOH_SELF'),answers=questions(await api.assessments.get({path:{clientId:r.target.clientId,assessmentCode:discovered.entry.assessment.code},query:{dateEffective:context.entryDate}}));
|
|
477
491
|
const answer=answers.find(q=>q.virt_field_name==='HUD_RELATIONTOHOH')?.answer_value;
|
|
478
|
-
if(self.length!==1||answer===null||answer===undefined||answer===''||(String(answer)===String(self[0].id))!==(r.target.clientId===head.target.clientId))fail('adoption requires the original enrollment head relationship');
|
|
492
|
+
if(self.length!==1||answer===null||answer===undefined||answer===''||!headQuestion.options.some(o=>String(o.id)===String(answer))||(String(answer)===String(self[0].id))!==(r.target.clientId===head.target.clientId))fail('adoption requires the original enrollment head relationship');
|
|
479
493
|
}
|
|
480
494
|
const existing=prior.get(r.target.clientId);
|
|
481
495
|
if(existing&&(existing.enrollmentId!==r.target.enrollmentId||canonical(existing.context)!==canonical(context)||existing.schemaHash!==schemaHash))fail('adopted enrollment identity or configuration changed');
|
|
@@ -485,17 +499,18 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
485
499
|
const results=prepared.map(r=>({key:r.key,outcome:wasVerified?'already-present':'verified',clientId:r.context.clientId,enrollmentId:r.enrollmentId,assessmentCode:r.assessmentCode,effectiveDate:r.context.entryDate,recordsetIds:structuredClone(r.rows)}));
|
|
486
500
|
if(household){
|
|
487
501
|
if(!group){group={version:1,key:input.key,kind:'entry',adopted:true,intentHash,context:{householdId:input.householdId,headClientId:input.headClientId,providerId:head.s.providerId,type:head.s.enrollmentType,entryDate:new Date(head.start).toISOString()},clientIds:ids,stage:'pending',members:{},attached:true};groups.push(group);}
|
|
502
|
+
if(native){group.context.groupId=head.s.groupId;group.context.householdSource='native-enrollment';}
|
|
488
503
|
await store.write(householdScope(input.headClientId),structuredClone(groups));
|
|
489
504
|
for(const clientId of ids)await store.withLock(reservationScope(clientId),()=>store.write(reservationScope(clientId),[{owner,stage:'pending'}]));
|
|
490
505
|
}
|
|
491
506
|
for(const r of prepared){if(!prior.get(r.context.clientId))journals.get(r.context.clientId).push(r);await store.write(scope(r.context.clientId),structuredClone(journals.get(r.context.clientId)));}
|
|
492
|
-
if(household){group.members=Object.fromEntries(results.map(r=>[r.clientId,r]));group.stage='verified';await store.write(householdScope(input.headClientId),structuredClone(groups));for(const clientId of ids)await store.withLock(reservationScope(clientId),()=>store.write(reservationScope(clientId),[{owner,stage:'verified'}]));return {key:input.key,outcome:wasVerified?'already-present':'verified',householdId:input.householdId,headClientId:input.headClientId,members:results};}
|
|
507
|
+
if(household){group.members=Object.fromEntries(results.map(r=>[r.clientId,r]));group.stage='verified';await store.write(householdScope(input.headClientId),structuredClone(groups));for(const clientId of ids)await store.withLock(reservationScope(clientId),()=>store.write(reservationScope(clientId),[{owner,stage:'verified'}]));return {key:input.key,outcome:wasVerified?'already-present':'verified',householdId:input.householdId,...(native?{groupId:head.s.groupId,householdSource:'native-enrollment'}:{}),headClientId:input.headClientId,members:results};}
|
|
493
508
|
return results[0];
|
|
494
509
|
});
|
|
495
510
|
});
|
|
496
511
|
};
|
|
497
512
|
const householdRun=async(kind,raw,operation={attempted:false})=>{
|
|
498
|
-
if(typeof enrollmentSession?.readEnrollment!=='function'||typeof helpers.addHouseholdMembers!=='function'||typeof api.households?.get!=='function')fail('household API and authoritative enrollment session required');
|
|
513
|
+
if(typeof enrollmentSession?.readEnrollment!=='function'||['entry','addition'].includes(kind)&&(typeof helpers.addHouseholdMembers!=='function'||typeof api.households?.get!=='function'))fail('household API and authoritative enrollment session required');
|
|
499
514
|
fields(raw,kind==='addition'?['key','enrollmentKey','headClientId','entryDate','members']:kind==='entry'?['key','householdId','headClientId','providerId','type','entryDate','members']:['key','enrollmentKey','headClientId',kind==='exit'?'exitDate':'reviewDate','members',...(kind==='exit'?['destination','reasonForLeaving','destinationDetails','reasonForLeavingOther']:[])]);
|
|
500
515
|
const input=structuredClone(raw);key(input.key);
|
|
501
516
|
if(kind==='exit'&&(typeof exitUi?.saveExit!=='function'||typeof exitUi?.validateExit!=='function'||typeof input.destination!=='string'||!input.destination.trim()||typeof input.reasonForLeaving!=='string'||!input.reasonForLeaving.trim()))fail('exit UI, destination and reason for leaving required');
|
|
@@ -537,14 +552,25 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
537
552
|
const cohortIds=[...new Set([...Object.keys(catalog).map(Number),...ids])].sort((a,b)=>a-b);
|
|
538
553
|
const context=kind==='entry'?{householdId:input.householdId,headClientId:input.headClientId,providerId:input.providerId,type:input.type,entryDate:input.entryDate}:kind==='addition'?{...entry.context,entryDate:input.entryDate}:entry.context;
|
|
539
554
|
if(!['entry','addition'].includes(kind)&&ids.some(id=>restDate(input.exitDate??input.reviewDate)<restDate(catalog[id].entryDate)))fail('review precedes entry');
|
|
540
|
-
|
|
541
|
-
if(
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
555
|
+
let nativeGroupId;
|
|
556
|
+
if(!['entry','addition'].includes(kind)){
|
|
557
|
+
const anchor=await enrollmentSession.readEnrollment({clientId:input.headClientId,enrollmentId:catalog[input.headClientId]?.enrollmentId});
|
|
558
|
+
if(anchor?.householdId!==undefined||entry.context.groupId!==undefined||record?.context.groupId!==undefined){
|
|
559
|
+
if(!positive(anchor?.householdId)||anchor.householdId!==context.householdId||!validGroupId(anchor?.groupId)||anchor.grouped===false||entry.context.groupId!==undefined&&entry.context.groupId!==anchor.groupId||record?.context.groupId!==undefined&&record.context.groupId!==anchor.groupId)fail('native enrollment household or group changed',{code:'HOUSEHOLD_NATIVE_LINK_CHANGED'});
|
|
560
|
+
nativeGroupId=anchor.groupId;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if(nativeGroupId===undefined){
|
|
564
|
+
if(typeof api.households?.get!=='function')fail('household API and authoritative enrollment session required');
|
|
565
|
+
const household=toItems(await api.households.get({path:{householdId:context.householdId}}));
|
|
566
|
+
if(household.length!==1||household[0].householdId!==context.householdId||household[0].isActive===false||!Array.isArray(household[0].clients))fail('active household required');
|
|
567
|
+
const people=household[0].clients;
|
|
568
|
+
if(new Set(people.map(p=>p.clientId)).size!==people.length)fail('ambiguous household membership');
|
|
569
|
+
const heads=people.filter(p=>p.headofhouseholdStatus&&p.isActive!==false&&!p.dateLeft);
|
|
570
|
+
if(heads.length===0)fail('invalid enrollment data: household has no active head of household');
|
|
571
|
+
if(heads.length!==1||heads[0].clientId!==input.headClientId)fail('household head mismatch; head changes are a red flag and require separate retroactive correction or a fresh enrollment');
|
|
572
|
+
for(const clientId of ids){const p=people.find(p=>p.clientId===clientId);if(!p||p.isActive===false||p.dateLeft||!Number.isFinite(restDate(p.dateJoined))||restDate(p.dateJoined)>restDate(catalog[clientId]?.entryDate??context.entryDate))fail('member is not active in household at entry');}
|
|
573
|
+
}
|
|
548
574
|
for(const clientId of cohortIds){const reservations=await store.withLock(reservationScope(clientId),()=>store.read(reservationScope(clientId)));if(!Array.isArray(reservations)||reservations.some(r=>r.stage!=='verified'&&r.owner!==owner))fail('unfinished household workflow must be reconciled first');}
|
|
549
575
|
const validateSnapshot=(snapshot,target,{partial=false}={})=>{
|
|
550
576
|
if(snapshot?.source!=='gwt-rpc'||snapshot.tenant!==api.tenant||snapshot.clientId!==target.clientId||snapshot.enrollmentId!==target.enrollmentId||snapshot.providerId!==context.providerId||snapshot.enrollmentType!==context.type||restDate(snapshot.projectStartDate)!==restDate(catalog[target.clientId]?.entryDate??context.entryDate))fail('household enrollment context mismatch');
|
|
@@ -553,7 +579,9 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
553
579
|
if(!Array.isArray(members)||members.some(m=>!positive(m.clientId)||!positive(m.enrollmentId))||new Set(members.map(m=>m.clientId)).size!==members.length||new Set(members.map(m=>m.enrollmentId)).size!==members.length||!members.some(m=>m.clientId===target.clientId&&m.enrollmentId===target.enrollmentId))fail('invalid household enrollment members');
|
|
554
580
|
const actual=members.map(m=>m.clientId).sort((a,b)=>a-b);
|
|
555
581
|
if(partial?actual.some(id=>!cohortIds.includes(id)):canonical(actual)!==canonical(cohortIds))fail('household enrollment members changed');
|
|
556
|
-
|
|
582
|
+
const pinnedGroup=nativeGroupId??context.groupId;
|
|
583
|
+
if(pinnedGroup!==undefined){if(snapshot.householdId!==context.householdId||snapshot.groupId!==pinnedGroup||snapshot.grouped===false)fail('native enrollment household or group changed',{code:'HOUSEHOLD_NATIVE_LINK_CHANGED'});}
|
|
584
|
+
if(nativeGroupId===undefined&&(!Array.isArray(snapshot.householdClientIds)||cohortIds.some(id=>!snapshot.householdClientIds.includes(id))))fail('household members unavailable');
|
|
557
585
|
for(const m of members){const known=record?.members[m.clientId]?.enrollmentId??catalog[m.clientId]?.enrollmentId;if(known&&known!==m.enrollmentId)fail('household enrollment identity changed');}
|
|
558
586
|
return snapshot;
|
|
559
587
|
};
|
|
@@ -583,6 +611,7 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
583
611
|
const preflight=[];for(const member of input.members)preflight.push(await run(kind==='addition'?'entry':kind,toMember(member),{locked:true,owner,operation,headClientId:input.headClientId,preflight:true,...(kind==='addition'?{pinSchema:true}:{})}));
|
|
584
612
|
if(kind==='addition'&&record?.entrySchemas&&canonical(record.entrySchemas)!==canonical(preflight))fail('additional member assessment configuration changed');
|
|
585
613
|
if(kind==='exit'&&record?.exitSchemas&&canonical(record.exitSchemas)!==canonical(preflight))fail('exit assessment configuration changed');
|
|
614
|
+
if(nativeGroupId!==undefined){context.groupId=nativeGroupId;context.householdSource='native-enrollment';entry.context.groupId=nativeGroupId;entry.context.householdSource='native-enrollment';if(record){record.context.groupId=nativeGroupId;record.context.householdSource='native-enrollment';}}
|
|
586
615
|
const wasVerified=record?.stage==='verified';
|
|
587
616
|
if(!record){record={version:1,key:input.key,kind,intentHash,context,...(input.enrollmentKey?{enrollmentKey:input.enrollmentKey}:{}),clientIds:ids,stage:'pending',members:{},...(kind==='exit'?{exitSchemas:preflight}:kind==='addition'?{entrySchemas:preflight}:{})};records.push(record);}
|
|
588
617
|
const persist=()=>store.write(householdScope(input.headClientId),structuredClone(records));await persist();
|
|
@@ -742,7 +771,7 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
742
771
|
if(kind==='exit'||kind==='addition')await preserve();
|
|
743
772
|
await validateHeadCoverage();
|
|
744
773
|
record.stage='verified';await persist();for(const clientId of cohortIds)await store.withLock(reservationScope(clientId),()=>store.write(reservationScope(clientId),[{owner,stage:'verified'}]));
|
|
745
|
-
return {key:input.key,outcome:wasVerified?'already-present':'verified',householdId:context.householdId,headClientId:input.headClientId,members:ids.map(id=>structuredClone(record.members[id]))};
|
|
774
|
+
return {key:input.key,outcome:wasVerified?'already-present':'verified',householdId:context.householdId,...(context.groupId!==undefined?{groupId:context.groupId,householdSource:context.householdSource}:{}),headClientId:input.headClientId,members:ids.map(id=>structuredClone(record.members[id]))};
|
|
746
775
|
}));
|
|
747
776
|
};
|
|
748
777
|
return {
|
|
@@ -761,7 +790,7 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
761
790
|
const records=await store.read(householdScope(input.headClientId));if(!Array.isArray(records))fail('invalid household journal');
|
|
762
791
|
const matches=records.filter(r=>r.key===input.key);if(matches.length>1)fail('duplicate household journal keys');const record=matches[0];if(!record)return null;
|
|
763
792
|
if(record.version!==1||!object(record.members))fail('invalid household journal');
|
|
764
|
-
return {key:record.key,kind:record.kind,stage:record.stage,householdId:record.context.householdId,headClientId:input.headClientId,members:record.clientIds.map(clientId=>({clientId,key:memberKey(input.key,clientId),...structuredClone(record.members[clientId]??{})}))};
|
|
793
|
+
return {key:record.key,kind:record.kind,stage:record.stage,householdId:record.context.householdId,...(record.context.groupId!==undefined?{groupId:record.context.groupId,householdSource:record.context.householdSource}:{}),headClientId:input.headClientId,members:record.clientIds.map(clientId=>({clientId,key:memberKey(input.key,clientId),...structuredClone(record.members[clientId]??{})}))};
|
|
765
794
|
});
|
|
766
795
|
});
|
|
767
796
|
},
|
|
@@ -3,24 +3,33 @@ import {parseRestInstant,toItems} from '../dist/esm/index.js';
|
|
|
3
3
|
const positive=v=>Number.isSafeInteger(v)&&v>0;
|
|
4
4
|
const pairs=members=>JSON.stringify(members.map(({clientId,enrollmentId})=>({clientId,enrollmentId})).sort((a,b)=>a.clientId-b.clientId));
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/** Read the native household association; older custom readers retain explicitly labeled membership matching. */
|
|
7
7
|
export function createHouseholdEnrollmentDiscovery({api,enrollmentSession,makeError}){
|
|
8
8
|
const fail=(code,reason)=>{throw makeError(reason,{code,committed:false});};
|
|
9
9
|
return async input=>{
|
|
10
10
|
if(!input||Object.keys(input).some(k=>!['headClientId','headEnrollmentId'].includes(k))||!positive(input.headClientId)||!positive(input.headEnrollmentId))fail('HOUSEHOLD_DISCOVERY_INPUT_INVALID','head client and enrollment IDs required');
|
|
11
|
-
if(typeof enrollmentSession?.readEnrollment!=='function'
|
|
11
|
+
if(typeof enrollmentSession?.readEnrollment!=='function')fail('HOUSEHOLD_DISCOVERY_READERS_REQUIRED','authoritative enrollment reader required');
|
|
12
12
|
const target={clientId:input.headClientId,enrollmentId:input.headEnrollmentId};
|
|
13
13
|
const anchor=await enrollmentSession.readEnrollment(target);
|
|
14
14
|
const valid=(s,t)=>s?.source==='gwt-rpc'&&s.tenant===api.tenant&&s.clientId===t.clientId&&s.enrollmentId===t.enrollmentId&&positive(s.providerId)&&typeof s.enrollmentType==='string'&&Number.isFinite(parseRestInstant(s.projectStartDate));
|
|
15
15
|
if(!valid(anchor,target)||!Array.isArray(anchor.members)||!anchor.members.length||anchor.members.some(m=>!positive(m?.clientId)||!positive(m?.enrollmentId))||new Set(anchor.members.map(m=>m.clientId)).size!==anchor.members.length||new Set(anchor.members.map(m=>m.enrollmentId)).size!==anchor.members.length||!anchor.members.some(m=>m.clientId===target.clientId&&m.enrollmentId===target.enrollmentId))fail('HOUSEHOLD_DISCOVERY_DETAIL_INVALID','complete linked enrollment cohort required');
|
|
16
16
|
if(anchor.members.length===1&&anchor.grouped!==true)fail('ENROLLMENT_NOT_GROUPED','standalone enrollment uses adoptEnrollment');
|
|
17
17
|
if(anchor.grouped===false&&anchor.members.length>1)fail('HOUSEHOLD_DISCOVERY_DETAIL_INVALID','group flag conflicts with linked cohort');
|
|
18
|
+
const native=anchor.householdId!==undefined,validGroup=v=>Number.isSafeInteger(v)&&v>0||typeof v==='string'&&/^[a-zA-Z0-9_-]+$/.test(v);
|
|
19
|
+
if(native&&(!positive(anchor.householdId)||!validGroup(anchor.groupId)))fail('HOUSEHOLD_DISCOVERY_NATIVE_LINK_REQUIRED','native household and enrollment group IDs required');
|
|
18
20
|
const members=anchor.members.map(({clientId,enrollmentId})=>({clientId,enrollmentId})).sort((a,b)=>a.clientId-b.clientId),starts=new Map();
|
|
19
21
|
for(const member of members){
|
|
20
22
|
const s=member.clientId===target.clientId?anchor:await enrollmentSession.readEnrollment(member);
|
|
21
23
|
if(!valid(s,member)||s.providerId!==anchor.providerId||s.enrollmentType!==anchor.enrollmentType||!Array.isArray(s.members)||pairs(s.members)!==pairs(members))fail('HOUSEHOLD_DISCOVERY_COHORT_CHANGED','linked enrollment cohort changed during discovery');
|
|
24
|
+
if(native&&(s.householdId!==anchor.householdId||s.groupId!==anchor.groupId||s.grouped===false))fail('HOUSEHOLD_DISCOVERY_COHORT_CHANGED','native household or enrollment group changed during discovery');
|
|
22
25
|
starts.set(member.clientId,parseRestInstant(s.projectStartDate));
|
|
23
26
|
}
|
|
27
|
+
if(native){
|
|
28
|
+
const fresh=await enrollmentSession.readEnrollment(target);
|
|
29
|
+
if(!valid(fresh,target)||fresh.householdId!==anchor.householdId||fresh.groupId!==anchor.groupId||fresh.providerId!==anchor.providerId||fresh.enrollmentType!==anchor.enrollmentType||!Array.isArray(fresh.members)||pairs(fresh.members)!==pairs(members))fail('HOUSEHOLD_DISCOVERY_COHORT_CHANGED','native household or enrollment cohort changed during discovery');
|
|
30
|
+
return {...input,householdId:anchor.householdId,householdSource:'native-enrollment',groupId:anchor.groupId,providerId:anchor.providerId,type:anchor.enrollmentType,members};
|
|
31
|
+
}
|
|
32
|
+
if(typeof api.households?.listForClient!=='function'||typeof api.households?.get!=='function')fail('HOUSEHOLD_DISCOVERY_READERS_REQUIRED','legacy readers require complete household readers');
|
|
24
33
|
const candidates=[],seen=new Set();let total;
|
|
25
34
|
for(let startPage=1;startPage<=100;startPage++){
|
|
26
35
|
const result=await api.households.listForClient({path:{clientId:input.headClientId},query:{perPage:100,startPage,total_count:true}});
|
package/browser/index.d.mts
CHANGED
|
@@ -17,12 +17,14 @@ export class LoginError extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
export interface UiSession {readonly connection:UiConnection;browser: Browser;context: BrowserContext;page: Page;passwordRecovery:'restored-original'|'not-needed';ensureAuthenticated():Promise<void>;reauthenticate():Promise<void>;close():Promise<void>;}
|
|
19
19
|
export interface PlaywrightGwtOptions {page:Page;tenant:string;moduleBaseUrl:string;}
|
|
20
|
-
export interface PlaywrightEnrollmentSnapshot extends GwtEnrollmentSnapshot {providerId:number;enrollmentType:string;exits:readonly GwtEnrollmentExit[];groupId?:number|string|null;grouped?:boolean;}
|
|
20
|
+
export interface PlaywrightEnrollmentSnapshot extends GwtEnrollmentSnapshot {providerId:number;enrollmentType:string;exits:readonly GwtEnrollmentExit[];groupId?:number|string|null;grouped?:boolean;householdId?:number|null;}
|
|
21
21
|
export interface EnrollmentListRow {
|
|
22
22
|
enrollmentId:number;clientId:number;enrollmentProvider:number;type:string;
|
|
23
23
|
projectStartDate:string;projectExitDate:string|null;isActive:boolean;
|
|
24
24
|
/** An enrollment group pointer, not a household identifier or member count. */
|
|
25
25
|
groupId:number|string|null;grouped:boolean;
|
|
26
|
+
/** Native enrollment association, distinct from groupId and current household membership. */
|
|
27
|
+
householdId?:number|null;
|
|
26
28
|
}
|
|
27
29
|
export interface EnrollmentListSnapshot {
|
|
28
30
|
source:'gwt-rpc';tenant:string;clientId:number;count:number;values:EnrollmentListRow[];
|
|
@@ -114,11 +114,12 @@ export function createPlaywrightGwtSession({page,tenant,moduleBaseUrl}) {
|
|
|
114
114
|
};
|
|
115
115
|
const listEnrollments=async({clientId})=>{
|
|
116
116
|
await openReadContext(clientId);
|
|
117
|
-
const rows=await mapped(`SELECT e.entryExitId AS enrollmentId, e.clientId AS clientId, e.entryDate AS projectStartDate, e.exitDate AS projectExitDate, e.providerId AS enrollmentProvider, e.typeEntryExit.name AS type, e.groupId AS groupId FROM SpEntryExit AS e WHERE e.active = TRUE AND e.client.active = TRUE AND e.clientId = ${clientId} ORDER BY e.entryExitId`);
|
|
117
|
+
const rows=await mapped(`SELECT e.entryExitId AS enrollmentId, e.clientId AS clientId, e.entryDate AS projectStartDate, e.exitDate AS projectExitDate, e.providerId AS enrollmentProvider, e.typeEntryExit.name AS type, e.groupId AS groupId, e.householdId AS householdId FROM SpEntryExit AS e WHERE e.active = TRUE AND e.client.active = TRUE AND e.clientId = ${clientId} ORDER BY e.entryExitId`);
|
|
118
118
|
const ids=new Set();
|
|
119
119
|
for(const row of rows){
|
|
120
120
|
if(!Number.isSafeInteger(row.enrollmentId)||row.enrollmentId<=0||row.clientId!==clientId||!Number.isSafeInteger(row.enrollmentProvider)||row.enrollmentProvider<=0||typeof row.type!=='string'||!row.type.trim()||typeof row.projectStartDate!=='string'||!Number.isFinite(Date.parse(row.projectStartDate))||row.projectExitDate!==null&&(typeof row.projectExitDate!=='string'||!Number.isFinite(Date.parse(row.projectExitDate)))||ids.has(row.enrollmentId))throw Error('Invalid enrollment list');
|
|
121
121
|
ids.add(row.enrollmentId);
|
|
122
|
+
if(row.householdId!==null&&(!Number.isSafeInteger(row.householdId)||row.householdId<=0))throw Error('Invalid enrollment household ID');
|
|
122
123
|
if(row.groupId!==null){if(typeof row.groupId==='number'&&row.groupId<=0)throw Error('Invalid enrollment group');literal(row.groupId);}
|
|
123
124
|
}
|
|
124
125
|
return {source:'gwt-rpc',tenant,clientId,count:rows.length,values:rows.map(row=>({...row,isActive:true,grouped:row.groupId!==null}))};
|
|
@@ -137,9 +138,10 @@ export function createPlaywrightGwtSession({page,tenant,moduleBaseUrl}) {
|
|
|
137
138
|
};
|
|
138
139
|
const readEnrollment=async target=>{
|
|
139
140
|
const model=await openTarget(target),householdClientIds=await householdIds(target);
|
|
140
|
-
const entries=await mapped(`SELECT e.entryExitId AS enrollmentId, e.clientId AS clientId, e.entryDate AS projectStartDate, e.groupId AS groupId, e.providerId AS providerId, e.typeEntryExit.name AS enrollmentType FROM SpEntryExit AS e WHERE e.active = TRUE AND e.entryExitId = ${id(target.enrollmentId)} AND e.clientId = ${id(target.clientId)}`);
|
|
141
|
+
const entries=await mapped(`SELECT e.entryExitId AS enrollmentId, e.clientId AS clientId, e.entryDate AS projectStartDate, e.groupId AS groupId, e.householdId AS householdId, e.providerId AS providerId, e.typeEntryExit.name AS enrollmentType FROM SpEntryExit AS e WHERE e.active = TRUE AND e.entryExitId = ${id(target.enrollmentId)} AND e.clientId = ${id(target.clientId)}`);
|
|
141
142
|
if(entries.length!==1)throw Error('Enrollment missing');const entry=entries[0];
|
|
142
143
|
if(entry.enrollmentId!==target.enrollmentId||entry.clientId!==target.clientId||!Number.isSafeInteger(entry.providerId)||entry.providerId<=0||typeof entry.enrollmentType!=='string'||!entry.enrollmentType.trim())throw Error('Enrollment context incomplete');
|
|
144
|
+
if(entry.householdId!==null&&(!Number.isSafeInteger(entry.householdId)||entry.householdId<=0))throw Error('Invalid enrollment household ID');
|
|
143
145
|
const members=entry.groupId===null?[{clientId:target.clientId,enrollmentId:target.enrollmentId}]:await mapped(`SELECT e.entryExitId AS enrollmentId, e.clientId AS clientId FROM SpEntryExit AS e WHERE e.active = TRUE AND e.client.active = TRUE AND e.groupId = ${literal(entry.groupId)}`);
|
|
144
146
|
members.sort((a,b)=>a.clientId-b.clientId);
|
|
145
147
|
const exitRows=await mapped(`SELECT e.entryExitId AS enrollmentId, e.clientId AS clientId, e.exitDate AS projectExitDate, e.destinationValue.name AS destination, e.destinationValue.code AS destinationCode, e.reasonLeavingValue.name AS reasonForLeaving, e.reasonLeavingValue.code AS reasonForLeavingCode, e.exitSubgroupId AS exitSubgroupId FROM SpEntryExit AS e WHERE e.active = TRUE AND e.client.active = TRUE AND ${entry.groupId===null?`e.entryExitId = ${id(target.enrollmentId)}`:`e.groupId = ${literal(entry.groupId)}`}`);
|
|
@@ -165,7 +167,7 @@ export function createPlaywrightGwtSession({page,tenant,moduleBaseUrl}) {
|
|
|
165
167
|
reviews.push({reviewId:row.reviewId,type:row.type,reviewDate:row.reviewDate,clientIds:clients.map(c=>c.clientId).sort((a,b)=>a-b)});
|
|
166
168
|
}
|
|
167
169
|
reviews.sort((a,b)=>a.reviewId-b.reviewId);
|
|
168
|
-
const data={source:'gwt-rpc',tenant,sessionId,...target,projectStartDate:entry.projectStartDate,providerId:entry.providerId,enrollmentType:entry.enrollmentType,groupId:entry.groupId,grouped:entry.groupId!==null,householdClientIds,members,exits,reviews};
|
|
170
|
+
const data={source:'gwt-rpc',tenant,sessionId,...target,projectStartDate:entry.projectStartDate,providerId:entry.providerId,enrollmentType:entry.enrollmentType,groupId:entry.groupId,grouped:entry.groupId!==null,householdId:entry.householdId,householdClientIds,members,exits,reviews};
|
|
169
171
|
return {...data,revision:digest(model+JSON.stringify({...data,sessionId:undefined}))};
|
|
170
172
|
};
|
|
171
173
|
const chooseMembers=async(desired,implicitClientId,dialogButton)=>{
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Stable public codes. Keep code values when changing their associated messages.
|
|
2
2
|
const codes = new Map([
|
|
3
|
+
["adoption requires the complete linked enrollment cohort", "ADOPTION_COMPLETE_COHORT_REQUIRED"],
|
|
4
|
+
["linked household enrollment requires adoptHouseholdEnrollment", "ADOPTION_HOUSEHOLD_REQUIRED"],
|
|
5
|
+
["verified step changed", "VERIFIED_STEP_CHANGED"],
|
|
3
6
|
["DOB and household head corrections require their dedicated workflows", "DOB_AND_HOUSEHOLD_HEAD_CORRECTIONS_REQUIRE_THEIR_DEDICATED_WORKFLOWS"],
|
|
4
7
|
["DOB helper required for birth-date answers and carry-forward", "DOB_HELPER_REQUIRED_FOR_BIRTH_DATE_ANSWERS_AND_CARRY_FORWARD"],
|
|
5
8
|
["DOB question type changed", "DOB_QUESTION_TYPE_CHANGED"],
|
package/docs/assessments.md
CHANGED
|
@@ -73,7 +73,7 @@ Reproduced on November 2, 2026 in a sandbox tenant with synthetic clients:
|
|
|
73
73
|
|
|
74
74
|
- **Mobile `AddAssessmentAnswer` does not store yes/no values.** Requests with `"Yes"`, `"1"`/`"0"`, and `"true"`/`"false"` returned success and created a row at the intended effective date, but its value read back empty through both the generic and mobile reads. An integrator's 30-answer entry batch showed the same result.
|
|
75
75
|
- **Mobile writes are insert-only per question and effective date.** A second mobile write to any question that already has a row at that date returned HTTP 400 `An error occurred while saving the entity changes`. This affected yes/no and integer questions alike, so a mobile retry cannot repair an empty yes/no row.
|
|
76
|
-
- **A failed mobile batch can still save part of its answers.**
|
|
76
|
+
- **A failed mobile batch can still save part of its answers.** An integrator's November 2, 2026 report describes a 15-answer batch returning HTTP 400 with only the score total saved. GitLab issue #2 reports a captured entity-save error and no prior rows; separate retained notes describe a send shortly after household enrollment and do not retain a before-write snapshot. These accounts do not establish the same pre-write state. Later sends of the batch failed with the insert-only error above because that row existed. On November 3, the same byte-identical batch returned 200 and read back completely on fresh synthetic clients with no enrollment, an enrolled single adult, and an enrolled household head; batches with and without the score total question, and in either order, also succeeded. The cause is unknown; a race with enrollment creation is possible but unproven. Treat a failed mobile write as uncertain and reconcile it from a read, as the enrollment workflow does; never resend it.
|
|
77
77
|
- **Standard `assessments.create` stores `"Yes"`/`"No"`.** It rejects `"true"`/`"false"`. It updates existing rows at the same effective date, including empty yes/no rows created by mobile writes, and changed only the answers it was sent in a full before/after comparison.
|
|
78
78
|
- **Every standard write must include every mandatory answer**, even one already recorded at that date; otherwise it returns `All mandatory answers are not provided`. A conditionally mandatory lookup that does not apply can be sent as `""`; it is stored empty at that date.
|
|
79
79
|
- **Generic reads omit yes/no answers at exactly their effective instant.** `assessments.get` with `dateEffective` equal to the saved instant returns `null` for those yes/no answers while other types read normally. One second later returns the saved value. The standard create response has the same omission. Mobile reads return the value, but they have no date filter and show only the latest row, so a later record hides the date being checked.
|
|
@@ -101,7 +101,9 @@ await writeMobileAnswersChecked(api, {
|
|
|
101
101
|
|
|
102
102
|
`MobileAnswerPreflightError` has a stable `code`, affected `fields`, and `committed: false`. A collision uses `MOBILE_ANSWER_EXISTS`; unavailable evidence uses `MOBILE_PREFLIGHT_UNAVAILABLE`. The helper checks scalars, including questions shared across instruments, but does not detect duplicate subassessment rows. It makes one write after a successful preflight and does not retry or journal it. An error from that write retains its original type and possible uncertain outcome. Reads and writes are not atomic against other users. Pass the original instant; the SDK transport applies any configured mobile date correction.
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
Enrollment workflow plans check scalar collisions before creating an enrollment and again immediately before the first scalar batch. An answer can appear during enrollment/review creation or from another writer after the earlier read. If the complete requested scalar state already matches, the workflow verifies it without another insert. If only some rows occupy the instant, or their values conflict, it refuses before marking the scalar stage attempted. Unavailable evidence also blocks that first attempt.
|
|
105
|
+
|
|
106
|
+
If an earlier enrollment, review or exit stage was already attempted, this refusal still has `committed: 'unknown'` for the overall operation and `step: 'scalars'`. Keep the existing key and journal; the enrollment may already exist. Reconcile the stored answers before resuming. Previously attempted scalar stages retain their normal uncertain-write reconciliation and are never resent or split automatically. No read-before-write check eliminates changes after the last read.
|
|
105
107
|
|
|
106
108
|
## Race and ethnicity combinations
|
|
107
109
|
|
package/docs/capabilities.md
CHANGED
|
@@ -96,7 +96,7 @@ The integrator reports these training-tenant results on November 10 and 11, 2026
|
|
|
96
96
|
- Three pending 0.5.0 corrections were refused without confirmation and resolved as `rejected` with confirmation.
|
|
97
97
|
- Earlier 0.5.0 runs verified individual/household adoption followed by exit, household re-enrollment after exit, and path corrections in both directions for individuals and households, with no-write same-key repeats.
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
Further integrator reports in GitLab issues #5, #6 and #7 confirm three additional 0.6.0 paths on November 11: household adoption followed by `createHouseholdInterim` (a session interruption reauthenticated, and a same-key retry verified without a duplicate review); scalar-only correction on an adopted enrollment (same-key retry wrote nothing); and a new correction key after the original correction was resolved as not applied. These are reported live results, not an independent SDK-team rerun. Row corrections on adopted entries require the later `registerEntryRows` API.
|
|
100
100
|
|
|
101
101
|
The 0.7.0 label resolver, group flags, household discovery, row registration, error codes and mobile collision preflight have local regression coverage, not new live tenant acceptance. In 0.7.0, full write labels remained unresolved for unobserved options; the 0.8.0 catalog reader below removes that limitation when UI catalog access is available. Household discovery refuses ambiguous current-membership matches. Yes/no blanking remains unsupported; no alternative route has been verified.
|
|
102
102
|
|
|
@@ -105,3 +105,9 @@ The 0.7.0 label resolver, group flags, household discovery, row registration, er
|
|
|
105
105
|
Read-only training checks matched all 1,288 distinct option IDs in the Coordinated Entry instrument to current GWT catalog rows across three synthetic clients (4,089 question/option pairs). Catalog names differed from REST metadata names for 139 option occurrences in one definition. The five enhanced yes/no HUD questions shared a catalog; plain yes/no alternatives had separate option IDs. The visible profile DOB-quality dropdown matched the catalog's full labels. This establishes metadata identity and displayed labels without prior-answer evidence, not fresh acceptance of every standard write. Existing enrollment and answer records were left unchanged.
|
|
106
106
|
|
|
107
107
|
SDK 0.8.0 uses the catalog automatically for workflows with a built-in enrollment session. REST-only applications can supply a catalog resolver or retain their explicit callback. See [catalog evidence and limitations](vendor-issues/lookup-write-labels.md).
|
|
108
|
+
|
|
109
|
+
## Native enrollment household identity, November 11, 2026
|
|
110
|
+
|
|
111
|
+
Read-only GWT checks of four existing Coordinated Entry groups (eight enrollments) returned both `groupId` and a distinct native `householdId`, including exited groups. The SDK's updated managed reader independently verified six enrollments across three groups. REST complete reads still returned HTTP 400 for the grouped controls, while standalone controls returned null group and household IDs.
|
|
112
|
+
|
|
113
|
+
Using a temporary private journal, native discovery and `adoptHouseholdEnrollment` succeeded for one open and one exited two-person group using only the head's client/enrollment IDs. Both same-key retries returned `already-present`. The workflow was given no household REST reader; no WellSky record-write requests were made. These results verify native reads and read-only adoption, not new live interim or exit writes after household changes. Synthetic tests cover those later operations, identity drift, original-head evidence, current-membership requirements for additions, and legacy-key retries. See [the identity evidence](vendor-issues/enrollment-household-identity.md).
|
package/docs/enrollments.md
CHANGED
|
@@ -232,7 +232,7 @@ const detail = await session.enrollmentSession.readEnrollment({
|
|
|
232
232
|
// detail.members contains the complete linked clientId/enrollmentId pairs.
|
|
233
233
|
```
|
|
234
234
|
|
|
235
|
-
Use the session returned by `openEnrollmentSession`. Lists include open and exited entries, validate complete counts, and refuse more than 1,000 records. List rows and details include `groupId` and `grouped`. The flag describes a stored group link, including a group with one participant; it is not a member count and requires no per-row detail reads. These fields are required on managed catalog rows and optional on custom detail-reader contracts for compatibility. `groupId` is never a household ID.
|
|
235
|
+
Use the session returned by `openEnrollmentSession`. Lists include open and exited entries, validate complete counts, and refuse more than 1,000 records. List rows and details include `groupId` and `grouped`. The flag describes a stored group link, including a group with one participant; it is not a member count and requires no per-row detail reads. These fields are required on managed catalog rows and optional on custom detail-reader contracts for compatibility. `groupId` is never a household ID. Managed lists and details also return `householdId`, read directly from the enrollment record; `null` means no recorded association. It is optional on custom reader contracts for compatibility. Use `discoverHouseholdEnrollment` below to verify that association across the complete cohort.
|
|
236
236
|
|
|
237
237
|
The same session also exposes `readPicklistOptions({clientId, optionIds})` for exact labels before answers exist. It uses fresh read context, validates counted results, and makes no record writes. Enrollment workflows use it automatically unless `writeLabel` is supplied. See [catalog lookup labels](assessments.md#resolve-lookup-labels-before-an-answer-exists).
|
|
238
238
|
|
|
@@ -262,7 +262,7 @@ For a person in a linked household enrollment, use `exitHousehold` with that per
|
|
|
262
262
|
|
|
263
263
|
## Adopt an enrollment entered manually in WellSky
|
|
264
264
|
|
|
265
|
-
Use `adoptEnrollment({key, clientId, enrollmentId})` for a standalone enrollment. For linked household enrollments, use `adoptHouseholdEnrollment({key, householdId, headClientId, members})`, where each member supplies both `clientId` and `enrollmentId`. Supply the complete linked cohort, including members already exited, and the original enrollment head. Adoption verifies authoritative GWT identities, program/type, entry dates,
|
|
265
|
+
Use `adoptEnrollment({key, clientId, enrollmentId})` for a standalone enrollment. For linked household enrollments, use `adoptHouseholdEnrollment({key, householdId, headClientId, members})`, where each member supplies both `clientId` and `enrollmentId`. Supply the complete linked cohort, including members already exited, and the original enrollment head. Adoption verifies authoritative GWT identities, program/type, entry dates, the native household/group association when available, original head relationships, and head coverage across all enrollment intervals. An explicit household ID must match the native enrollment field. Legacy custom readers without that field retain their current-household validation.
|
|
266
266
|
|
|
267
267
|
Adoption writes only to the local durable journal. It does not create, attach, exit or modify an HMIS record. Later entrants retain their own original entry dates. An enrollment already tracked under another entry key is rejected; use its existing key. Retry an interrupted adoption with the same inputs and store.
|
|
268
268
|
|
|
@@ -274,13 +274,20 @@ After standalone adoption, its key can be used as `enrollmentKey` for `createInt
|
|
|
274
274
|
const discovered = await workflow.discoverHouseholdEnrollment({
|
|
275
275
|
headClientId, headEnrollmentId,
|
|
276
276
|
});
|
|
277
|
-
//
|
|
277
|
+
// Built-in reader: householdId, groupId, householdSource: "native-enrollment", providerId, type, members
|
|
278
278
|
const adopted = await workflow.adoptHouseholdEnrollment({
|
|
279
279
|
key: sourceEnrollmentKey, headClientId, headEnrollmentId,
|
|
280
280
|
});
|
|
281
281
|
```
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
WellSky stores both `SpEntryExit.groupId` and `SpEntryExit.householdId`. With a built-in reader, discovery reads the household ID directly from the enrollment, verifies that every linked participant has the same household and group IDs, and rechecks the anchor before returning `householdSource: 'native-enrollment'`. It does not search current households to infer the association. Missing native IDs, mixed associations, incomplete cohorts or changed identities stop discovery. A present-but-null native household field never falls back to a membership guess.
|
|
284
|
+
|
|
285
|
+
Native adoption also verifies the original head using entry-time `HUD_RELATIONTOHOH` answers and checks that the head's enrollment covers every member's interval. New native adoptions without that head evidence are refused. Existing verified journal keys retain their accepted head identity and intent hashes. Adoption only records identities locally; it makes no WellSky record writes.
|
|
286
|
+
|
|
287
|
+
Interims and exits for native-linked groups validate the recorded household/group IDs and enrolled participants without requiring them to remain in an active current household. Group identities are pinned in the journal; drift blocks later operations. This allows recorded groups to remain usable after current household membership changes. Enrollment-head replacement remains unsupported, and the original head must still cover the members' enrollment dates. Creation and later member additions continue to require valid current household membership.
|
|
288
|
+
|
|
289
|
+
Legacy custom detail readers that omit `householdId` retain the older unique-current-membership discovery, explicitly labeled `householdSource: 'verified-membership'`. That fallback does not prove a historical association. Upgrade those adapters to return the native `householdId` and `groupId` to use the new path. Existing keys whose original IDs agree remain valid; a conflicting historical guess is refused rather than silently rebound. See [native household evidence](vendor-issues/enrollment-household-identity.md).
|
|
290
|
+
|
|
284
291
|
|
|
285
292
|
### Register existing entry rows
|
|
286
293
|
|
package/docs/reference.md
CHANGED
|
@@ -37,7 +37,7 @@ Runtime exports: `AttachmentConflictError`, `CoordinatedEntryEventConflictError`
|
|
|
37
37
|
| Profile fields and Sex | [Client profile](../browser/client-profile.d.mts) | `read`, `update` |
|
|
38
38
|
| Calendar DOB | [Birth dates](../browser/date-of-birth.d.mts) | `read`, `validate`, `verify`, `update` |
|
|
39
39
|
| Provider assessment defaults | [Discovery](../browser/program-discovery.d.mts) | `listAssessments`, `getProviderAssessments` |
|
|
40
|
-
| Enrollment and household lifecycle | [Workflow](../browser/enrollment-workflow.d.mts) | Discovery, entry, `correctEntry`, `resolveEntryCorrection`, `adoptEnrollment`, `adoptHouseholdEnrollment`, `discoverHouseholdEnrollment`, `registerEntryRows`, member additions, interim, `exitEnrollment`, `exitHousehold`, journal status; `EntryExitType` and per-type `assessmentLocations` in [enrollments](enrollments.md) |
|
|
40
|
+
| Enrollment and household lifecycle | [Workflow](../browser/enrollment-workflow.d.mts) | Discovery, entry, `correctEntry`, `resolveEntryCorrection`, `adoptEnrollment`, `adoptHouseholdEnrollment`, `discoverHouseholdEnrollment`, `registerEntryRows`, native household/group identity, member additions, interim, `exitEnrollment`, `exitHousehold`, journal status; `EntryExitType` and per-type `assessmentLocations` in [enrollments](enrollments.md) |
|
|
41
41
|
| Entry-date and exit UI ports | [Member entry](../browser/member-entry-date.d.mts), [exit](../browser/household-exit.d.mts) | Fresh reads and targeted completion |
|
|
42
42
|
| Services and recovery diagnostics | [Service recording](../browser/service-recording.d.mts) | `record`, `recordBulk`, Funding Sources, `RecoveryDiagnostic` |
|
|
43
43
|
| SSVF conditional rules | [SSVF types](../browser/ssvf-rules.d.mts) | [Field rules](../SSVF.md) and dated option snapshots |
|
package/docs/troubleshooting.md
CHANGED
|
@@ -59,7 +59,7 @@ Enrollment workflow errors expose stable `code` values. Common actionable codes
|
|
|
59
59
|
| `MOBILE_ANSWER_EXISTS` | A scalar row already occupies the requested mobile effective instant |
|
|
60
60
|
| `MOBILE_PREFLIGHT_UNAVAILABLE` | The read cannot establish whether a scalar insert is safe |
|
|
61
61
|
|
|
62
|
-
The [
|
|
62
|
+
The complete [SDK-owned code inventory](../API.md#enrollment-workflow-errors) is generated from the guards and stable reason map. Generation rejects new constant refusal reasons that have no specific code, including conditional branches. `committed: false` identifies a pre-write refusal for the named operation. `'unknown'` requires journal/readback reconciliation. Resolution errors separately retain `originalCommitted: 'unknown'` for the original correction. HTTP status and causes are exposed where captured; they are not proof of non-application.
|
|
63
63
|
|
|
64
64
|
Pending 0.5.0 corrections may have no saved `failure` kind or status. Keep your own original failure evidence for the confirmation callback. Do not repeat the write to recover missing diagnostics.
|
|
65
65
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Enrollment household identity
|
|
2
|
+
|
|
3
|
+
On November 11, 2026, authenticated read-only training checks showed that WellSky exposes both an enrollment group ID and a household ID on `SpEntryExit`. The SDK had selected only the group ID. A group ID must never be substituted for a household ID.
|
|
4
|
+
|
|
5
|
+
The application's current `MetaQueryService` / `GenericService.getMappedResults` accepted a projection of `entryExitId`, `clientId`, `groupId`, `householdId`, provider and enrollment dates. Four existing Coordinated Entry groups, containing eight enrollments, had consistent native household IDs within each group; the household IDs differed from the group IDs. Three of those groups were exited. This establishes the recorded enrollment association without matching current household membership. It does not claim the field is an immutable audit history.
|
|
6
|
+
|
|
7
|
+
The [managed reader](../../browser/playwright-session.mjs) now selects and validates the native field in both catalog and detail reads. SDK reads independently verified six enrollments across three groups. Native discovery and adoption then succeeded on an open group and an exited group, including same-key no-write retries, using a temporary local journal. No enrollment, household, assessment, review, exit or password was changed. Raw captures and client identifiers are excluded from the package.
|
|
8
|
+
|
|
9
|
+
The pinned REST schema already declares `householdId`, but grouped complete reads returned HTTP 400 in the read-only controls. Standalone control reads succeeded with null household and group IDs. The existing [grouped REST defect](enrollment-effective-date-column.md) still applies.
|
|
10
|
+
|
|
11
|
+
Native adoption verifies the complete cohort's recorded household/group identities and entry-time head relationship. Reviews and exits retain the original enrollment head and interval coverage while using the native association. Current household membership continues to constrain creation and member additions. A missing native field in an older custom reader retains the explicitly labeled legacy path; a null or inconsistent native field is not guessed from current membership.
|
|
12
|
+
|
|
13
|
+
See [adoption and discovery](../enrollments.md#discover-a-household-from-its-head-enrollment) for usage and compatibility limits.
|
|
@@ -36,8 +36,16 @@ After a successful standard write, the generic assessment read with `dateEffecti
|
|
|
36
36
|
|
|
37
37
|
## A failed mobile batch saved one of its answers
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
GitLab issue #2 reports a November 2, 2026 synthetic-client test: a combined 15-answer batch (1 `date`, 13 `lookup`, 1 `int`) returned HTTP 400 with `An error occurred while saving the entity changes. See the inner exception for details.` Only the integer score answer was stored afterwards. The issue says no row existed beforehand, that the error body was captured, and that the non-score batch and score-only batch each succeeded separately.
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Separate retained run notes describe a batch sent shortly after household enrollment and entry answers, without a retained before-write assessment snapshot or raw error body. These accounts differ in their stated evidence. The SDK team cannot establish that they describe identical attempts or independently confirm their pre-write state. An existing answer shared by two instruments is one possible collision source, not an established explanation of the reported failure.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
On November 3, the byte-identical batch returned 200 and read back completely on fresh synthetic clients without an enrollment, an enrolled single adult, and an enrolled household head. Splitting the batch, reordering it, or omitting the `int` answer made no difference in those successful controls. The original failure remains unreproduced in that follow-up. These controls do not disprove the original report or establish a score-specific serialization rule.
|
|
44
|
+
|
|
45
|
+
## SDK guard for an observed collision after preflight
|
|
46
|
+
|
|
47
|
+
A November 11 synthetic regression demonstrated a narrower SDK defect: enrollment creation populated a scalar after the initial preflight; the scalar-stage read observed that occupied instant, but the workflow still submitted the full mobile batch. SDK 0.8.1 now refuses that first scalar attempt before marking it attempted. This protects against an observed collision and unavailable evidence after an earlier lifecycle stage. It does not establish the cause of issue #2 or make mobile batches atomic.
|
|
48
|
+
|
|
49
|
+
A separate synthetic partial-commit control returned HTTP 400 after saving only one answer. Its stage stayed pending, same-key retries sent nothing, and later row stages did not run. Preserve that uncertain-write behavior. Do not split, replay, change the effective date, or replace the key to work around an already-attempted batch.
|
|
50
|
+
|
|
51
|
+
**Ask WellSky:** Can a mobile write that returns HTTP 400 commit part of its batch? Is a write to an assessment assigned to the provider affected by work the server does after enrollment creation? Which server-side diagnostic can identify the failing question and database constraint?
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {createEnrollmentWorkflow,type EnrollmentWorkflowOptions,type EntryCorrectionInput} from '@choiceapps/communityservices-sdk/browser';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** Built-in readers verify the native household and group; older adapters identify their membership fallback explicitly. */
|
|
4
4
|
export async function adoptFromHead(dependencies:EnrollmentWorkflowOptions,input:{key:string;headClientId:number;headEnrollmentId:number}){
|
|
5
5
|
const workflow=createEnrollmentWorkflow(dependencies);
|
|
6
6
|
const discovery=await workflow.discoverHouseholdEnrollment({headClientId:input.headClientId,headEnrollmentId:input.headEnrollmentId});
|
package/package.json
CHANGED