@choiceapps/communityservices-sdk 0.1.0
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/AGENTS.md +43 -0
- package/API.md +86 -0
- package/CHANGELOG.md +12 -0
- package/LICENSE +201 -0
- package/NOTICE +11 -0
- package/README.md +100 -0
- package/SSVF.md +131 -0
- package/browser/attachment-conflict.mjs +6 -0
- package/browser/case-plans-ui.mjs +23 -0
- package/browser/case-plans.d.mts +20 -0
- package/browser/case-plans.mjs +89 -0
- package/browser/client-creation.d.mts +16 -0
- package/browser/client-creation.mjs +60 -0
- package/browser/client-profile-ui.mjs +59 -0
- package/browser/client-profile.d.mts +24 -0
- package/browser/client-profile.mjs +94 -0
- package/browser/client-workflows.mjs +135 -0
- package/browser/connection.d.mts +4 -0
- package/browser/connection.mjs +41 -0
- package/browser/current-living-situation.d.mts +44 -0
- package/browser/current-living-situation.mjs +161 -0
- package/browser/date-of-birth-ui.mjs +68 -0
- package/browser/date-of-birth.d.mts +28 -0
- package/browser/date-of-birth.mjs +112 -0
- package/browser/enrollment-workflow.d.mts +174 -0
- package/browser/enrollment-workflow.mjs +518 -0
- package/browser/exit-details.mjs +21 -0
- package/browser/file-store.mjs +32 -0
- package/browser/gwt-mapped.mjs +46 -0
- package/browser/household-exit.d.mts +34 -0
- package/browser/household-exit.mjs +102 -0
- package/browser/households-ui.mjs +20 -0
- package/browser/households.d.mts +22 -0
- package/browser/households.mjs +115 -0
- package/browser/index.d.mts +78 -0
- package/browser/index.mjs +46 -0
- package/browser/member-entry-date.d.mts +30 -0
- package/browser/member-entry-date.mjs +71 -0
- package/browser/need-recording.d.mts +37 -0
- package/browser/need-recording.mjs +68 -0
- package/browser/need-ui.mjs +138 -0
- package/browser/page-lock.mjs +11 -0
- package/browser/password-recovery.mjs +49 -0
- package/browser/playwright-session.mjs +196 -0
- package/browser/program-discovery.d.mts +22 -0
- package/browser/program-discovery.mjs +80 -0
- package/browser/recovery-diagnostics.mjs +9 -0
- package/browser/service-recording.d.mts +45 -0
- package/browser/service-recording.mjs +148 -0
- package/browser/service-ui.mjs +160 -0
- package/browser/session-authentication.mjs +40 -0
- package/browser/shelter-inventory.d.mts +103 -0
- package/browser/shelter-inventory.mjs +273 -0
- package/browser/shelter-ui.mjs +382 -0
- package/browser/shelters.d.mts +139 -0
- package/browser/shelters.mjs +425 -0
- package/browser/ssvf-rules.d.mts +21 -0
- package/browser/ssvf-rules.mjs +145 -0
- package/browser/ui-login.mjs +135 -0
- package/dist/cjs/bulk.d.ts +33 -0
- package/dist/cjs/bulk.d.ts.map +1 -0
- package/dist/cjs/bulk.js +42 -0
- package/dist/cjs/bulk.js.map +1 -0
- package/dist/cjs/control.d.ts +35 -0
- package/dist/cjs/control.d.ts.map +1 -0
- package/dist/cjs/control.js +141 -0
- package/dist/cjs/control.js.map +1 -0
- package/dist/cjs/effective-date.d.ts +3 -0
- package/dist/cjs/effective-date.d.ts.map +1 -0
- package/dist/cjs/effective-date.js +27 -0
- package/dist/cjs/effective-date.js.map +1 -0
- package/dist/cjs/encoding.d.ts +32 -0
- package/dist/cjs/encoding.d.ts.map +1 -0
- package/dist/cjs/encoding.js +121 -0
- package/dist/cjs/encoding.js.map +1 -0
- package/dist/cjs/enrollment-verifications.d.ts +39 -0
- package/dist/cjs/enrollment-verifications.d.ts.map +1 -0
- package/dist/cjs/enrollment-verifications.js +96 -0
- package/dist/cjs/enrollment-verifications.js.map +1 -0
- package/dist/cjs/errors.d.ts +33 -0
- package/dist/cjs/errors.d.ts.map +1 -0
- package/dist/cjs/errors.js +45 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/generated/v2-models.d.ts +1482 -0
- package/dist/cjs/generated/v2-models.d.ts.map +1 -0
- package/dist/cjs/generated/v2-models.js +3 -0
- package/dist/cjs/generated/v2-models.js.map +1 -0
- package/dist/cjs/generated/v2.d.ts +1149 -0
- package/dist/cjs/generated/v2.d.ts.map +1 -0
- package/dist/cjs/generated/v2.js +252 -0
- package/dist/cjs/generated/v2.js.map +1 -0
- package/dist/cjs/generated/v3-models.d.ts +1420 -0
- package/dist/cjs/generated/v3-models.d.ts.map +1 -0
- package/dist/cjs/generated/v3-models.js +3 -0
- package/dist/cjs/generated/v3-models.js.map +1 -0
- package/dist/cjs/generated/v3.d.ts +1149 -0
- package/dist/cjs/generated/v3.d.ts.map +1 -0
- package/dist/cjs/generated/v3.js +252 -0
- package/dist/cjs/generated/v3.js.map +1 -0
- package/dist/cjs/gwt-enrollments.d.ts +131 -0
- package/dist/cjs/gwt-enrollments.d.ts.map +1 -0
- package/dist/cjs/gwt-enrollments.js +280 -0
- package/dist/cjs/gwt-enrollments.js.map +1 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +19 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/metadata.d.ts +100 -0
- package/dist/cjs/metadata.d.ts.map +1 -0
- package/dist/cjs/metadata.js +165 -0
- package/dist/cjs/metadata.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/program-discovery.d.ts +28 -0
- package/dist/cjs/program-discovery.d.ts.map +1 -0
- package/dist/cjs/program-discovery.js +58 -0
- package/dist/cjs/program-discovery.js.map +1 -0
- package/dist/cjs/race.d.ts +4 -0
- package/dist/cjs/race.d.ts.map +1 -0
- package/dist/cjs/race.js +40 -0
- package/dist/cjs/race.js.map +1 -0
- package/dist/cjs/runtime.d.ts +108 -0
- package/dist/cjs/runtime.d.ts.map +1 -0
- package/dist/cjs/runtime.js +38 -0
- package/dist/cjs/runtime.js.map +1 -0
- package/dist/cjs/subassessments.d.ts +29 -0
- package/dist/cjs/subassessments.d.ts.map +1 -0
- package/dist/cjs/subassessments.js +107 -0
- package/dist/cjs/subassessments.js.map +1 -0
- package/dist/cjs/testing.d.ts +45 -0
- package/dist/cjs/testing.d.ts.map +1 -0
- package/dist/cjs/testing.js +66 -0
- package/dist/cjs/testing.js.map +1 -0
- package/dist/cjs/transport.d.ts +19 -0
- package/dist/cjs/transport.d.ts.map +1 -0
- package/dist/cjs/transport.js +373 -0
- package/dist/cjs/transport.js.map +1 -0
- package/dist/cjs/v2.d.ts +13 -0
- package/dist/cjs/v2.d.ts.map +1 -0
- package/dist/cjs/v2.js +29 -0
- package/dist/cjs/v2.js.map +1 -0
- package/dist/cjs/v3.d.ts +13 -0
- package/dist/cjs/v3.d.ts.map +1 -0
- package/dist/cjs/v3.js +29 -0
- package/dist/cjs/v3.js.map +1 -0
- package/dist/cjs/validation.d.ts +33 -0
- package/dist/cjs/validation.d.ts.map +1 -0
- package/dist/cjs/validation.js +77 -0
- package/dist/cjs/validation.js.map +1 -0
- package/dist/cjs/verification.d.ts +82 -0
- package/dist/cjs/verification.d.ts.map +1 -0
- package/dist/cjs/verification.js +122 -0
- package/dist/cjs/verification.js.map +1 -0
- package/dist/esm/bulk.d.ts +33 -0
- package/dist/esm/bulk.d.ts.map +1 -0
- package/dist/esm/bulk.js +39 -0
- package/dist/esm/bulk.js.map +1 -0
- package/dist/esm/control.d.ts +35 -0
- package/dist/esm/control.d.ts.map +1 -0
- package/dist/esm/control.js +132 -0
- package/dist/esm/control.js.map +1 -0
- package/dist/esm/effective-date.d.ts +3 -0
- package/dist/esm/effective-date.d.ts.map +1 -0
- package/dist/esm/effective-date.js +24 -0
- package/dist/esm/effective-date.js.map +1 -0
- package/dist/esm/encoding.d.ts +32 -0
- package/dist/esm/encoding.d.ts.map +1 -0
- package/dist/esm/encoding.js +117 -0
- package/dist/esm/encoding.js.map +1 -0
- package/dist/esm/enrollment-verifications.d.ts +39 -0
- package/dist/esm/enrollment-verifications.d.ts.map +1 -0
- package/dist/esm/enrollment-verifications.js +93 -0
- package/dist/esm/enrollment-verifications.js.map +1 -0
- package/dist/esm/errors.d.ts +33 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/errors.js +33 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/generated/v2-models.d.ts +1482 -0
- package/dist/esm/generated/v2-models.d.ts.map +1 -0
- package/dist/esm/generated/v2-models.js +2 -0
- package/dist/esm/generated/v2-models.js.map +1 -0
- package/dist/esm/generated/v2.d.ts +1149 -0
- package/dist/esm/generated/v2.d.ts.map +1 -0
- package/dist/esm/generated/v2.js +247 -0
- package/dist/esm/generated/v2.js.map +1 -0
- package/dist/esm/generated/v3-models.d.ts +1420 -0
- package/dist/esm/generated/v3-models.d.ts.map +1 -0
- package/dist/esm/generated/v3-models.js +2 -0
- package/dist/esm/generated/v3-models.js.map +1 -0
- package/dist/esm/generated/v3.d.ts +1149 -0
- package/dist/esm/generated/v3.d.ts.map +1 -0
- package/dist/esm/generated/v3.js +247 -0
- package/dist/esm/generated/v3.js.map +1 -0
- package/dist/esm/gwt-enrollments.d.ts +131 -0
- package/dist/esm/gwt-enrollments.d.ts.map +1 -0
- package/dist/esm/gwt-enrollments.js +274 -0
- package/dist/esm/gwt-enrollments.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/metadata.d.ts +100 -0
- package/dist/esm/metadata.d.ts.map +1 -0
- package/dist/esm/metadata.js +157 -0
- package/dist/esm/metadata.js.map +1 -0
- package/dist/esm/program-discovery.d.ts +28 -0
- package/dist/esm/program-discovery.d.ts.map +1 -0
- package/dist/esm/program-discovery.js +55 -0
- package/dist/esm/program-discovery.js.map +1 -0
- package/dist/esm/race.d.ts +4 -0
- package/dist/esm/race.d.ts.map +1 -0
- package/dist/esm/race.js +37 -0
- package/dist/esm/race.js.map +1 -0
- package/dist/esm/runtime.d.ts +108 -0
- package/dist/esm/runtime.d.ts.map +1 -0
- package/dist/esm/runtime.js +33 -0
- package/dist/esm/runtime.js.map +1 -0
- package/dist/esm/subassessments.d.ts +29 -0
- package/dist/esm/subassessments.d.ts.map +1 -0
- package/dist/esm/subassessments.js +103 -0
- package/dist/esm/subassessments.js.map +1 -0
- package/dist/esm/testing.d.ts +45 -0
- package/dist/esm/testing.d.ts.map +1 -0
- package/dist/esm/testing.js +60 -0
- package/dist/esm/testing.js.map +1 -0
- package/dist/esm/transport.d.ts +19 -0
- package/dist/esm/transport.d.ts.map +1 -0
- package/dist/esm/transport.js +369 -0
- package/dist/esm/transport.js.map +1 -0
- package/dist/esm/v2.d.ts +13 -0
- package/dist/esm/v2.d.ts.map +1 -0
- package/dist/esm/v2.js +13 -0
- package/dist/esm/v2.js.map +1 -0
- package/dist/esm/v3.d.ts +13 -0
- package/dist/esm/v3.d.ts.map +1 -0
- package/dist/esm/v3.js +13 -0
- package/dist/esm/v3.js.map +1 -0
- package/dist/esm/validation.d.ts +33 -0
- package/dist/esm/validation.d.ts.map +1 -0
- package/dist/esm/validation.js +72 -0
- package/dist/esm/validation.js.map +1 -0
- package/dist/esm/verification.d.ts +82 -0
- package/dist/esm/verification.d.ts.map +1 -0
- package/dist/esm/verification.js +117 -0
- package/dist/esm/verification.js.map +1 -0
- package/docs/README.md +56 -0
- package/docs/assessments.md +103 -0
- package/docs/browser-session.md +129 -0
- package/docs/capabilities.md +75 -0
- package/docs/configuration.md +81 -0
- package/docs/current-living-situation.md +56 -0
- package/docs/dates-and-answers.md +40 -0
- package/docs/development.md +81 -0
- package/docs/durable-records.md +156 -0
- package/docs/enrollments.md +217 -0
- package/docs/files-and-notes.md +39 -0
- package/docs/getting-started.md +56 -0
- package/docs/independent-needs.md +77 -0
- package/docs/integration-methods.md +60 -0
- package/docs/profiles.md +68 -0
- package/docs/reference.md +64 -0
- package/docs/rest-client.md +97 -0
- package/docs/services.md +66 -0
- package/docs/shelter-inventory.md +64 -0
- package/docs/shelters.md +75 -0
- package/docs/troubleshooting.md +61 -0
- package/docs/using-the-sdk.md +203 -0
- package/docs/vendor-issues/README.md +46 -0
- package/docs/vendor-issues/api-coverage.md +27 -0
- package/docs/vendor-issues/attachment-authentication.md +40 -0
- package/docs/vendor-issues/date-semantics.md +51 -0
- package/docs/vendor-issues/empty-service-list-404.md +51 -0
- package/docs/vendor-issues/enrollment-complete-target-error.md +178 -0
- package/docs/vendor-issues/enrollment-effective-date-column.md +112 -0
- package/docs/vendor-issues/household-head-validation.md +26 -0
- package/docs/vendor-issues/write-contracts.md +51 -0
- package/examples/README.md +41 -0
- package/examples/current-living-situation.mts +27 -0
- package/examples/durable-records.mts +78 -0
- package/examples/enrollment-workflow.mts +46 -0
- package/examples/gwt-enrollment-session.ts +17 -0
- package/examples/gwt-file-store.mjs +2 -0
- package/examples/household-enrollment-workflow.mts +43 -0
- package/examples/household-exit-workflow.mts +35 -0
- package/examples/household-lifecycle-workflow.mts +34 -0
- package/examples/independent-needs.mts +30 -0
- package/examples/program-discovery.ts +19 -0
- package/examples/quickstart.mjs +13 -0
- package/examples/shelter-inventory.mts +30 -0
- package/examples/shelters.mts +33 -0
- package/examples/usage.ts +23 -0
- package/examples/verification-snapshot.ts +9 -0
- package/llms.txt +26 -0
- package/package.json +103 -0
- package/specs/provenance.json +17 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Grouped enrollment reads fail in V2 and V3
|
|
2
|
+
|
|
3
|
+
Status: reproduced in a sandbox tenant on October 24, 2026, with Client API V2 and V3. Other tenant deployments were not tested. The comparison used existing synthetic records and made no client, household, enrollment, assessment, or shelter writes.
|
|
4
|
+
|
|
5
|
+
## Summary for WellSky
|
|
6
|
+
|
|
7
|
+
Both versions return HTTP 400 with the same missing-column error for grouped enrollment reads. All 16 tested grouped complete-enrollment requests per version failed. None of the six individual enrollments produced this particular error, although one had a separate complete-read failure.
|
|
8
|
+
|
|
9
|
+
The strongest comparison is within client 575: direct reads of its individual enrollments succeed, its grouped enrollment fails, and pagination can return the individual rows successfully. This points toward processing a grouped enrollment rather than the client simply belonging to a household. The underlying SQL/view defect remains unconfirmed.
|
|
10
|
+
|
|
11
|
+
## Request contract and exact error
|
|
12
|
+
|
|
13
|
+
```http
|
|
14
|
+
GET /client-api-prod/enrollments/{clientId}?perPage=100&startPage=1&total_count=true
|
|
15
|
+
GET /client-api-prod/enrollments/complete/{enrollmentId}
|
|
16
|
+
Host: csclientapi.wellsky.com
|
|
17
|
+
Accept: application/json
|
|
18
|
+
x-tenant: your-tenant
|
|
19
|
+
x-version: 2.0
|
|
20
|
+
Authorization: Bearer <redacted>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Repeat each request with `x-version: 3.0`, keeping the other inputs unchanged. The server returned HTTP 400 and this JSON string value:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
42703: column v.entry_date_effective does not exist
|
|
27
|
+
|
|
28
|
+
POSITION: 116
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Response headers advertised `api-supported-versions: 3.0` and `api-deprecated-versions: 2.0`. V2 still accepted requests; switching to it did not avoid the defect. A provider-complete control request returned HTTP 200 in each version with the same credentials.
|
|
32
|
+
|
|
33
|
+
## Method and results
|
|
34
|
+
|
|
35
|
+
The main comparison ran at 15:56 UTC, 11:56 a.m. Eastern, on October 24. Follow-up pagination and filter checks ran through 15:57:01 UTC. Requests used raw HTTP, bypassing SDK response handling, with the same bearer token within each comparison run. Only `x-version` changed between each pair.
|
|
36
|
+
|
|
37
|
+
Fresh, counted GWT reads established 22 active enrollment records across the synthetic cohort. Six had a null `groupId`; 16 had a non-null group ID across eight household groups. Here, “individual” describes the enrollment's grouping, not whether the client belongs to a household elsewhere.
|
|
38
|
+
|
|
39
|
+
Results below are counts **per API version**. V2 and V3 had identical outcomes.
|
|
40
|
+
|
|
41
|
+
| Read and cohort | Tested per version | V2 | V3 |
|
|
42
|
+
| --- | ---: | --- | --- |
|
|
43
|
+
| Client list, individual enrollments only | 4 clients | 4 HTTP 200 | 4 HTTP 200 |
|
|
44
|
+
| Client list, grouped enrollments only | 13 clients | 13 HTTP 400, missing column | 13 HTTP 400, missing column |
|
|
45
|
+
| Client list, individual and grouped enrollments mixed | 1 client | HTTP 400, missing column | HTTP 400, missing column |
|
|
46
|
+
| Complete individual enrollment | 6 enrollments | 5 HTTP 200; 1 different HTTP 400 | 5 HTTP 200; 1 different HTTP 400 |
|
|
47
|
+
| Complete grouped enrollment | 16 enrollments | 16 HTTP 400, missing column | 16 HTTP 400, missing column |
|
|
48
|
+
| Client list, no active enrollments | 1 client | HTTP 404 | HTTP 404 |
|
|
49
|
+
|
|
50
|
+
The grouped cohort included HUD and Basic enrollments, household heads and other members, open and exited records, and RRH, TH, and ES programs. Earlier capture established the error after ordinary UI saves and after RPC replay, including before interim creation. It is not confined to shelter-created enrollments or interims.
|
|
51
|
+
|
|
52
|
+
## Concrete reproduction pairs
|
|
53
|
+
|
|
54
|
+
These identifiers refer only to existing synthetic sandbox fixtures. They contain no client demographics.
|
|
55
|
+
|
|
56
|
+
| Client | Enrollment | Group ID | Result in both versions |
|
|
57
|
+
| --- | --- | --- | --- |
|
|
58
|
+
| 577 | 679 | null | List and complete read succeed |
|
|
59
|
+
| 581 | 682 | 682 | List and complete read fail with missing column |
|
|
60
|
+
| 582 | 684 | 682 | Other household member also fails |
|
|
61
|
+
| 575 | 677, 678 | null | Both direct complete reads succeed |
|
|
62
|
+
| 575 | 725 | 725 | Direct complete read fails; full client list also fails |
|
|
63
|
+
| 644, 645 | 729, 730 | 729 | Both exited TH enrollments fail |
|
|
64
|
+
| 642, 643 | 731, 732 | 731 | Both open nightly ES enrollments fail |
|
|
65
|
+
|
|
66
|
+
Enrollments 679 and 682 use the same provider, 16951, the same HUD type, and the same entry instant, `2026-10-18T16:00:00Z`. Their different grouping provides a closer comparison than records from unrelated programs. Both were confirmed present by fresh GWT reads.
|
|
67
|
+
|
|
68
|
+
To reproduce without changing records:
|
|
69
|
+
|
|
70
|
+
1. Call `/enrollments/577` and `/enrollments/complete/679` with `x-version: 2.0`, then `3.0`. Expect HTTP 200 for these fixtures.
|
|
71
|
+
2. Call `/enrollments/581` and `/enrollments/complete/682` with each version. Observe HTTP 400 and the missing-column message.
|
|
72
|
+
3. Compare `/enrollments/complete/678` and `/enrollments/complete/725`, both belonging to client 575. The individual enrollment succeeds; the grouped enrollment fails.
|
|
73
|
+
4. Compare the client-575 pagination cases below. Preserve the response status, timestamp, version, and any support correlation ID. Do not include credentials in a ticket.
|
|
74
|
+
|
|
75
|
+
## Pagination and query isolation
|
|
76
|
+
|
|
77
|
+
These follow-up results were identical in V2 and V3:
|
|
78
|
+
|
|
79
|
+
| Request | Result |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `/enrollments/575?perPage=1&startPage=1&total_count=true` | HTTP 400, missing column |
|
|
82
|
+
| Same request with `startPage=2` | HTTP 200, enrollment 678, null group, total count 3 |
|
|
83
|
+
| Same request with `startPage=3` | HTTP 200, enrollment 677, null group, total count 3 |
|
|
84
|
+
| `/enrollments/575?perPage=100&startPage=1&total_count=false` | HTTP 400, missing column |
|
|
85
|
+
| `/enrollments/577` with no query parameters | HTTP 200 |
|
|
86
|
+
| `/enrollments/581` with no query parameters | HTTP 400, missing column |
|
|
87
|
+
| Client 575, normal counted request plus `$filter=groupId eq null` | HTTP 400, missing column |
|
|
88
|
+
|
|
89
|
+
The only other active enrollment for client 575 was grouped enrollment 725. Successful individual pages can still report a total count of three, so the evidence points toward processing the returned grouped row rather than calculating the collection count. The failure does not require explicit pagination parameters.
|
|
90
|
+
|
|
91
|
+
The filter did not produce a usable response. This test cannot establish whether the server ignored the filter, applied it after the failing query, or handled it another way. Neither filtering nor skipping failed pages is a verified workaround. Page order can change, and skipping a page would omit existing enrollments.
|
|
92
|
+
|
|
93
|
+
## Separate observations
|
|
94
|
+
|
|
95
|
+
- Individual enrollment 680 for client 579 returned HTTP 400, `Non-static method requires a target.`, in both versions. Its client enrollment list succeeded. This is a separate complete-read failure, not the missing-column error. The [follow-up investigation](enrollment-complete-target-error.md) isolated missing DOB information through four new synthetic enrollments: DOB alone or either tested unknown/declined quality response cleared the error. It also documents working alternate reads. A historical observation of that message on enrollment 677 did not reproduce in this run; 677 returned HTTP 200 in both versions.
|
|
96
|
+
- Client 619 had zero active enrollments in the counted GWT read, and its REST enrollment collection returned HTTP 404 in both versions. This is an additional empty-collection behavior, separate from both errors above. It should not be interpreted as proof that the client is missing.
|
|
97
|
+
|
|
98
|
+
## Impact and SDK workaround
|
|
99
|
+
|
|
100
|
+
Applications cannot use these REST endpoints to read the affected household enrollments. A mixed client's full list can fail even when its individual enrollments are readable. V2 is not a fallback. Returning only successful pages would hide household participation and could undermine duplicate protection.
|
|
101
|
+
|
|
102
|
+
The shelter and enrollment workflows use authenticated, current UI/GWT reads and verify complete row counts. A failed REST read must never trigger a replacement enrollment write. The records remained present in GWT and the UI; this test found a read failure, not evidence of failed enrollment creation.
|
|
103
|
+
|
|
104
|
+
## Questions for WellSky
|
|
105
|
+
|
|
106
|
+
- Which grouped-enrollment query or projection references `v.entry_date_effective` at SQL position 116? Is the alias or column incorrect, or is the deployed sandbox view/schema out of sync with the API?
|
|
107
|
+
- Do V2 and V3 share this grouped-record query? The tested versions returned identical errors while individual records in the same client remained readable.
|
|
108
|
+
- Why does the null-group filter fail to isolate readable individual records? Is `$filter` supported on this endpoint before grouped-record expansion?
|
|
109
|
+
- Can a database schema failure return a server-error status with a support correlation ID rather than HTTP 400?
|
|
110
|
+
- Can the separate complete-read target error and empty-enrollment 404 convention be tracked independently?
|
|
111
|
+
|
|
112
|
+
Private evidence retains sanitized response statuses, timestamps, returned IDs, body hashes, and response-header metadata. No credentials, raw authenticated GWT payloads, or client demographics are included in this report. The comparison was observational; it did not add or remove household membership to prove causality, inspect the database, or test production.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Clarify household head coverage validation
|
|
2
|
+
|
|
3
|
+
[WellSky feedback](README.md) · [Enrollment contracts](../enrollments.md)
|
|
4
|
+
|
|
5
|
+
Status: a validation-policy question based on writes reproduced in a sandbox tenant on October 23, 2026. This report does not establish a violation of WellSky's documented rules or a universal program requirement.
|
|
6
|
+
|
|
7
|
+
## Observed behavior
|
|
8
|
+
|
|
9
|
+
A grouped household enrollment accepted an exit for the original head while a child remained enrolled. A subsequent member addition also succeeded after the original adults had exited, using an enrolled child as the operation's anchor. Stored enrollment dates and membership were confirmed through UI/GWT readback.
|
|
10
|
+
|
|
11
|
+
Those writes demonstrated transport behavior but produced invalid data under the SDK's required enrollment contract: the original head must remain enrolled through every member's exit. They are not supported workflow examples. The historical fixture was not repaired as part of the later validation change.
|
|
12
|
+
|
|
13
|
+
## SDK handling and controls
|
|
14
|
+
|
|
15
|
+
The SDK rejects a head exit that would leave another member enrolled afterward, rejects existing groups without the required head coverage, and does not implement retroactive head replacement. Choosing a non-head anchor does not relax those rules.
|
|
16
|
+
|
|
17
|
+
A separate October 23 acceptance run confirmed that an early head-only exit was rejected before any REST or GWT mutation. Valid controls then exited other members on separate dates and the original head last. A second household exited all selected members together. Stored dates, original head identity, and selected exit subgroups were verified; fresh-helper retries produced zero additional record writes.
|
|
18
|
+
|
|
19
|
+
## Questions for WellSky
|
|
20
|
+
|
|
21
|
+
- Which rules govern head coverage across linked enrollment intervals, and do they vary by program or configuration?
|
|
22
|
+
- Should the UI and server reject an exit or member addition that leaves members without an enrolled head? If that state is supported, what head-transfer workflow and history model should integrations use?
|
|
23
|
+
- Is there a supported validation or repair endpoint for existing groups with inconsistent head coverage?
|
|
24
|
+
- Can the public API expose the authoritative head identity and effective membership intervals needed to check these rules before a write?
|
|
25
|
+
|
|
26
|
+
Discuss the intended policy before classifying this as a server defect. Any new reproduction should use explicitly authorized synthetic records and a planned cleanup; do not recreate the invalid historical sequence in an existing household.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Write outcomes and update contracts need clarification
|
|
2
|
+
|
|
3
|
+
[WellSky feedback](README.md) · [Recovery](../troubleshooting.md)
|
|
4
|
+
|
|
5
|
+
Status: reproduced observations from a sandbox tenant on October 22–24, 2026. Some indicate a failure; others need a contract clarification before they can be called defects. No vendor acknowledgement or resolution is recorded here.
|
|
6
|
+
|
|
7
|
+
## A service persisted despite HTTP 400
|
|
8
|
+
|
|
9
|
+
During the October 24 run, a fixture had veteran status `Yes` rather than the UI's accepted `Yes (HUD)`. The service create returned HTTP 400 with `Requested value 'ENHANCEDYESNO_YES_NONHUD' was not found.` A subsequent service list also failed with that error.
|
|
10
|
+
|
|
11
|
+
After an explicit correction of the fixture's veteran status, a counted list showed the service already existed. There had been exactly one POST to `/services/create`. The SDK reconciled the original service key and journal, then completed the associated need without creating another service.
|
|
12
|
+
|
|
13
|
+
The SDK's veteran-label validation was subsequently corrected. That client-side fix reduces exposure to the trigger but does not establish rollback semantics for server errors. The observation proves persistence despite the failed response; it does not establish the server's internal transaction sequence.
|
|
14
|
+
|
|
15
|
+
**Ask WellSky:** Can validation occur before commit, or can an error after commit return the created identity and commit status? What recovery contract should callers use after a failed create? Is there a supported idempotency key or external-reference lookup?
|
|
16
|
+
|
|
17
|
+
Treat this as a high-priority integration issue because retrying a failed create can duplicate records. The SDK retains the attempted stage and reconciles before any further mutation.
|
|
18
|
+
|
|
19
|
+
## Omitting SSN from a client update cleared it
|
|
20
|
+
|
|
21
|
+
A raw middle-name-only update cleared the existing synthetic SSN when `socSecNo` was omitted. Other observed fields remained. The profile helper now reads and preserves all nine supported REST Client Record fields when making a change, then verifies the stored result.
|
|
22
|
+
|
|
23
|
+
The October 23 reproduction compared the existing Client Record with a raw REST update containing only the intended middle-name change. The later helper control preserved SSN and Sex while changing the middle name, with REST and UI readback. The observation is specific to the tested payload; it does not establish omission semantics for every field.
|
|
24
|
+
|
|
25
|
+
**Ask WellSky:** Is this endpoint a replacement operation or a partial update? What do omission, null, and empty string mean for each field? If clearing on omission is intended, can the contract require or clearly document the preserved fields?
|
|
26
|
+
|
|
27
|
+
This is an observed data-loss risk, but whether the server behavior violates the intended update contract remains a question. Do not assume PATCH semantics from a method named update.
|
|
28
|
+
|
|
29
|
+
## Goal updates require a field marked optional
|
|
30
|
+
|
|
31
|
+
The tested server rejected a goal update without `clientId`, although the pinned Swagger model marks that field optional. The SDK supplies and verifies the owning client.
|
|
32
|
+
|
|
33
|
+
For a support reproduction, compare otherwise identical updates to an authorized synthetic goal with and without its owning `clientId`. Record the validation response and stored values after each attempt. Keep this schema mismatch separate from the nested relationship question below.
|
|
34
|
+
|
|
35
|
+
**Ask WellSky:** Is `clientId` always required for updates? If so, can the schema and validation message reflect that requirement?
|
|
36
|
+
|
|
37
|
+
Goal responses also included unrelated `households` rows when the inspected goal had no household association. This is a separate response-integrity question. **Ask:** What relationship does that nested collection represent, and should it be scoped to the requested goal or client? The SDK does not use it to select write targets. The observation does not establish cross-tenant exposure.
|
|
38
|
+
|
|
39
|
+
## Filtering and response shapes vary by operation
|
|
40
|
+
|
|
41
|
+
| Observation | Current handling | Question for WellSky |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| Counted client candidate queries required `first_name`, `last_name`, and `soc_sec_no`; simplified camel-case forms could fail | Uses the verified field names and bounds candidate counts | Can accepted filter fields and endpoint-specific syntax be documented? |
|
|
44
|
+
| Counted Client Notes accepted `query=client_id eq …`; `$filter` failed HTTP 400 | Uses the operation's native query form | Is this intentional, and can the contract distinguish it from other collection endpoints? |
|
|
45
|
+
| ROI `$filter=client_id eq …` returned an empty success while the documented `query` form returned 404 in the checked case | Preserves operation-specific evidence; does not treat every 404 as empty | Which query form and no-results response are supported? |
|
|
46
|
+
| Complete-service reads returned an array despite the documented object shape | Normalizes supported collection shapes while preserving HTTP errors | Can the schema or response be aligned? |
|
|
47
|
+
| Follow-up fields were not available in the REST case-plan read model | Does not expose unverifiable follow-up writes through the durable helper | Which endpoint provides authoritative readback? |
|
|
48
|
+
|
|
49
|
+
These observations should be submitted with the exact API version, endpoint, sanitized request, and response shape from an authorized reproduction. They are not evidence that every endpoint supports the same query syntax or update behavior.
|
|
50
|
+
|
|
51
|
+
The mobile configuration routes are described by Swagger as internal WellSky endpoints. An HTTP 401 from a service account on those routes alone is not evidence of a public API authentication defect or a need for another SDK login flow.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Packaged examples
|
|
2
|
+
|
|
3
|
+
[Package overview](../README.md) · [Documentation index](../docs/README.md)
|
|
4
|
+
|
|
5
|
+
Every file here ships with the package and imports the public `@choiceapps/communityservices-sdk` entry points. No example loads `.env`, credentials, or a live session on import.
|
|
6
|
+
|
|
7
|
+
| File | Purpose | Execution |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| [shelter-inventory.mts](shelter-inventory.mts) | Discover unit lists and report beds, occupants, holds, overflow, and known or unknown vacancies | Exported functions; caller supplies a dedicated authenticated application page; each list is a separate snapshot |
|
|
10
|
+
| [shelters.mts](shelters.mts) | Shelter mode discovery, household check-in/checkout, durable source keys | Exported functions; caller supplies beds, dates, assessment preparation, UI, and store; exercised against installed package |
|
|
11
|
+
| [quickstart.mjs](quickstart.mjs) | Read a synthetic client through the real SDK transport | Run directly; no network or credentials |
|
|
12
|
+
| [durable-records.mts](durable-records.mts) | Client source mapping, household creation/later members, case plans, keyed goal notes, bulk recovery decisions | Exported functions; application supplies API, store, UI ports, and inputs |
|
|
13
|
+
| [usage.ts](usage.ts) | REST clients, pagination, generic service operations | Exported functions; raw writes have no durable journal |
|
|
14
|
+
| [verification-snapshot.ts](verification-snapshot.ts) | Read assessment records effective at enrollment | Exported function; caller supplies assessment context |
|
|
15
|
+
| [program-discovery.ts](program-discovery.ts) | Program and VA-entry assessment discovery | Exported function; authenticated page required |
|
|
16
|
+
| [enrollment-workflow.mts](enrollment-workflow.mts) | Single entry and interim assessment plans | Exported function; caller supplies plans and dependencies |
|
|
17
|
+
| [household-enrollment-workflow.mts](household-enrollment-workflow.mts) | Per-member enrollment and review | Existing household and per-member answer plans required |
|
|
18
|
+
| [household-lifecycle-workflow.mts](household-lifecycle-workflow.mts) | Selected exits and a later child entry | Household membership precedes program attachment |
|
|
19
|
+
| [household-exit-workflow.mts](household-exit-workflow.mts) | Household exit with destination details | Explicit exit dates and per-member plans required |
|
|
20
|
+
| [gwt-enrollment-session.ts](gwt-enrollment-session.ts) | Custom GWT session and journal integration | Caller supplies all target IDs and dates |
|
|
21
|
+
| [gwt-file-store.mjs](gwt-file-store.mjs) | Compatibility re-export of the durable file store | Safe to import |
|
|
22
|
+
|
|
23
|
+
After installation, run the offline example from your application directory:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
node node_modules/@choiceapps/communityservices-sdk/examples/quickstart.mjs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
To use a TypeScript integration example, copy it into your application's source tree and compile with TypeScript's NodeNext module resolution. Install `playwright-core` for the browser type contracts and `@types/node` as a development dependency when compiling them in your application. Keep ESM `.mts` examples as `.mts` or use an ESM package. The examples are ordinary packaged files, not additional package export paths.
|
|
30
|
+
|
|
31
|
+
## What the package check proves
|
|
32
|
+
|
|
33
|
+
`npm run package:check` in the SDK checkout packs and installs the tarball in a temporary consumer. It compares every installed example with the source file, copies the installed examples outside the package, compiles all TypeScript examples against installed declarations, and imports every compiled example with global network access blocked. The offline quickstart executes its assertions.
|
|
34
|
+
|
|
35
|
+
The durable-record example also runs against the installed SDK's fake REST transport and a real temporary file journal. Checks cover stable source IDs, household head guards, later members, Client Notes/goals/actions, same-key retries, explicit source-key conflict, and structured service recovery decisions. Synthetic UI ports are used where needed. This verifies package behavior and example integration; it does not log in, click a real UI, or replace the separate live sandbox acceptance tests.
|
|
36
|
+
|
|
37
|
+
Installed checks also run the client-creation, attachment-retry, session-authentication, independent-needs, and GWT mapped-reader regression suites against the tarball. Browser example compilation uses `skipLibCheck` for peer declarations while still checking example code against the installed SDK types. Separate consumer checks reject invalid veteran-status literals and verify the exported attachment-conflict contract.
|
|
38
|
+
|
|
39
|
+
`independent-needs.mts` discovers tenant-wide service types and separate provider quicklists, records explicit PIT needs, and maps a chosen taxonomy type to the service recorder. Dependencies, event keys, provider, and selections are supplied by the application.
|
|
40
|
+
|
|
41
|
+
[current-living-situation.mts](current-living-situation.mts) records application-supplied outreach or night-by-night shelter contact events with distinct durable keys and optional recording-program picklist selections. It compiles and imports against the installed package without opening a live session.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type {WellSkyClient, PicklistSelector} from '@choiceapps/communityservices-sdk';
|
|
2
|
+
import {createCurrentLivingSituationRecorder, type CurrentLivingSituationFields, type UiMutationStore} from '@choiceapps/communityservices-sdk/browser';
|
|
3
|
+
|
|
4
|
+
/** Both outreach and night-by-night shelter callers supply actual contact events. */
|
|
5
|
+
export async function recordMeaningfulContacts(
|
|
6
|
+
api: WellSkyClient,
|
|
7
|
+
store: UiMutationStore,
|
|
8
|
+
fields: CurrentLivingSituationFields,
|
|
9
|
+
target: {clientId: number; enrollmentId: number; assessmentCode: string},
|
|
10
|
+
contacts: readonly {eventKey: string; informationDate: string; livingSituation: PicklistSelector; recordingProgramOption?: PicklistSelector}[],
|
|
11
|
+
) {
|
|
12
|
+
const recorder = createCurrentLivingSituationRecorder({api, store, fields});
|
|
13
|
+
const receipts = [];
|
|
14
|
+
for (const contact of contacts) {
|
|
15
|
+
receipts.push(await recorder.record({
|
|
16
|
+
...target,
|
|
17
|
+
key: contact.eventKey,
|
|
18
|
+
informationDate: contact.informationDate,
|
|
19
|
+
livingSituation: {option: contact.livingSituation},
|
|
20
|
+
endDate: null,
|
|
21
|
+
...(contact.recordingProgramOption ? {verifiedBy: {option: contact.recordingProgramOption}} : {}),
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
// Each distinct event keeps its own row, even when date and answers match.
|
|
25
|
+
// Replay these same event keys with the same store to verify without another save.
|
|
26
|
+
return receipts;
|
|
27
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type {WellSkyClient} from '@choiceapps/communityservices-sdk';
|
|
2
|
+
import {
|
|
3
|
+
createClientCreator, createHouseholds, createCasePlans,
|
|
4
|
+
type UiMutationStore, type HouseholdUi, type CasePlanUi,
|
|
5
|
+
type ClientCreationInput, type HouseholdMemberInput,
|
|
6
|
+
type NoteValues, type GoalValues, type ActionValues,
|
|
7
|
+
type ClientWorkflows, type ServiceRecorder, type ServiceRecordInput,
|
|
8
|
+
} from '@choiceapps/communityservices-sdk/browser';
|
|
9
|
+
|
|
10
|
+
/** Application-owned dependencies. Use one persistent store across restarts and callers. */
|
|
11
|
+
export function durableRecords(api:WellSkyClient, store:UiMutationStore, ui:{households:HouseholdUi;casePlans:CasePlanUi}) {
|
|
12
|
+
return {
|
|
13
|
+
clients:createClientCreator({api,store}),
|
|
14
|
+
households:createHouseholds({api,store,ui:ui.households}),
|
|
15
|
+
casePlans:createCasePlans({api,store,ui:ui.casePlans}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export type DurableRecords = ReturnType<typeof durableRecords>;
|
|
19
|
+
|
|
20
|
+
/** These source IDs belong to your upstream records. Never regenerate them for a retry. */
|
|
21
|
+
export async function createPeopleAndHousehold(records:DurableRecords, input:{
|
|
22
|
+
householdSourceId:string;householdType:string;
|
|
23
|
+
head:ClientCreationInput;member:ClientCreationInput;
|
|
24
|
+
joinedAt:string;memberRelationship:string;
|
|
25
|
+
}) {
|
|
26
|
+
const head=await records.clients.create(input.head);
|
|
27
|
+
const member=await records.clients.create(input.member);
|
|
28
|
+
const household=await records.households.create({
|
|
29
|
+
key:input.householdSourceId,headClientId:head.clientId,householdType:input.householdType,
|
|
30
|
+
members:[
|
|
31
|
+
{clientId:head.clientId,relationship:'Self',dateJoined:input.joinedAt},
|
|
32
|
+
{clientId:member.clientId,relationship:input.memberRelationship,dateJoined:input.joinedAt},
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
return {head,member,household};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** This adds household membership. Attach the person to a program separately. */
|
|
39
|
+
export function addLaterMember(records:DurableRecords, input:{
|
|
40
|
+
key:string;householdId:number;headClientId:number;householdType:string;member:HouseholdMemberInput;
|
|
41
|
+
}) {
|
|
42
|
+
const {member,...context}=input;
|
|
43
|
+
return records.households.addMembers({...context,members:[member]});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** No automatic recovery by name or DOB. Call only after verifying the create receipt externally. */
|
|
47
|
+
export function resolveMissingClientReceipt(records:DurableRecords, original:ClientCreationInput, verifiedCreatedClientId:number) {
|
|
48
|
+
return records.clients.resolveCreated({...original,clientId:verifiedCreatedClientId});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Separate keys allow a partially completed sequence to resume. This is not atomic. */
|
|
52
|
+
export async function createCasePlan(records:DurableRecords, input:{
|
|
53
|
+
clientId:number;sourceId:string;note:NoteValues;goal:GoalValues;action:ActionValues;
|
|
54
|
+
}) {
|
|
55
|
+
const {clientId,sourceId}=input;
|
|
56
|
+
const note=await records.casePlans.createClientNote({key:`${sourceId}:note`,clientId,values:input.note});
|
|
57
|
+
const goal=await records.casePlans.createGoal({key:`${sourceId}:goal`,clientId,values:input.goal});
|
|
58
|
+
const action=await records.casePlans.createActionStep({key:`${sourceId}:action`,clientId,goalId:goal.recordId,values:input.action});
|
|
59
|
+
return {note,goal,action};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Require a source key even though the SDK keeps legacy unkeyed calls compatible. */
|
|
63
|
+
export function writeGoalProgress(workflows:ClientWorkflows, input:Parameters<ClientWorkflows['createCaseNote']>[0]&{key:string}) {
|
|
64
|
+
return workflows.createCaseNote(input);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Return diagnostics to the application. This example never retries a raw write. */
|
|
68
|
+
export async function recordServiceBatch(recorder:ServiceRecorder, rows:ServiceRecordInput[]) {
|
|
69
|
+
const batch=await recorder.recordBulk(rows);
|
|
70
|
+
const failures=batch.results.flatMap(row=>{
|
|
71
|
+
if(row.outcome!=='rejected')return [];
|
|
72
|
+
const nextAction=row.error.code==='SESSION_INTERRUPTED'
|
|
73
|
+
? row.error.reauthenticated?'reconcile-same-key':'authenticate-then-reconcile'
|
|
74
|
+
: 'review-saved-state';
|
|
75
|
+
return [{key:row.key,message:row.message,error:row.error,nextAction}];
|
|
76
|
+
});
|
|
77
|
+
return {...batch,failures};
|
|
78
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEnrollmentWorkflow,
|
|
3
|
+
type EnrollmentAssessmentPlan,
|
|
4
|
+
type EnrollmentWorkflowDiscovery,
|
|
5
|
+
type EnrollmentWorkflowResult,
|
|
6
|
+
type EnrollmentWorkflowOptions,
|
|
7
|
+
} from '@choiceapps/communityservices-sdk/browser';
|
|
8
|
+
|
|
9
|
+
/** Call with a REST client, authenticated discovery/enrollment adapters,
|
|
10
|
+
* a birthDates adapter for DOB plans, and the same durable store on every run. This example does not log in or run itself. */
|
|
11
|
+
export async function enrollAndReview(
|
|
12
|
+
dependencies:EnrollmentWorkflowOptions,
|
|
13
|
+
input:{
|
|
14
|
+
clientId:number;
|
|
15
|
+
providerId:number;
|
|
16
|
+
type:'HUD'|'VA';
|
|
17
|
+
/** Stable application record ID, not a new UUID on each retry. */
|
|
18
|
+
sourceId:string;
|
|
19
|
+
entryDate:string;
|
|
20
|
+
reviewDate:string;
|
|
21
|
+
buildEntryPlan:(catalog:EnrollmentWorkflowDiscovery)=>EnrollmentAssessmentPlan;
|
|
22
|
+
buildInterimPlan:(entry:EnrollmentWorkflowResult,catalog:EnrollmentWorkflowDiscovery)=>EnrollmentAssessmentPlan;
|
|
23
|
+
},
|
|
24
|
+
) {
|
|
25
|
+
const workflow=createEnrollmentWorkflow(dependencies);
|
|
26
|
+
const context={clientId:input.clientId,providerId:input.providerId,type:input.type,entryDate:input.entryDate};
|
|
27
|
+
const catalog=await workflow.discover(context);
|
|
28
|
+
// Render catalog.entry/interim.definition in the application's intake form.
|
|
29
|
+
// Plans must explicitly cover applicable fields and all required verification types.
|
|
30
|
+
const enrollmentKey=`${input.sourceId}:entry`;
|
|
31
|
+
const entry=await workflow.createEnrollment({
|
|
32
|
+
...context,key:enrollmentKey,assessment:input.buildEntryPlan(catalog),
|
|
33
|
+
});
|
|
34
|
+
// Use entry.recordsetIds and the discovered startDateField/endDateField names
|
|
35
|
+
// when ending a changed verification row. Carry forward
|
|
36
|
+
// unchanged groups instead of adding copies. End the old row on the prior day.
|
|
37
|
+
// On a rejected operation, call workflow.getStatus({clientId, key}) with that
|
|
38
|
+
// operation's original key. Inspect the attempted steps before retrying the
|
|
39
|
+
// same intent. Never remove the journal or generate a fresh key to bypass it.
|
|
40
|
+
const interim=await workflow.createInterim({
|
|
41
|
+
key:`${input.sourceId}:interim:${input.reviewDate}`,enrollmentKey,
|
|
42
|
+
clientId:input.clientId,reviewDate:input.reviewDate,
|
|
43
|
+
assessment:input.buildInterimPlan(entry,catalog),
|
|
44
|
+
});
|
|
45
|
+
return {entry,interim};
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { GwtEnrollmentHelpers, type GwtEnrollmentSession, type GwtMutationStore } from "@choiceapps/communityservices-sdk";
|
|
2
|
+
|
|
3
|
+
/** Supply a trusted authenticated browser adapter and a shared durable journal.
|
|
4
|
+
* prepare() must capture/serialize inside this call and prevent the original UI request from being sent.
|
|
5
|
+
* readEnrollment() must return complete fresh GWT state, including every review and household member.
|
|
6
|
+
* See docs/browser-session.md for the adapter contract. */
|
|
7
|
+
export async function updateSyntheticHousehold(session: GwtEnrollmentSession, store: GwtMutationStore, input: {tenant:string;moduleBaseUrl:string;clientId:number;enrollmentId:number;memberClientIds:number[];reviewDate:string}) {
|
|
8
|
+
const helpers = new GwtEnrollmentHelpers({
|
|
9
|
+
tenant: input.tenant,
|
|
10
|
+
moduleBaseUrl: input.moduleBaseUrl,
|
|
11
|
+
session,
|
|
12
|
+
store,
|
|
13
|
+
});
|
|
14
|
+
const membership = await helpers.addHouseholdMembers({ clientId: input.clientId, enrollmentId: input.enrollmentId, memberClientIds: input.memberClientIds });
|
|
15
|
+
const interim = await helpers.createInterimUpdate({ clientId: input.clientId, enrollmentId: input.enrollmentId, clientIds: [input.clientId, ...input.memberClientIds], reviewDate: input.reviewDate });
|
|
16
|
+
return { membership, interim }; // Reuse the same target, dates and journal when retrying.
|
|
17
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type {WellSkyClient} from '@choiceapps/communityservices-sdk';
|
|
2
|
+
import {
|
|
3
|
+
createDateOfBirth,createDateOfBirthUi,createEnrollmentWorkflow,createProgramDiscovery,
|
|
4
|
+
type EnrollmentAssessmentPlan,type EnrollmentWorkflowDiscovery,
|
|
5
|
+
type HouseholdEnrollmentResult,type EnrollmentReader,type UiMutationStore,
|
|
6
|
+
} from '@choiceapps/communityservices-sdk/browser';
|
|
7
|
+
import type {GwtEnrollmentHelpers} from '@choiceapps/communityservices-sdk';
|
|
8
|
+
import type {Page} from 'playwright-core';
|
|
9
|
+
|
|
10
|
+
/** The application owns answers and applicability for each person. Reuse the same
|
|
11
|
+
* source keys and durable store when resuming after an interruption. */
|
|
12
|
+
export async function enrollHouseholdAndReview(options:{
|
|
13
|
+
api:WellSkyClient;
|
|
14
|
+
page:Page;
|
|
15
|
+
helpers:GwtEnrollmentHelpers;
|
|
16
|
+
enrollmentSession:EnrollmentReader;
|
|
17
|
+
store:UiMutationStore;
|
|
18
|
+
sourceEnrollmentId:string;
|
|
19
|
+
sourceReviewId:string;
|
|
20
|
+
householdId:number;
|
|
21
|
+
headClientId:number;
|
|
22
|
+
clientIds:number[];
|
|
23
|
+
providerId:number;
|
|
24
|
+
entryDate:string;
|
|
25
|
+
reviewDate:string;
|
|
26
|
+
buildEntryPlan:(clientId:number,definition:EnrollmentWorkflowDiscovery)=>EnrollmentAssessmentPlan;
|
|
27
|
+
buildInterimPlan:(clientId:number,definition:EnrollmentWorkflowDiscovery,entry:HouseholdEnrollmentResult)=>EnrollmentAssessmentPlan;
|
|
28
|
+
}){
|
|
29
|
+
const workflow=createEnrollmentWorkflow({...options,birthDates:createDateOfBirth({api:options.api,ui:createDateOfBirthUi({page:options.page}),store:options.store}),discovery:createProgramDiscovery({page:options.page})});
|
|
30
|
+
const definitions=new Map<number,EnrollmentWorkflowDiscovery>();
|
|
31
|
+
for(const clientId of options.clientIds)definitions.set(clientId,await workflow.discover({clientId,providerId:options.providerId,type:'HUD',entryDate:options.entryDate}));
|
|
32
|
+
const definitionFor=(clientId:number)=>{const value=definitions.get(clientId);if(!value)throw Error('Member definition missing');return value;};
|
|
33
|
+
const entry=await workflow.createHouseholdEnrollment({
|
|
34
|
+
key:options.sourceEnrollmentId,householdId:options.householdId,headClientId:options.headClientId,
|
|
35
|
+
providerId:options.providerId,type:'HUD',entryDate:options.entryDate,
|
|
36
|
+
members:options.clientIds.map(clientId=>({clientId,assessment:options.buildEntryPlan(clientId,definitionFor(clientId))})),
|
|
37
|
+
});
|
|
38
|
+
const interim=await workflow.createHouseholdInterim({
|
|
39
|
+
key:options.sourceReviewId,enrollmentKey:options.sourceEnrollmentId,headClientId:options.headClientId,reviewDate:options.reviewDate,
|
|
40
|
+
members:options.clientIds.map(clientId=>({clientId,assessment:options.buildInterimPlan(clientId,definitionFor(clientId),entry)})),
|
|
41
|
+
});
|
|
42
|
+
return {entry,interim};
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type {WellSkyClient,GwtEnrollmentHelpers} from '@choiceapps/communityservices-sdk';
|
|
2
|
+
import type {Page} from 'playwright-core';
|
|
3
|
+
import {
|
|
4
|
+
createDateOfBirth,createDateOfBirthUi,createEnrollmentWorkflow,createProgramDiscovery,createHouseholdExitUi,
|
|
5
|
+
type EnrollmentReader,type UiMutationStore,type ExitWorkflowDiscovery,
|
|
6
|
+
type EnrollmentAssessmentPlan,type HouseholdExitDetailInput,
|
|
7
|
+
} from '@choiceapps/communityservices-sdk/browser';
|
|
8
|
+
|
|
9
|
+
/** Continue a verified household enrollment in the same durable workflow store.
|
|
10
|
+
* Application policy chooses each member's answers and the records that changed. */
|
|
11
|
+
export async function exitHousehold(options:HouseholdExitDetailInput&{
|
|
12
|
+
api:WellSkyClient;page:Page;helpers:GwtEnrollmentHelpers;
|
|
13
|
+
enrollmentSession:EnrollmentReader;store:UiMutationStore;
|
|
14
|
+
sourceExitId:string;sourceEnrollmentId:string;headClientId:number;
|
|
15
|
+
clientIds:number[];providerId:number;exitDate:string;
|
|
16
|
+
destination:string;reasonForLeaving:string;
|
|
17
|
+
buildExitPlan:(clientId:number,definition:ExitWorkflowDiscovery)=>EnrollmentAssessmentPlan;
|
|
18
|
+
}){
|
|
19
|
+
const workflow=createEnrollmentWorkflow({
|
|
20
|
+
...options,birthDates:createDateOfBirth({api:options.api,ui:createDateOfBirthUi({page:options.page}),store:options.store}),discovery:createProgramDiscovery({page:options.page}),
|
|
21
|
+
exitUi:createHouseholdExitUi({page:options.page}),
|
|
22
|
+
});
|
|
23
|
+
const members=[];
|
|
24
|
+
for(const clientId of options.clientIds){
|
|
25
|
+
const definition=await workflow.discoverExit({clientId,providerId:options.providerId,type:'HUD',entryDate:options.exitDate});
|
|
26
|
+
members.push({clientId,assessment:options.buildExitPlan(clientId,definition)});
|
|
27
|
+
}
|
|
28
|
+
return workflow.exitHousehold({
|
|
29
|
+
key:options.sourceExitId,enrollmentKey:options.sourceEnrollmentId,
|
|
30
|
+
headClientId:options.headClientId,exitDate:options.exitDate,
|
|
31
|
+
destination:options.destination,reasonForLeaving:options.reasonForLeaving,members,
|
|
32
|
+
...(options.destinationDetails?{destinationDetails:options.destinationDetails}:{}),
|
|
33
|
+
...(options.reasonForLeavingOther?{reasonForLeavingOther:options.reasonForLeavingOther}:{}),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type {WellSkyClient,GwtEnrollmentHelpers} from '@choiceapps/communityservices-sdk';
|
|
2
|
+
import type {Page} from 'playwright-core';
|
|
3
|
+
import {
|
|
4
|
+
createDateOfBirth,createDateOfBirthUi,createEnrollmentWorkflow,createProgramDiscovery,createHouseholdExitUi,
|
|
5
|
+
createMemberEntryDatePort,
|
|
6
|
+
type EnrollmentReader,type UiMutationStore,type EnrollmentAssessmentPlan,
|
|
7
|
+
} from '@choiceapps/communityservices-sdk/browser';
|
|
8
|
+
|
|
9
|
+
/** All members must already belong to the household. The new child's joined date
|
|
10
|
+
* must be on or before their own entry. Application policy builds age-appropriate plans. */
|
|
11
|
+
export async function householdLifecycle(options:{
|
|
12
|
+
api:WellSkyClient;page:Page;helpers:GwtEnrollmentHelpers;enrollmentSession:EnrollmentReader;store:UiMutationStore;
|
|
13
|
+
sourceEnrollmentKey:string;headClientId:number;departingMemberId:number;childClientId:number;
|
|
14
|
+
memberExitDate:string;childEntryDate:string;sourceExitKey:string;sourceChildEntryKey:string;
|
|
15
|
+
memberExitPlan:EnrollmentAssessmentPlan;childEntryPlan:EnrollmentAssessmentPlan;
|
|
16
|
+
}){
|
|
17
|
+
const workflow=createEnrollmentWorkflow({
|
|
18
|
+
...options,birthDates:createDateOfBirth({api:options.api,ui:createDateOfBirthUi({page:options.page}),store:options.store}),discovery:createProgramDiscovery({page:options.page}),
|
|
19
|
+
exitUi:createHouseholdExitUi({page:options.page}),
|
|
20
|
+
memberEntryDatePort:createMemberEntryDatePort(options),
|
|
21
|
+
});
|
|
22
|
+
const exit=await workflow.exitHousehold({
|
|
23
|
+
key:options.sourceExitKey,enrollmentKey:options.sourceEnrollmentKey,headClientId:options.headClientId,
|
|
24
|
+
exitDate:options.memberExitDate,destination:'Rental by client, no ongoing housing subsidy',reasonForLeaving:'Completed program',
|
|
25
|
+
members:[{clientId:options.departingMemberId,assessment:options.memberExitPlan}],
|
|
26
|
+
});
|
|
27
|
+
const child=await workflow.addHouseholdMembers({
|
|
28
|
+
key:options.sourceChildEntryKey,enrollmentKey:options.sourceEnrollmentKey,headClientId:options.headClientId,
|
|
29
|
+
entryDate:options.childEntryDate,members:[{clientId:options.childClientId,assessment:options.childEntryPlan}],
|
|
30
|
+
});
|
|
31
|
+
// Later child interims/exits still use sourceEnrollmentKey. The workflow resolves
|
|
32
|
+
// the child's own entry key and date from the durable household journal.
|
|
33
|
+
return {exit,child};
|
|
34
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createNeedRecorder, type IndependentNeedUi, type UiMutationStore,
|
|
3
|
+
type NeedUi, type IndependentNeedInput, type ServiceRecorder,
|
|
4
|
+
type ServiceRecordInput, type ServiceTypeOption,
|
|
5
|
+
} from '@choiceapps/communityservices-sdk/browser';
|
|
6
|
+
|
|
7
|
+
/** Pass createNeedUi({page: session.page}) from a managed browser session. */
|
|
8
|
+
export function independentNeeds(ui:IndependentNeedUi,store:UiMutationStore,tenant:string){
|
|
9
|
+
return createNeedRecorder({ui,store,tenant});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Cache once per tenant, with expiry. clientId is only browser navigation context. */
|
|
13
|
+
export function discoverServiceTypes(ui:NeedUi,context:{clientId:number}){
|
|
14
|
+
return ui.listServiceTypes(context);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Cache per tenant and provider. This does not reread the full taxonomy. */
|
|
18
|
+
export function discoverServiceQuicklist(ui:NeedUi,context:{clientId:number;providerId:number}){
|
|
19
|
+
return ui.listServiceQuicklist(context);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Explicit status/outcome: a PIT need does not imply delivery of a service. */
|
|
23
|
+
export function recordPointInTimeNeed(ui:IndependentNeedUi,store:UiMutationStore,input:IndependentNeedInput,tenant:string){
|
|
24
|
+
return independentNeeds(ui,store,tenant).record(input);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Services accept the discovered taxonomy string; independent needs accept serviceCodeId. */
|
|
28
|
+
export function recordChosenService(recorder:ServiceRecorder,type:ServiceTypeOption,input:Omit<ServiceRecordInput,'body'>&{body:Omit<ServiceRecordInput['body'],'serviceCode'>}){
|
|
29
|
+
return recorder.record({...input,body:{...input.body,serviceCode:type.code}});
|
|
30
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {WellSkyClient, readProgramCatalog, flattenQuestions} from '@choiceapps/communityservices-sdk';
|
|
2
|
+
import {createProgramDiscovery} from '@choiceapps/communityservices-sdk/browser';
|
|
3
|
+
import type {Page} from 'playwright-core';
|
|
4
|
+
|
|
5
|
+
/** Supply an authenticated server-side API client and a dedicated authenticated UI page.
|
|
6
|
+
* Keep browser credentials/session cookies on the server, never in a consuming web app. */
|
|
7
|
+
export async function getProgramAssessmentChoices(api:WellSkyClient, page:Page, providerId:number, metadataClientId:number) {
|
|
8
|
+
const catalog=await readProgramCatalog(api);
|
|
9
|
+
const provider=catalog.providers.find(item=>item.providerId===providerId);
|
|
10
|
+
if(!provider)throw Error('Provider is not active and operational in the API catalog');
|
|
11
|
+
const discovery=createProgramDiscovery({page});
|
|
12
|
+
const configuration=await discovery.getProviderAssessments({providerId});
|
|
13
|
+
const entry=configuration.defaults.find(item=>item.location==='showVaEntry');
|
|
14
|
+
if(entry?.resolution!=='resolved'||!entry.assessment)throw Error('VA entry assessment is not uniquely resolved');
|
|
15
|
+
// REST metadata currently requires a permitted client context even for definitions.
|
|
16
|
+
// Its result strips answers/recordset IDs. It is not a patient/client export.
|
|
17
|
+
const definition=await api.assessments.getDefinition({clientId:metadataClientId,assessmentCode:entry.assessment.code});
|
|
18
|
+
return {provider,configuration,definition,questions:flattenQuestions(definition.questions)};
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {createFakeWellSkyClient} from '@choiceapps/communityservices-sdk/testing';
|
|
3
|
+
import {toItems} from '@choiceapps/communityservices-sdk';
|
|
4
|
+
|
|
5
|
+
const client = createFakeWellSkyClient();
|
|
6
|
+
client.fake.set('GET /clients/{clientId}', {
|
|
7
|
+
kind: 'reply', body: [{clientId: 123, firstName: 'Synthetic'}],
|
|
8
|
+
});
|
|
9
|
+
const people = toItems(await client.clients.get({path: {clientId: 123}}));
|
|
10
|
+
assert.equal(people.length, 1);
|
|
11
|
+
assert.equal(people[0].clientId, 123);
|
|
12
|
+
assert.equal(client.fake.calls.length, 1);
|
|
13
|
+
console.log('Read synthetic client 123; no network requests.');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
import { createShelterInventory, type ShelterInventorySnapshot, type ShelterStay } from '@choiceapps/communityservices-sdk/browser';
|
|
3
|
+
|
|
4
|
+
/** Uses the caller's dedicated authenticated application page. No credential loading. */
|
|
5
|
+
export async function reportShelterInventory(page: Page, providerId: number) {
|
|
6
|
+
const inventory = createShelterInventory({ page });
|
|
7
|
+
const catalog = await inventory.listUnitLists({ providerId });
|
|
8
|
+
// Each list has its own observation interval. The provider report is not atomic.
|
|
9
|
+
const snapshots: ShelterInventorySnapshot[] = [];
|
|
10
|
+
for (const { unitListId } of catalog.unitLists) {
|
|
11
|
+
snapshots.push(await inventory.getInventory({ providerId, unitListId }));
|
|
12
|
+
}
|
|
13
|
+
return { catalog, snapshots };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function vacancyReport(snapshot: ShelterInventorySnapshot) {
|
|
17
|
+
if (snapshot.availability.status === 'unknown') {
|
|
18
|
+
return { status: 'unknown', reasons: snapshot.availability.reasons };
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
status: 'known',
|
|
22
|
+
observedAt: snapshot.freshness.observedAt,
|
|
23
|
+
count: snapshot.availability.vacantCount,
|
|
24
|
+
beds: snapshot.beds.filter(bed => bed.status === 'vacant'),
|
|
25
|
+
};
|
|
26
|
+
// A later check-in must recheck availability. These keys are not server bed IDs.
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Null is a valid group for a single-person stay, including installed declarations.
|
|
30
|
+
export const singlePersonGroup: ShelterStay['groupId'] = null;
|