@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,156 @@
|
|
|
1
|
+
# Create clients, households, and case plans safely
|
|
2
|
+
|
|
3
|
+
[Documentation index](README.md) · [Typed integration example](../examples/durable-records.mts) · [Recovery](troubleshooting.md)
|
|
4
|
+
|
|
5
|
+
These helpers require a V3 `WellSkyClient` configured for the target tenant. The browser adapters require a dedicated authenticated page. All snippets below are integration fragments: your application supplies the IDs, source keys, dates, and values. No example logs in or writes on import.
|
|
6
|
+
|
|
7
|
+
## Set up the helpers
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import {
|
|
11
|
+
createFileGwtMutationStore, createClientCreator,
|
|
12
|
+
createHouseholds, createHouseholdUi,
|
|
13
|
+
createCasePlans, createCasePlanUi,
|
|
14
|
+
} from '@choiceapps/communityservices-sdk/browser';
|
|
15
|
+
|
|
16
|
+
const store = createFileGwtMutationStore(journalDirectory);
|
|
17
|
+
const clients = createClientCreator({api, store});
|
|
18
|
+
const households = createHouseholds({
|
|
19
|
+
api, store, ui: createHouseholdUi({page: session.page}),
|
|
20
|
+
});
|
|
21
|
+
const casePlans = createCasePlans({
|
|
22
|
+
api, store, ui: createCasePlanUi({page: session.page}),
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use the same private journal directory on every retry and process restart. The file store coordinates one host. Multiple hosts need a shared durable locking implementation. These operations are not atomic across entities, and external writers do not use the SDK's lock.
|
|
27
|
+
|
|
28
|
+
## Map a source person to a client
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const original = {
|
|
32
|
+
sourceId: sourcePersonId,
|
|
33
|
+
body: {firstName, lastName, dateEffective: effectiveInstant},
|
|
34
|
+
};
|
|
35
|
+
const created = await clients.create(original);
|
|
36
|
+
const mapping = await clients.lookup(sourcePersonId);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`create` records the server-returned client ID and verifies the supported identity fields. An identical source-ID retry returns that ID without another create. A changed creation intent under the same source raises `SOURCE_ID_CONFLICT`. To edit a person later, use the [profile helpers](profiles.md), not a revised creation request. After a legitimate profile edit, `lookup` retrieves the mapping; replaying the old create can reject the changed fields.
|
|
40
|
+
|
|
41
|
+
Name and SSN candidate matches raise `POSSIBLE_DUPLICATE` with `candidateClientIds`. They never select, link, or merge a person automatically. After reviewing those candidates, an application can explicitly create a distinct person with `allowDistinctClient: true`. Retain that flag with the original intent on retries. The helper does not perform demographic deduplication or fuzzy matching.
|
|
42
|
+
|
|
43
|
+
If the create response is lost before an ID is durably saved, `CLIENT_CREATE_UNCERTAIN` blocks another create. Verify the server's creation receipt or the target record independently. Only then call:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
const recovered = await clients.resolveCreated({
|
|
47
|
+
...original, clientId: verifiedCreatedClientId,
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This records an explicitly verified mapping and performs no server write. It does not find the client for you. Never obtain the ID by automatically choosing a name or DOB match. An ID already mapped to another source is rejected.
|
|
52
|
+
|
|
53
|
+
Supported creation fields are names, suffix, alias, name quality, SSN and its quality, veteran status, and the explicit effective instant. Use the [DOB helper](profiles.md#birth-dates) for calendar birth dates and the profile/assessment helpers for other fields. Unsupported fields are rejected. The exact input and result contracts are in [client-creation.d.mts](../browser/client-creation.d.mts).
|
|
54
|
+
|
|
55
|
+
For `veteranStatus`, omit the field or supply exactly `Yes (HUD)`, `No (HUD)`, `Client doesn't know (HUD)`, `Client prefers not to answer (HUD)`, or `Data not collected (HUD)`. Other values, including `Yes`, `No`, and an empty string, raise `INVALID_VETERAN_STATUS` before API access or journal reservation. The helper does not translate aliases. An invalid label accepted by REST can later break service reads or return an error after a service has already been created. This validation does not repair older client records or journals. Review those records and retain the original journal rather than changing a saved creation intent to bypass a conflict. Profile updates validate veteran status against the current UI choices.
|
|
56
|
+
|
|
57
|
+
## Create and manage a household
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const createdHousehold = await households.create({
|
|
61
|
+
key: sourceHouseholdId,
|
|
62
|
+
headClientId,
|
|
63
|
+
householdType,
|
|
64
|
+
members: [
|
|
65
|
+
{clientId: headClientId, relationship: 'Self', dateJoined: joinedAt},
|
|
66
|
+
{clientId: memberClientId, relationship: memberRelationship, dateJoined: joinedAt},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
const householdId = createdHousehold.household.householdId;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`getHouseholdOptions()` returns the verified sandbox labels and their verification date. It is a dated snapshot, not a live configuration query. Use one head with relationship `Self`, unique member IDs, and timezone-qualified join dates. The head's join date must cover every member's membership. Existing invalid head state is a data error and blocks mutation.
|
|
73
|
+
|
|
74
|
+
Add a child or another person with their own later join date:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
await households.addMembers({
|
|
78
|
+
key: sourceMembershipId, householdId, headClientId, householdType,
|
|
79
|
+
members: [{clientId: childClientId, relationship: 'Daughter', dateJoined: childJoinedAt}],
|
|
80
|
+
});
|
|
81
|
+
await households.updateRelationships({
|
|
82
|
+
key: sourceRelationshipChangeId, householdId, headClientId,
|
|
83
|
+
members: [{clientId: memberClientId, relationship: 'Significant other'}],
|
|
84
|
+
});
|
|
85
|
+
await households.endMembers({
|
|
86
|
+
key: sourceDepartureId, householdId, headClientId,
|
|
87
|
+
clientIds: [memberClientId], dateEnded: memberLeftAt,
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The helper preserves unrelated members, previous join dates, and ended records. Existing membership cannot be silently replaced or rejoined. The head cannot leave an active household, be demoted, or be replaced through these methods. Household closure and retroactive head corrections need separate workflows.
|
|
92
|
+
|
|
93
|
+
Household membership is separate from program enrollment. After creating the household, pass its ID to the [household enrollment workflow](enrollments.md). A later child must first belong to the household, then be attached to the program with their own entry date and assessment. Ending household membership does not exit a program. Program exits continue to use the enrollment head-coverage guards.
|
|
94
|
+
|
|
95
|
+
Creation and changes reconcile exact saved state after uncertain responses. Completed retries also recognize subsequent verified changes in the same journal. External drift and unresolved attempts block writes. See [households.d.mts](../browser/households.d.mts) for all methods.
|
|
96
|
+
|
|
97
|
+
## Create and update Client Notes, goals, and action steps
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
const note = await casePlans.createClientNote({
|
|
101
|
+
key: sourceClientNoteId, clientId,
|
|
102
|
+
values: {note_date: noteInstant, note: clientNoteText},
|
|
103
|
+
});
|
|
104
|
+
const goal = await casePlans.createGoal({
|
|
105
|
+
key: sourceGoalId, clientId,
|
|
106
|
+
values: {
|
|
107
|
+
providerId, dateGoalsSet: goalInstant, classification: 'Case Plan',
|
|
108
|
+
type: 'Develop a Case Plan', overallStatus: 'In Progress',
|
|
109
|
+
goalDescription,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const action = await casePlans.createActionStep({
|
|
113
|
+
key: sourceActionId, clientId, goalId: goal.recordId,
|
|
114
|
+
values: {providerId, dateSet: actionInstant, actionStep, status: 'In Progress'},
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Choose classification, type, and status values from the actual tenant form. The strings above are verified sandbox examples, not universal options. Each operation checks ownership and verifies its supported fields after saving. A sequence can stop partway through. Reuse every original key when resuming; the [typed example](../examples/durable-records.mts) gives each component its own stable suffix.
|
|
119
|
+
|
|
120
|
+
Use a new source key for an intentional update:
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
await casePlans.updateClientNote({
|
|
124
|
+
key: sourceNoteRevisionId, clientId, recordId: note.recordId,
|
|
125
|
+
values: {note: revisedText},
|
|
126
|
+
});
|
|
127
|
+
await casePlans.updateGoal({
|
|
128
|
+
key: sourceGoalRevisionId, clientId, recordId: goal.recordId,
|
|
129
|
+
values: {goalDescription: revisedDescription, percentComplete: '50%'},
|
|
130
|
+
});
|
|
131
|
+
await casePlans.updateActionStep({
|
|
132
|
+
key: sourceActionRevisionId, clientId, goalId: goal.recordId,
|
|
133
|
+
recordId: action.recordId, values: {actionStep: revisedAction},
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Update requests preserve the helper's supported unmodified fields. Unreadable follow-up fields are not exposed as writable helper fields. Percent complete uses UI labels `10%` through `90%`, or an empty string; numeric string `50` is invalid. Consult [case-plans.d.mts](../browser/case-plans.d.mts) for field coverage. Initial empty REST results need fresh UI confirmation. Failed or incomplete reads never authorize a repeat write.
|
|
138
|
+
|
|
139
|
+
Client Notes differ from goal case notes. The latter use the Goal's **Add Case Note** button or the Notes-column paper icon. Never use the unrelated `households` array observed in REST goal responses to select note participants.
|
|
140
|
+
|
|
141
|
+
## Give goal case notes a source key
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
await session.workflows.createCaseNote({
|
|
145
|
+
key: sourceGoalNoteId, clientId, goalId, providerId,
|
|
146
|
+
noteDate: localCalendarDate, note: goalNoteText,
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
A keyed retry must retain the same client, goal, provider, calendar date, and text. Changed intent raises `CASE_NOTE_SOURCE_CONFLICT` before browser access. The journal also verifies the saved note ID. The key is optional only for compatibility: unkeyed calls retain text/date/provider matching. Identical existing notes can be reconciled; a new source key is not a command to force a duplicate. Notes are single-client operations.
|
|
151
|
+
|
|
152
|
+
## Return service diagnostics to the application
|
|
153
|
+
|
|
154
|
+
Rejected `recordBulk` rows include `message` and structured `error` metadata. Fields include `code`, `kind`, `stage`, `status`, `reauthenticated`, and bounded causes when available. These are optional because some failures are ordinary JavaScript errors. The [typed example](../examples/durable-records.mts) retains them and returns an application action without replaying any write.
|
|
155
|
+
|
|
156
|
+
For `SESSION_INTERRUPTED`, check `reauthenticated`. Resume with the same row keys and journal only after authentication is ready. Other failures need saved-state review. Keep fulfilled and skipped rows in the application's report. A successful REST create may still need UI completion and Closed/Fully Met need verification. See [services](services.md) and [recovery](troubleshooting.md).
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Discover programs and manage enrollments
|
|
2
|
+
|
|
3
|
+
[Documentation index](README.md) · [Agent guide](../AGENTS.md)
|
|
4
|
+
|
|
5
|
+
The snippets are integration fragments. Your application supplies existing client and household IDs, stable source keys, and assessment plans. Use the [durable client and household helpers](durable-records.md) before enrolling new people. Client identity decisions and household membership remain separate from program participation.
|
|
6
|
+
|
|
7
|
+
## Programs, assessments and form definitions
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import {readProgramCatalog, flattenQuestions} from '@choiceapps/communityservices-sdk';
|
|
11
|
+
import {createProgramDiscovery} from '@choiceapps/communityservices-sdk/browser';
|
|
12
|
+
const catalog = await readProgramCatalog(api); // Counted REST pages, active/operational by default.
|
|
13
|
+
const discovery = createProgramDiscovery({page: session.page});
|
|
14
|
+
const configuration = await discovery.getProviderAssessments({providerId});
|
|
15
|
+
const entry = configuration.defaults.find(d => d.location === 'showVaEntry');
|
|
16
|
+
if (entry?.resolution !== 'resolved') throw new Error('Entry assessment not resolved');
|
|
17
|
+
const definition = await api.assessments.getDefinition({
|
|
18
|
+
clientId:metadataClientId, assessmentCode:entry.assessment.code,
|
|
19
|
+
});
|
|
20
|
+
const questions = flattenQuestions(definition.questions); // Includes subassessment children.
|
|
21
|
+
const picklists = await api.picklists.list({clientId:metadataClientId, assessmentCode:entry.assessment.code});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Provider lists include agencies and parent organizations as well as programs. Listing a provider does not establish permission to enroll or provide services. Swagger lacks provider-to-assessment assignments, so the browser adapter reads Provider Admin display settings and the published catalog. It distinguishes assessment-tab assignment from entry/interim/exit defaults and leaves ambiguous names unresolved. `listAssessments()` returns the published top-level catalog. Browser discovery requires administrative read access and shares the page lock with the other helpers.
|
|
25
|
+
|
|
26
|
+
REST form metadata includes question paths, mandatory flags, current option IDs/codes/names, and nested subassessment definitions without recorded client answers. It requires a permitted client context and does not reproduce every UI conditional rule or historical picklist version. See [program discovery findings](capabilities.md) and [example](../examples/program-discovery.ts).
|
|
27
|
+
|
|
28
|
+
## Enrollment workflow from program discovery
|
|
29
|
+
|
|
30
|
+
`createEnrollmentWorkflow` composes the provider catalog, current program assessment defaults, question/picklist metadata, REST enrollment and answer writes, and the durable GWT interim helper. It accepts an **existing client ID** and supports HUD and VA program defaults. It does not create clients or infer program eligibility, answers, conditional applicability, or which verification types a program requires.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import {
|
|
34
|
+
openEnrollmentSession, createProgramDiscovery,
|
|
35
|
+
createEnrollmentWorkflow, createFileGwtMutationStore,
|
|
36
|
+
createDateOfBirth, createDateOfBirthUi,
|
|
37
|
+
} from '@choiceapps/communityservices-sdk/browser';
|
|
38
|
+
|
|
39
|
+
const store = createFileGwtMutationStore('/private/application/hmis-journal');
|
|
40
|
+
const session = await openEnrollmentSession({ chromium, store });
|
|
41
|
+
try {
|
|
42
|
+
const workflow = createEnrollmentWorkflow({
|
|
43
|
+
api, // WellSkyClient with the deployment's explicitly measured mobile date correction.
|
|
44
|
+
discovery: createProgramDiscovery({ page: session.page }),
|
|
45
|
+
helpers: session.helpers,
|
|
46
|
+
store,
|
|
47
|
+
birthDates: createDateOfBirth({
|
|
48
|
+
api, store, ui: createDateOfBirthUi({page: session.page}),
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
const context = { clientId, providerId, type: 'HUD' as const, entryDate };
|
|
52
|
+
const catalog = await workflow.discover(context);
|
|
53
|
+
// Render catalog.entry.definition and catalog.interim.definition in the application.
|
|
54
|
+
const entry = await workflow.createEnrollment({
|
|
55
|
+
...context, key: `${sourceEnrollmentId}:entry`, assessment: entryPlan,
|
|
56
|
+
});
|
|
57
|
+
const interim = await workflow.createInterim({
|
|
58
|
+
key: `${sourceEnrollmentId}:review:${sourceReviewId}`,
|
|
59
|
+
enrollmentKey: `${sourceEnrollmentId}:entry`, clientId, reviewDate,
|
|
60
|
+
assessment: interimPlan,
|
|
61
|
+
});
|
|
62
|
+
} finally {
|
|
63
|
+
await session.close();
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
An assessment plan has these fields:
|
|
68
|
+
|
|
69
|
+
- `answers`: field names mapped to the SDK's `{value}`, `{option: {code|name}}`, or supported `{options}` selectors.
|
|
70
|
+
- `subassessments`: new rows with a stable `key`, `parentField`, and child `answers`. Results return `recordsetIds` keyed by those row keys. Equivalent duplicate rows are rejected, including reordered answer properties, equivalent dates and monetary representations.
|
|
71
|
+
- `endDates`: explicit `parentField`, `recordsetId`, `startDateField`, `endDateField`, and `endDate`. Interims and exits can close rows; entry plans cannot. The end must be on or after the stored start and enrollment, and strictly before that interim or exit timestamp. Closure readback verifies retained fields as well as the end date. A different existing end date is a conflict.
|
|
72
|
+
- `carryForward`: fields or subassessment groups that already have values at this effective date. Unchanged verification groups should be carried forward instead of recreated.
|
|
73
|
+
- `notApplicable`: field names mapped to nonempty explanations. Every editable top-level question must have exactly one applicable coverage choice; multiple new rows and closures in the same group are allowed. Display labels and blanks need no answers. Child fields omitted from a new row are encoded as empty; the application must decide which child questions apply.
|
|
74
|
+
|
|
75
|
+
A replacement-income interim closes the original row using `entry.recordsetIds['earned-income']`, adds one new earned-income row with a new stable key and review-date start, and writes the new overall income answer and amount. The other groups carry forward. Supply timezone-qualified whole-second timestamps; body dates are never shifted. Scalar verification checks both the saved value and its effective timestamp. It uses the assessment API, not the known-defective complete-enrollment entry arrays.
|
|
76
|
+
|
|
77
|
+
DOB plans use `answers: {SVPPROFDOB: {value: '1984-02-29'}}`, a strict `YYYY-MM-DD` calendar date. Supply the `birthDates` adapter above whenever a plan answers or carries forward DOB. The workflow removes DOB from the ordinary mobile answer batch and runs the dedicated helper after other assessment writes. It preserves the enrollment/effective timestamp, verifies the client API and fresh UI, and returns `result.birthDate`. Household operations apply this to every selected member, including children joining later at their own entry date. All member DOBs are preflighted before enrollment creation or attachment.
|
|
78
|
+
|
|
79
|
+
For an unknown or declined DOB, answer `SVPPROFDOBTYPE` with its discovered option code and provide a `notApplicable.SVPPROFDOB` explanation. Do not submit a DOB value or carry forward an absent date. This preserves missing DOBs for each household member and does not require the calendar-date writer. See [quality-only DOB plans](profiles.md#unknown-or-declined-birth-date).
|
|
80
|
+
|
|
81
|
+
When an actual DOB exists and the discovered interim or exit instrument includes DOB, use `carryForward: ['SVPPROFDOB', ...otherUnchangedFields]` if DOB has not changed. If the instrument does not include DOB, leave it out of that plan. This reads the client API and UI even when the mobile answer value is blank, and creates no new DOB history row. Completed workflow retries verify the saved DOB without restoring earlier assessment values. An explicit new DOB at a later effective timestamp uses the helper; correction at an existing timestamp is supported only with matching entry context. Earlier historical corrections stop before writes. Migrate older workflow DOB timestamps to calendar dates explicitly; the workflow rejects timestamp-shaped DOB input.
|
|
82
|
+
|
|
83
|
+
All cooperating writers must retain the same durable store and keys. The workflow pins program/assessment metadata, rejects changed intent or schema under an existing key, checks complete counted enrollment lists, and records each attempt before sending. It adopts a unique existing enrollment with the same client/provider/type/start and exact existing subassessment rows rather than duplicating them. Multiple matches fail. A lost response is reconciled by readback; an empty, partial or failed read does not authorize resending. Another unfinished workflow for the client blocks subsequent workflow keys until it is reconciled. Existing GWT helpers keep their separate durable review journal.
|
|
84
|
+
|
|
85
|
+
`await workflow.getStatus({clientId, key})` reads local progress and attempted/verified stages without making network requests. Its status describes the journal, not fresh server state. Retry the same intent to reconcile; do not delete the journal or change keys to bypass an uncertain write. The process is not a transaction: saved enrollment/review records remain when a later stage fails. Locks coordinate this integration on one host; unrelated users or integrations can still race it. These single-client methods populate one person's assessment per call; use the household methods below to coordinate linked enrollments and reviews.
|
|
86
|
+
|
|
87
|
+
See the [packaged enrollment example](../examples/enrollment-workflow.mts) for a typed application-owned plan builder. The optional browser export requires a dedicated authenticated application page with the administrative read access needed for program assessment discovery.
|
|
88
|
+
|
|
89
|
+
[Live workflow acceptance](capabilities.md) covers a new synthetic client, exact entry/interim UI values, all verification table pages, and repeated calls with zero REST writes or GWT creations. [DOB workflow acceptance](capabilities.md) covers a leap-day DOB, distinct household member DOBs, a later-entry newborn, and repeated entry/interim operations after later assessment changes.
|
|
90
|
+
|
|
91
|
+
## Household enrollment and interim assessments
|
|
92
|
+
|
|
93
|
+
The same workflow exposes `createHouseholdEnrollment` and `createHouseholdInterim`. Supply `enrollmentSession: session.enrollmentSession` when constructing it. Initial household enrollment requires an existing household and at least two existing clients, including its head. Later interims and exits accept a nonempty subset of enrolled members. Establish household membership separately before enrollment.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const workflow = createEnrollmentWorkflow({
|
|
97
|
+
api, discovery: createProgramDiscovery({page: session.page}),
|
|
98
|
+
helpers: session.helpers, enrollmentSession: session.enrollmentSession, store,
|
|
99
|
+
birthDates: createDateOfBirth({api, store, ui: createDateOfBirthUi({page: session.page})}),
|
|
100
|
+
});
|
|
101
|
+
const entry = await workflow.createHouseholdEnrollment({
|
|
102
|
+
key: sourceEnrollmentId, householdId, headClientId,
|
|
103
|
+
providerId, type: 'HUD', entryDate,
|
|
104
|
+
members: [
|
|
105
|
+
{clientId: headClientId, assessment: headEntryPlan},
|
|
106
|
+
{clientId: memberClientId, assessment: memberEntryPlan},
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
const interim = await workflow.createHouseholdInterim({
|
|
110
|
+
key: sourceReviewId, enrollmentKey: sourceEnrollmentId,
|
|
111
|
+
headClientId, reviewDate,
|
|
112
|
+
members: [
|
|
113
|
+
{clientId: headClientId, assessment: headInterimPlan},
|
|
114
|
+
{clientId: memberClientId, assessment: memberInterimPlan},
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Each member receives their own assessment plan, enrollment ID, interim ID and verification recordset IDs in `result.members`. Use that member's returned IDs when closing and replacing verification rows. Interims accept a nonempty subset of currently enrolled members, including later entrants. The selected set is fixed under that interim key. Array order may change on retries; membership, dates and answers may not change under an existing key. The application still determines applicability, including child-specific questions and verification types.
|
|
120
|
+
|
|
121
|
+
All member plans are validated before record creation. The workflow creates the head's entry through REST, attaches members using the existing GWT helper, then populates their assessments individually. One household interim operation creates the linked reviews, followed by each person's assessment writes. Fresh browser reads verify every member's provider, type, entry date, enrollment identity and review membership. These reads avoid the sandbox server's broken grouped enrollment REST projection. Existing independent member enrollments or same-date reviews cause a conflict instead of being silently merged.
|
|
122
|
+
|
|
123
|
+
Client locks coordinate overlapping household and single-client operations. Durable household reservations block unrelated work after partial completion. A failed attachment, review or answer batch must be reconciled with the same keys and journal; no uncertain mutation is automatically resent. `getHouseholdStatus({headClientId,key})` reports local member progress and generated member keys, which can be passed to `getStatus({clientId,key})`. This is a resumable sequence, not an atomic transaction. The file store fails closed on a concurrent writer or leftover crash lock; inspect and reconcile such a lock before removing it.
|
|
124
|
+
|
|
125
|
+
See the typed [household example](../examples/household-enrollment-workflow.mts) and [live household verification](capabilities.md). The integration uses the configured tenant and uses an isolated authenticated page.
|
|
126
|
+
|
|
127
|
+
## Household exits
|
|
128
|
+
|
|
129
|
+
Add `exitUi: createHouseholdExitUi({page: session.page})` to the workflow options. `discoverExit` returns the provider's entry, interim and exit definitions; use the exit definition to build each person's plan.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const exit = await workflow.exitHousehold({
|
|
133
|
+
key: sourceExitId, enrollmentKey: sourceEnrollmentId,
|
|
134
|
+
headClientId, exitDate,
|
|
135
|
+
destination: 'Rental by client, no ongoing housing subsidy',
|
|
136
|
+
reasonForLeaving: 'Completed program',
|
|
137
|
+
members: [
|
|
138
|
+
{clientId: headClientId, assessment: headExitPlan},
|
|
139
|
+
{clientId: memberClientId, assessment: memberExitPlan},
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This requires a verified household entry in the same durable workflow store. Supply only the members exiting on this date, including a single member if needed; the household head need not be selected. Use separate stable keys for exits on different dates. Every exit plan is validated before saving. The browser submits one shared exit, then authoritative GWT reads verify every member's exit date, destination, reason and exact linked exit group before individual assessment writes. The returned members include `projectExitDate`, `exitSubgroupId` and any new verification `recordsetIds`. Exit dates cannot precede entry or an existing interim.
|
|
145
|
+
|
|
146
|
+
Use `endDates` only for verification records that changed, with replacement rows in `subassessments`; use `carryForward` for unchanged groups. Ending enrollment does not automatically end all verification records. Stored dates are never shifted. Discovery and completeness checks do not decide individual eligibility or which verification types apply.
|
|
147
|
+
|
|
148
|
+
Reusing the same key and unchanged plans reconciles against fresh reads without repeating writes, including after a lost save response. A partial household exit, unexpected linked member, conflicting exit metadata, schema drift, or an uncertain uncommitted save fails closed. Do not change keys or delete journals to bypass these checks. This remains a non-atomic sequence and does not prevent unrelated integrations or users from editing concurrently.
|
|
149
|
+
|
|
150
|
+
The supplied Swagger has no exit mutation. The optional UI port exposes `listOptions({clientId,enrollmentId})` for available destination, reason and `rentalSubsidyTypes` choices. It opens the form and cancels without saving. Conditional exit details are shared by the selected members:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// Rental destination with an ongoing subsidy:
|
|
154
|
+
const subsidyDetails = {
|
|
155
|
+
destination: 'Rental by client, with ongoing housing subsidy',
|
|
156
|
+
destinationDetails: {
|
|
157
|
+
kind: 'rental-subsidy' as const,
|
|
158
|
+
rentalSubsidyType: 'RRH or equivalent subsidy',
|
|
159
|
+
},
|
|
160
|
+
reasonForLeaving: 'Completed program',
|
|
161
|
+
};
|
|
162
|
+
// Separate explanations for the destination and reason:
|
|
163
|
+
const otherDetails = {
|
|
164
|
+
destination: 'Other',
|
|
165
|
+
destinationDetails: {kind: 'other' as const, explanation: 'Application-provided destination explanation'},
|
|
166
|
+
reasonForLeaving: 'Other',
|
|
167
|
+
reasonForLeavingOther: 'Application-provided reason explanation',
|
|
168
|
+
};
|
|
169
|
+
// Spread the chosen details into exitHousehold alongside the key, date and member plans.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Subsidy types must match a currently available choice. Other explanations must contain 1 to 100 characters without leading/trailing whitespace or control characters. Missing, irrelevant or contradictory details fail before saving. The UI port fills each explanation beside its own labeled field and verifies the selected subsidy. `readExitDetails` reopens a saved member exit and reads its persisted details without saving. The workflow compares these details for every member on initial readback and retries; changed explanations or subsidy types conflict instead of being silently overwritten. Custom UI ports must implement `readExitDetails` to support conditional details. An unexpected legacy `Subsidy` form is still rejected because its storage contract has not been verified. Use the port through the journaled workflow, not as an independently idempotent writer. See the [typed exit example](../examples/household-exit-workflow.mts) and [live conditional-exit verification](capabilities.md).
|
|
173
|
+
|
|
174
|
+
## Later household members and staggered exits
|
|
175
|
+
|
|
176
|
+
`exitHousehold` and `createHouseholdInterim` accept selected members, including members added later. Continue supplying the original household `enrollmentKey` and `headClientId`. Each member retains their own enrollment ID and entry date. Exit subgroups are checked against the selected IDs; unselected enrollment dates, exits, conditional exit details and reviews are compared before and after a pending exit.
|
|
177
|
+
|
|
178
|
+
Head of household is fixed for the enrollment. A proposed or detected head change is a red flag, including after the original head exits. Using another member as the UI/RPC target never promotes that member, and a later entrant cannot become the enrollment head. When the discovered assessment includes `HUD_RELATIONTOHOH`, household workflow plans reject answers that promote another member, demote the original head, or clear the relationship. Interim and exit preflight also reject conflicting stored relationships in that assessment. These checks do not govern independent raw API writes.
|
|
179
|
+
|
|
180
|
+
An enrollment without a head of household is invalid data even when WellSky accepts it. Household helpers throw `EnrollmentWorkflowError` with an explicit `invalid enrollment data` reason when the designated head is absent or other members would remain enrolled beyond the head's exit. Preflight checks both existing records and the requested change before reserving or writing the operation, and readback checks the saved group. A head-only exit is allowed only after every other member has an exit at or before that date. A later child cannot join a group whose head has exited. Existing orphaned groups, including retries against them, require explicit reconciliation; the helper does not promote a replacement or silently repair dates. Closed groups with valid historical head coverage can still be retried without duplicate writes.
|
|
181
|
+
|
|
182
|
+
Selecting every enrolled member in one `exitHousehold` call saves their exit metadata through one UI-generated `updateEntryExitWithHouseholdMembers` GWT request. Per-member assessment writes follow through REST, so the complete workflow is not atomic. [Live required-head acceptance](capabilities.md) verifies blocked head-first exits, separate member exits, head-last and all-member exits, unchanged retries, and persisted UI forms on new synthetic clients.
|
|
183
|
+
|
|
184
|
+
When the discovered instrument exposes the head relationship question, the designated head must have a valid Self answer in the plan or at the assessment's effective date. Marking the question not applicable cannot substitute for a missing relationship. Standalone enrollment helpers treat the sole client as head. These guards cannot enforce constraints on writes made directly through WellSky or the raw REST/GWT APIs.
|
|
185
|
+
|
|
186
|
+
A head correction must be a separate, explicit retroactive correction, and the replacement must have the same first entry date as the original head. This workflow does not implement that correction or silently change its journal identity. An actual change of head during participation generally requires exiting all enrollments and creating a fresh enrollment with the new head. A fresh entry may establish relationships different from prior assessment history.
|
|
187
|
+
|
|
188
|
+
For a newborn or a child moving in later, establish the child in the existing household first, with the correct relationship and joined date. Then add a separate immutable operation:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const workflow = createEnrollmentWorkflow({
|
|
192
|
+
api, discovery, helpers: session.helpers, store,
|
|
193
|
+
birthDates: createDateOfBirth({api, store, ui: createDateOfBirthUi({page: session.page})}),
|
|
194
|
+
enrollmentSession: session.enrollmentSession,
|
|
195
|
+
exitUi: createHouseholdExitUi({page: session.page}),
|
|
196
|
+
memberEntryDatePort: createMemberEntryDatePort({
|
|
197
|
+
api, enrollmentSession: session.enrollmentSession,
|
|
198
|
+
}),
|
|
199
|
+
});
|
|
200
|
+
const child = await workflow.addHouseholdMembers({
|
|
201
|
+
key: sourceChildEntryId, enrollmentKey: sourceEnrollmentId, headClientId,
|
|
202
|
+
entryDate: childEntryDate,
|
|
203
|
+
members: [{clientId: childClientId, assessment: childEntryPlan}],
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The later date must follow the original household entry, and an existing member must still be enrolled. The child must have joined the household on or before their own entry date. New client and household-member creation remain separate operations. The application supplies age-appropriate answers and explicit applicability reasons; the SDK does not infer them.
|
|
208
|
+
|
|
209
|
+
The workflow discovers and pins the new member's assessments, attaches the member through a fresh GWT request, then uses a date-only REST update for their enrollment. It verifies all linked enrollment dates and exits through GWT before populating answers and verification rows at the child's own entry date. Existing assessment dates are never rewritten. These stages are not atomic: a failure may leave a linked child awaiting the correct date or assessment completion. Keep the same journal and key to reconcile; an uncertain date update is never blindly resent.
|
|
210
|
+
|
|
211
|
+
Retries can verify an earlier entry, addition or exit after later household changes without creating duplicates. All cooperating writers must share the durable store. External changes to enrollment identity, selected exit membership, or pending-operation preserved state cause a conflict. The UI adapter scopes checkbox selection to the active dialog because the additional-members popup can overlay another visible household tree.
|
|
212
|
+
|
|
213
|
+
See the [typed lifecycle example](../examples/household-lifecycle-workflow.mts) and [sandbox verification](capabilities.md).
|
|
214
|
+
|
|
215
|
+
## Current Living Situation contacts
|
|
216
|
+
|
|
217
|
+
For street outreach and night-by-night shelter meaningful contacts, use the [Current Living Situation recorder](current-living-situation.md). Start always comes from program entry, information date comes from the contact, and end may be empty. Generic `subassessments` plans do not infer these event rules and can adopt an existing semantically matching row. They are not a distinct-contact recorder. Do not use the income replacement example to close or replace CLS contact history.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Upload files and record notes
|
|
2
|
+
|
|
3
|
+
[Documentation index](README.md) · [Agent guide](../AGENTS.md)
|
|
4
|
+
|
|
5
|
+
The snippets assume `session` from [browser session setup](browser-session.md), plus application-owned client, goal, and provider IDs.
|
|
6
|
+
|
|
7
|
+
## Attachments and goal case notes
|
|
8
|
+
|
|
9
|
+
After an attachment is adopted or created, retries require its saved attachment ID as well as matching bytes and metadata. A missing attachment or an identical replacement under a different ID is a conflict. Inspect the original record and journal; do not discard the journal to upload another copy.
|
|
10
|
+
|
|
11
|
+
Conflicts raise the exported `AttachmentConflictError` with code `ATTACHMENT_CONFLICT` and a `reason`: `intent-changed`, `duplicate-name`, `identity-changed`, `content-changed`, or `unresolved`. Managed sessions preserve this error without probing or reauthenticating, so an unrelated expired session cannot turn a confirmed conflict into `SESSION_INTERRUPTED`. The next operation still checks authentication normally. Failed reads and uncertain new upload responses remain subject to session recovery and journal reconciliation.
|
|
12
|
+
|
|
13
|
+
`session.workflows` provides browser-authenticated reads and writes:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const uploaded = await session.workflows.uploadAttachment({
|
|
17
|
+
clientId,
|
|
18
|
+
location: { kind: 'goal', goalId }, // Or {kind:'profile'} / {kind:'case-plans'}.
|
|
19
|
+
file: { name: 'synthetic.txt', mimeType: 'text/plain', buffer: new TextEncoder().encode('Synthetic test') },
|
|
20
|
+
description: 'Synthetic test attachment',
|
|
21
|
+
});
|
|
22
|
+
const files = await session.workflows.listAttachments(clientId);
|
|
23
|
+
const bytes = await session.workflows.downloadAttachment({
|
|
24
|
+
clientId, attachmentId: uploaded.attachment.attachmentId,
|
|
25
|
+
});
|
|
26
|
+
const note = await session.workflows.createCaseNote({
|
|
27
|
+
key: sourceGoalNoteId, clientId, goalId, providerId,
|
|
28
|
+
noteDate: '2026-10-21', note: 'Synthetic progress note',
|
|
29
|
+
});
|
|
30
|
+
const notes = await session.workflows.listCaseNotes({clientId, goalId});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Uploads are limited to 10,000,000 bytes and verified by downloading and hashing the stored bytes. Repeating the same client/location/name/content/description returns the existing attachment. Reusing the name with different content or description is a conflict. Supply a stable source key for case notes. Reusing it with changed intent raises `CASE_NOTE_SOURCE_CONFLICT`; saved note identity is also checked. Unkeyed legacy calls match the goal, client, provider, exact text, and browser-local calendar date. The helper verifies the provider shown by the goal's note form and saves only for the requested client. It does not switch providers or create notes for multiple household members. Existing identical notes are returned; partial or uncertain attempts are never automatically repeated. These helpers use the same durable store and do not change file visibility settings.
|
|
34
|
+
|
|
35
|
+
The UI uses authenticated multipart `uploadAttachment` and `downloadAttachment`, while REST uses `fileAttachments.createForClient`/`getForClient` with credentials in the body in addition to the REST token. REST entity labels are `Client Profile`, `Case Plans`, and `Goal`; the UI uses `SpClient`, `CasePlans`, and `SpGoal`. In the tested tenant both REST attachment calls returned a credential/token mismatch even with the token account's credentials. Browser upload/readback succeeded at all three locations. Profile lists include attachments from other locations and paginate at five rows. No anonymous-access or other-provider visibility claim was tested.
|
|
36
|
+
|
|
37
|
+
Further authentication testing reproduced the failure with native JSON and multipart requests outside the SDK. Ordinary client reads succeeded with the same fresh token. `SetAuthKey` issued a user-ID claim containing literal single quotes, which may affect attachment identity comparisons, but the server's root cause is unconfirmed. Alternate documented authentication routes did not yield a working attachment token. No password transformation or token modification is applied by the SDK.
|
|
38
|
+
|
|
39
|
+
Use [journaled case-plan helpers](durable-records.md#create-and-update-client-notes-goals-and-action-steps) for Client Notes, goals, and action steps. Raw REST create/update/read methods are also available but do not provide durable retry protection. Include `clientId` when updating a goal; the tested server rejected an update without it despite the optional Swagger field. Goal case notes use the paper icon in the Notes column or Add Case Note inside the goal; they are distinct from Client Notes. The REST goal response did not expose these case notes. Observed goal responses also included unrelated `households` rows while the goal had no household association; do not use that nested array to choose clients for a write.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Read a synthetic client without an HMIS account
|
|
2
|
+
|
|
3
|
+
[Documentation index](README.md) · [Using the SDK](using-the-sdk.md)
|
|
4
|
+
|
|
5
|
+
This example reads one synthetic client through the SDK's real validation and transport code. The fake transport cannot send requests to WellSky.
|
|
6
|
+
|
|
7
|
+
## Install the package
|
|
8
|
+
|
|
9
|
+
Use Node 22 or newer, then install from the default npm registry:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @choiceapps/communityservices-sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
If you are in the SDK source checkout, run `npm ci` and `npm run build` in `packages/wellsky-sdk` instead. This builds the package's self-referenced imports.
|
|
16
|
+
|
|
17
|
+
## Run the read
|
|
18
|
+
|
|
19
|
+
Save the following as `quickstart.mjs` in your application directory:
|
|
20
|
+
|
|
21
|
+
<!-- checked-example: examples/quickstart.mjs -->
|
|
22
|
+
```js
|
|
23
|
+
import assert from 'node:assert/strict';
|
|
24
|
+
import {createFakeWellSkyClient} from '@choiceapps/communityservices-sdk/testing';
|
|
25
|
+
import {toItems} from '@choiceapps/communityservices-sdk';
|
|
26
|
+
|
|
27
|
+
const client = createFakeWellSkyClient();
|
|
28
|
+
client.fake.set('GET /clients/{clientId}', {
|
|
29
|
+
kind: 'reply', body: [{clientId: 123, firstName: 'Synthetic'}],
|
|
30
|
+
});
|
|
31
|
+
const people = toItems(await client.clients.get({path: {clientId: 123}}));
|
|
32
|
+
assert.equal(people.length, 1);
|
|
33
|
+
assert.equal(people[0].clientId, 123);
|
|
34
|
+
assert.equal(client.fake.calls.length, 1);
|
|
35
|
+
console.log('Read synthetic client 123; no network requests.');
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Run it:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
node quickstart.mjs
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Expected output:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
Read synthetic client 123; no network requests.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The same file ships as [examples/quickstart.mjs](../examples/quickstart.mjs). From the SDK checkout, run `node examples/quickstart.mjs` after the build.
|
|
51
|
+
|
|
52
|
+
## Connect your application
|
|
53
|
+
|
|
54
|
+
For a real read, construct a `WellSkyClient` with your tenant and credentials from your server's secret store. Follow [Configure a client](rest-client.md#configure-a-client). Replace the synthetic ID with an authorized client ID from your application.
|
|
55
|
+
|
|
56
|
+
For mutations, first choose a verified workflow from the [capability table](capabilities.md). Preserve its journal and stable keys. Do not convert this example into a retry loop around a raw write.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Independent needs and service types
|
|
2
|
+
|
|
3
|
+
`createNeedRecorder` records a need without a service or referral. This supports workflows such as a Point in Time count's need for emergency shelter. The supplied V2/V3 Swagger documents reads of needs, but no independent-need create operation or service taxonomy catalog. These helpers therefore use the authenticated UI and counted GWT reads in the configured tenant.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import {
|
|
7
|
+
createNeedUi, createNeedRecorder,
|
|
8
|
+
} from '@choiceapps/communityservices-sdk/browser';
|
|
9
|
+
|
|
10
|
+
const ui = createNeedUi({page: session.page});
|
|
11
|
+
const catalog = await ui.listServiceTypes({clientId});
|
|
12
|
+
const quicklist = await ui.listServiceQuicklist({clientId, providerId});
|
|
13
|
+
// Let the application choose from quicklist.types or catalog.types.
|
|
14
|
+
const chosen = catalog.types.find(t => t.serviceCodeId === selectedServiceCodeId);
|
|
15
|
+
if (!chosen) throw new Error('Select an available service type');
|
|
16
|
+
const needs = createNeedRecorder({ui, store});
|
|
17
|
+
await needs.record({
|
|
18
|
+
key: sourceEventId, clientId, providerId,
|
|
19
|
+
serviceCodeId: chosen.serviceCodeId,
|
|
20
|
+
needDate: observedAt, // timezone-qualified instant, whole seconds
|
|
21
|
+
status: selectedStatus, outcome: selectedOutcome,
|
|
22
|
+
note: 'Synthetic PIT emergency shelter need',
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Custom adapters must supply an explicit `tenant` to `createNeedRecorder`; the packaged UI adapter supplies its bound connection automatically.
|
|
27
|
+
|
|
28
|
+
The application supplies the provider, source event ID, date, and status/outcome. Choose labels from `quicklist.needOptions`. Independent needs do not default to Closed/Fully Met. A `Not Met` outcome can include `reasonUnmet`; `amount` optionally records a nonnegative financial amount with up to two decimal places. No household members are silently included.
|
|
29
|
+
|
|
30
|
+
## REST route investigation
|
|
31
|
+
|
|
32
|
+
Authenticated sandbox probes on October 24, 2026 found no standalone need-create route among eight candidates in either API V2 or V3. Live Swagger matched the pinned specifications.
|
|
33
|
+
|
|
34
|
+
| POST route tested | V2 and V3 result |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `/needs`, `/needs/create`, `/needs/add` | 405 Method Not Allowed; `Allow: GET` |
|
|
37
|
+
| `/need/create`, `/services/needs/create`, `/needs/household/create` | 404 Not Found |
|
|
38
|
+
| `/needs/create/0`, `/needs/household/create/0` | 404 Not Found |
|
|
39
|
+
|
|
40
|
+
Probes used malformed JSON with no valid client IDs or write data. As a control, documented `POST /services/create` reached JSON validation and returned 400. `GET /needs/create` returned a validation error identifying `create` as an invalid `clientId`, confirming that this path matches `/needs/{clientId}` rather than a create action. No probe reached a valid write.
|
|
41
|
+
|
|
42
|
+
These results rule out the tested routes, not every possible undocumented URL. The SDK continues to use the verified UI/GWT `saveNeeds` operation for independent creation. `/services/needs/household/update/{serviceId}` is an update associated with an existing service, not an independent create operation.
|
|
43
|
+
|
|
44
|
+
## Discover types for both needs and services
|
|
45
|
+
|
|
46
|
+
`listServiceTypes({clientId})` returns the **tenant-wide** catalog:
|
|
47
|
+
|
|
48
|
+
- `codeSets`: configured code-set IDs and names, including AIRS Taxonomy when configured.
|
|
49
|
+
- `types`: active assignable terms, each with `serviceCodeId`, taxonomy `code`, `description`, `codeSetId`, and `parentServiceCodeId`.
|
|
50
|
+
|
|
51
|
+
`listServiceQuicklist({clientId, providerId})` separately returns the selected program/provider's `types` and `needOptions` (status, outcome, and unmet-reason picklists). It reads only that quicklist's terms, without fetching the full catalog. Service types are configured at the tenant level, not per user or program; quicklists are configured per program/provider. Both methods require an authenticated session. `clientId` supplies browser navigation context, not a catalog filter.
|
|
52
|
+
|
|
53
|
+
The catalog follows the UI's exclusion of target-population Y codes and Modality/Delivery Format facets. It reads ordered, counted pages and rejects inconsistent counts, duplicate IDs, or catalogs above 50,000 terms. The provider quicklist is a convenience subset, not the full lookup. A parent ID can refer to an unassignable hierarchy node omitted from this selection list. Preserve the source code-set identity; do not assume every deployment uses AIRS or the same numeric IDs.
|
|
54
|
+
|
|
55
|
+
Pass `chosen.serviceCodeId` to independent needs; pass `chosen.code` as `body.serviceCode` to the [service recorder](services.md). These choices describe service types, not eligibility, SSVF conditional fields, funding availability, or authorization to use a provider. Use the service recorder's separate SSVF and funding contracts for those fields. The SDK does not bundle the licensed taxonomy or publish tenant catalogs.
|
|
56
|
+
|
|
57
|
+
## Cache the catalog in your application
|
|
58
|
+
|
|
59
|
+
Cache `listServiceTypes` results **once per instance/tenant**, instead of fetching the full catalog for every form, user, program, or transaction. The sandbox catalog contained 2,835 assignable terms when verified; its count and contents can change. Serve the cached list to your type selector and reuse the selected ID/code for subsequent transactions. The write helper opens its own fresh form and checks that the selected code is still active and assignable; it does not enumerate the entire catalog before every save.
|
|
60
|
+
|
|
61
|
+
Cache `listServiceQuicklist` separately by **instance/tenant and provider/program ID**. When the application switches programs, fetch or reuse that program's small quicklist and combine it with the existing tenant catalog. Do not invalidate the tenant catalog just because the program or user changes. The application still controls which users may access its cached data.
|
|
62
|
+
|
|
63
|
+
Use a configurable TTL appropriate to how often your organization changes configuration, such as one hour as an application starting point, and offer an explicit refresh. Invalidate the tenant catalog when service-type/code-set configuration changes, and invalidate the affected program's quicklist when its configuration changes. If a selected code is no longer accepted, refresh and have the application resolve the choice; do not substitute another code automatically. Coalesce concurrent refreshes so several forms do not fetch the same catalog repeatedly. Keep cached tenant/licensed taxonomy data within your application's authorized scope.
|
|
64
|
+
|
|
65
|
+
This is an application cache recommendation, not a built-in SDK cache or a promise that an old selection remains valid. Do **not** cache need-record reads used for duplicate detection, journal reconciliation, or post-save verification. Those reads must remain fresh.
|
|
66
|
+
|
|
67
|
+
## Retry and verification contract
|
|
68
|
+
|
|
69
|
+
Keep a persistent [mutation store](browser-session.md) and the same source key across restarts. A hash marker is appended to the need note. Source keys are scoped across clients, so reusing a key for another client or changed input is rejected before any browser operation.
|
|
70
|
+
|
|
71
|
+
The adapter opens a fresh Add Need form, journals immediately before its single Save & Exit click, and performs counted reads of active needs and service/referral links. It verifies the client, provider, code ID, exact stored instant, note, amount, status, outcome, unmet reason, and absence of associated service records. Stored dates are not shifted.
|
|
72
|
+
|
|
73
|
+
An identical retry verifies the original ID without another save. A lost response can reconcile if the marker and fields identify one saved record. Missing records after an attempted save, duplicate markers, replaced IDs, edited fields, or subsequently attached services produce `NeedConflictError` (`code: NEED_CONFLICT`). Inspect its `reason`; do not regenerate the key to bypass it. A failed read never proves absence. There is no server-side idempotency key and no protection against unrelated external writers copying the marker.
|
|
74
|
+
|
|
75
|
+
Use a dedicated managed page. Close any unfinished form or dialog before reconciling an interrupted save. Reauthentication never replays the save. `ui.list({clientId})` provides counted stored readback; the REST SDK's `needs.getComplete` remains available for a second read. The REST representation may omit the financial amount.
|
|
76
|
+
|
|
77
|
+
See the shipped [independent-needs example](../examples/independent-needs.mts) for discovery, PIT recording, and using the same selected type with a service.
|