@choiceapps/communityservices-sdk 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/browser/enrollment-workflow.d.mts +17 -3
- package/browser/enrollment-workflow.mjs +35 -5
- package/browser/household-exit.d.mts +1 -1
- package/browser/household-exit.mjs +4 -2
- package/docs/capabilities.md +1 -0
- package/docs/development.md +1 -1
- package/docs/enrollments.md +26 -0
- package/docs/reference.md +1 -1
- package/examples/README.md +1 -0
- package/examples/individual-exit-workflow.mts +16 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1 - 2026-11-10
|
|
4
|
+
|
|
5
|
+
- Pin GitHub verification and publication jobs to Ubuntu 24.04. The Ubuntu 26.04 runner selected by `ubuntu-latest` cannot install Chromium with the pinned Playwright version, which blocked public npm publication of 0.4.0. SDK behavior is unchanged.
|
|
6
|
+
|
|
7
|
+
## 0.4.0 - 2026-11-10
|
|
8
|
+
|
|
9
|
+
- Add `exitEnrollment` for standalone program enrollments, with durable save journaling, authoritative exit verification, conditional destination/reason details and per-person exit assessments. Linked household participants continue to use `exitHousehold`, including whole-household and selected-member exits.
|
|
10
|
+
- Support a standalone exit form without a household checkbox tree only when fresh enrollment data confirms exactly one participant. Added synthetic regression coverage; live standalone acceptance remains pending.
|
|
11
|
+
|
|
3
12
|
## 0.3.0 - 2026-11-03
|
|
4
13
|
|
|
5
14
|
- Compare journaled attachment and case-note intents, and shelter stay identities, by canonical hash instead of key-order-sensitive JSON, so an identical retry no longer conflicts when a store such as Postgres `jsonb` reorders keys. Existing journals remain valid.
|
|
@@ -57,6 +57,16 @@ export interface InterimWorkflowInput {
|
|
|
57
57
|
reviewDate:string;
|
|
58
58
|
assessment:EnrollmentAssessmentPlan;
|
|
59
59
|
}
|
|
60
|
+
/** A standalone enrollment created or adopted by createEnrollment in this store. */
|
|
61
|
+
export interface ExitEnrollmentInput extends HouseholdExitDetailInput {
|
|
62
|
+
key:string;
|
|
63
|
+
enrollmentKey:string;
|
|
64
|
+
clientId:number;
|
|
65
|
+
exitDate:string;
|
|
66
|
+
destination:string;
|
|
67
|
+
reasonForLeaving:string;
|
|
68
|
+
assessment:EnrollmentAssessmentPlan;
|
|
69
|
+
}
|
|
60
70
|
export interface EnrollmentWorkflowResult {
|
|
61
71
|
key:string;
|
|
62
72
|
outcome:'verified'|'already-present';
|
|
@@ -71,6 +81,8 @@ export interface EnrollmentWorkflowResult {
|
|
|
71
81
|
}
|
|
72
82
|
export interface InterimWorkflowResult extends EnrollmentWorkflowResult {reviewId:number;}
|
|
73
83
|
export interface ExitWorkflowResult extends EnrollmentWorkflowResult {projectExitDate:string;exitSubgroupId:string;}
|
|
84
|
+
/** An individual exit may have no linked exit subgroup. */
|
|
85
|
+
export interface ExitEnrollmentResult extends EnrollmentWorkflowResult {projectExitDate:string;exitSubgroupId:string|null;}
|
|
74
86
|
export interface ExitWorkflowDiscovery extends EnrollmentWorkflowDiscovery {exit:{assessment:ProviderAssessmentItem;definition:AssessmentDefinition};}
|
|
75
87
|
export interface EnrollmentWorkflowStatus {
|
|
76
88
|
key:string;
|
|
@@ -80,7 +92,7 @@ export interface EnrollmentWorkflowStatus {
|
|
|
80
92
|
enrollmentId?:number;
|
|
81
93
|
reviewId?:number;
|
|
82
94
|
projectExitDate?:string;
|
|
83
|
-
exitSubgroupId?:string;
|
|
95
|
+
exitSubgroupId?:string|null;
|
|
84
96
|
recordsetIds:Record<string,number>;
|
|
85
97
|
steps:{name:string;state:'attempted'|'verified'}[];
|
|
86
98
|
}
|
|
@@ -89,6 +101,8 @@ export interface EnrollmentWorkflow {
|
|
|
89
101
|
discoverExit(input:EnrollmentDiscoveryInput):Promise<ExitWorkflowDiscovery>;
|
|
90
102
|
createEnrollment(input:EnrollmentWorkflowInput):Promise<EnrollmentWorkflowResult>;
|
|
91
103
|
createInterim(input:InterimWorkflowInput):Promise<InterimWorkflowResult>;
|
|
104
|
+
/** Standalone participant only. Linked household participants use exitHousehold. */
|
|
105
|
+
exitEnrollment(input:ExitEnrollmentInput):Promise<ExitEnrollmentResult>;
|
|
92
106
|
/** Reads the local journal only. A verified journal is not fresh server verification. */
|
|
93
107
|
getStatus(input:{clientId:number;key:string}):Promise<EnrollmentWorkflowStatus|null>;
|
|
94
108
|
createHouseholdEnrollment(input:HouseholdEnrollmentInput):Promise<HouseholdEnrollmentResult>;
|
|
@@ -164,9 +178,9 @@ export interface EnrollmentWorkflowOptions {
|
|
|
164
178
|
store:UiMutationStore;
|
|
165
179
|
/** Required for SVPPROFDOB answers or carry-forward. Share this workflow's durable store. */
|
|
166
180
|
birthDates?:DateOfBirth;
|
|
167
|
-
/** Required
|
|
181
|
+
/** Required for exits; also required with api.households and helpers.addHouseholdMembers for household operations. */
|
|
168
182
|
enrollmentSession?:EnrollmentReader;
|
|
169
|
-
/** Required for exitHousehold; the workflow owns its durable write journal. */
|
|
183
|
+
/** Required for exitEnrollment and exitHousehold; the workflow owns its durable write journal. */
|
|
170
184
|
exitUi?:HouseholdExitUi;
|
|
171
185
|
/** Required to set the later entry date for newly attached household members. */
|
|
172
186
|
memberEntryDatePort?:MemberEntryDatePort;
|
|
@@ -119,10 +119,15 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
119
119
|
const scope=clientId=>JSON.stringify([tenantScope,'enrollment-workflow',clientId]);
|
|
120
120
|
const reservationScope=clientId=>JSON.stringify([tenantScope,'household-workflow-reservation',clientId]);
|
|
121
121
|
const run=async(kind,raw,internal={})=>{
|
|
122
|
-
|
|
122
|
+
const standaloneExit=kind==='exit'&&internal.standaloneExit===true;
|
|
123
|
+
fields(raw,kind==='entry'?['key','clientId','providerId','type','entryDate','assessment']:['key','enrollmentKey','clientId',kind==='exit'?'exitDate':'reviewDate','assessment',...(standaloneExit?['destination','reasonForLeaving','destinationDetails','reasonForLeavingOther']:[])]);
|
|
123
124
|
const input=structuredClone(raw);key(input.key);if(!positive(input.clientId))fail('client ID required');
|
|
124
125
|
if(kind==='entry'){if(!positive(input.providerId)||!entryExitTypes.includes(input.type))fail('program and supported Entry/Exit type required');input.entryDate=date(input.entryDate);}else{key(input.enrollmentKey);if(kind==='addition')input.entryDate=date(input.entryDate);else if(kind==='exit')input.exitDate=date(input.exitDate);else input.reviewDate=date(input.reviewDate);}
|
|
125
126
|
if(!object(input.assessment))fail('assessment plan required');
|
|
127
|
+
if(standaloneExit&&(typeof enrollmentSession?.readEnrollment!=='function'||typeof exitUi?.validateExit!=='function'||typeof exitUi?.saveExit!=='function'))fail('exit UI and authoritative enrollment session required');
|
|
128
|
+
if(standaloneExit&&['destination','reasonForLeaving'].some(field=>typeof input[field]!=='string'||!input[field].trim()))fail('destination and reason for leaving required');
|
|
129
|
+
const exitDetails=standaloneExit?expectedExitDetails(input):null;
|
|
130
|
+
if(standaloneExit&&(exitDetails.destinationDetails||exitDetails.reasonForLeavingOther)&&typeof exitUi.readExitDetails!=='function')fail('authoritative exit details reader required');
|
|
126
131
|
const locked=internal.locked?async(_scope,work)=>work():store.withLock.bind(store);
|
|
127
132
|
return locked(scope(input.clientId),async()=>{
|
|
128
133
|
const reservations=await store.withLock(reservationScope(input.clientId),()=>store.read(reservationScope(input.clientId)));
|
|
@@ -137,6 +142,27 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
137
142
|
if(kind!=='entry'&&!entry)fail('verified enrollment workflow required');
|
|
138
143
|
const context=kind==='entry'?input:kind==='addition'?{...entry.context,entryDate:input.entryDate}:entry.context;
|
|
139
144
|
if(kind!=='entry'&&Date.parse(input.exitDate??input.reviewDate)<Date.parse(context.entryDate))fail('review precedes entry');
|
|
145
|
+
const exitTarget=standaloneExit?{clientId:input.clientId,enrollmentId:entry.enrollmentId}:null;
|
|
146
|
+
const inspectStandaloneExit=async()=>{
|
|
147
|
+
const snapshot=await enrollmentSession.readEnrollment(exitTarget);
|
|
148
|
+
if(snapshot?.source!=='gwt-rpc'||snapshot.tenant!==api.tenant||snapshot.clientId!==input.clientId||snapshot.enrollmentId!==entry.enrollmentId||snapshot.providerId!==context.providerId||snapshot.enrollmentType!==context.type||restDate(snapshot.projectStartDate)!==restDate(context.entryDate))fail('individual enrollment context mismatch');
|
|
149
|
+
if(!Array.isArray(snapshot.members)||snapshot.members.length!==1||snapshot.members[0].clientId!==input.clientId||snapshot.members[0].enrollmentId!==entry.enrollmentId)fail('linked household enrollment requires exitHousehold');
|
|
150
|
+
if(!Array.isArray(snapshot.reviews)||snapshot.reviews.some(r=>!Number.isFinite(restDate(r.reviewDate))||restDate(r.reviewDate)>restDate(input.exitDate)))fail('exit precedes an existing review');
|
|
151
|
+
if(!Array.isArray(snapshot.exits)||snapshot.exits.length!==1)fail('authoritative individual exit read required');
|
|
152
|
+
const exit=snapshot.exits[0];
|
|
153
|
+
if(exit.clientId!==input.clientId||exit.enrollmentId!==entry.enrollmentId||canonical(exit.clientIds)!==canonical([input.clientId])||canonical(exit.enrollmentIds)!==canonical([entry.enrollmentId]))fail('individual exit membership mismatch');
|
|
154
|
+
if(exit.projectExitDate===null){
|
|
155
|
+
if([exit.destination,exit.reasonForLeaving,exit.exitSubgroupId].some(v=>v!==null))fail('incomplete existing exit metadata');
|
|
156
|
+
return {found:false};
|
|
157
|
+
}
|
|
158
|
+
const label=value=>typeof value==='string'?value.replace(/\s*\(HUD\)\s*$/,'').trim():value;
|
|
159
|
+
if(restDate(exit.projectExitDate)!==restDate(input.exitDate)||label(exit.destination)!==label(input.destination)||label(exit.reasonForLeaving)!==label(input.reasonForLeaving)||exit.exitSubgroupId!==null&&(typeof exit.exitSubgroupId!=='string'||!exit.exitSubgroupId))fail('existing exit conflicts with requested exit');
|
|
160
|
+
if(record&&Object.hasOwn(record,'exitSubgroupId')&&record.exitSubgroupId!==exit.exitSubgroupId)fail('exit identity changed');
|
|
161
|
+
if(record?.steps.exit?.state==='verified'&&record.steps.exit.value.exitSubgroupId!==exit.exitSubgroupId)fail('exit identity changed');
|
|
162
|
+
if(typeof exitUi.readExitDetails==='function'&&canonical(await exitUi.readExitDetails(exitTarget))!==canonical(exitDetails))fail('stored exit details conflict with requested exit');
|
|
163
|
+
return {found:true,value:exit};
|
|
164
|
+
};
|
|
165
|
+
const exitInput=standaloneExit?{...exitTarget,clientIds:[input.clientId],exitDate:input.exitDate,destination:input.destination,reasonForLeaving:input.reasonForLeaving,...(input.destinationDetails?{destinationDetails:input.destinationDetails}:{}),...(input.reasonForLeavingOther?{reasonForLeavingOther:input.reasonForLeavingOther}:{})}:null;
|
|
140
166
|
const discovered=await discover({clientId:input.clientId,providerId:context.providerId,type:context.type,entryDate:kind==='entry'?input.entryDate:(input.exitDate??input.reviewDate)},kind==='exit');
|
|
141
167
|
const schemaHash=hash({providerId:discovered.provider.providerId,entry:{assessment:discovered.entry.assessment,questions:discovered.entry.definition.questions},interim:{assessment:discovered.interim.assessment,questions:discovered.interim.definition.questions}});
|
|
142
168
|
if((record&&record.schemaHash!==schemaHash)||(entry&&entry.schemaHash!==schemaHash))fail('program assessment configuration changed');
|
|
@@ -207,6 +233,10 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
207
233
|
if(restDate(end.endDate)<restDate(start[0].answer_value))fail('end date precedes record start');
|
|
208
234
|
if(through[0].answer_value&&restDate(through[0].answer_value)!==restDate(end.endDate))fail('record already has a different end date');
|
|
209
235
|
}
|
|
236
|
+
if(standaloneExit){
|
|
237
|
+
const observed=await inspectStandaloneExit();
|
|
238
|
+
if(!observed.found&&!record?.steps.exit)await exitUi.validateExit(exitInput);
|
|
239
|
+
}
|
|
210
240
|
if(internal.preflight)return {assessmentCode:code,...(internal.pinSchema?{schemaHash}:{}),...(exitSchemaHash?{exitSchemaHash}:{})};
|
|
211
241
|
const wasVerified=record?.stage==='verified';
|
|
212
242
|
if(!record){record={version:1,key:input.key,kind,intentHash,schemaHash,...(exitSchemaHash?{exitSchemaHash}:{}),context:{clientId:input.clientId,providerId:context.providerId,type:context.type,entryDate:context.entryDate},assessmentCode:code,...(yesNoWrites==='standard'?{yesNoWrites}:{}),stage:'pending',steps:{},rows:{},ends:plan.ends};records.push(record);}
|
|
@@ -230,6 +260,7 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
230
260
|
const findEnrollment=async()=>{
|
|
231
261
|
const knownId=record.enrollmentId??record.steps.enrollment?.value??entry?.enrollmentId;
|
|
232
262
|
if(internal.findEnrollment)return internal.findEnrollment({knownId,context});
|
|
263
|
+
if(standaloneExit){await inspectStandaloneExit();return {found:true,value:entry.enrollmentId};}
|
|
233
264
|
const list=await enrollments(api,input.clientId),found=list.filter(e=>e.enrollmentProvider===context.providerId&&e.type===context.type&&restDate(e.projectStartDate)===restDate(context.entryDate)&&e.isActive!==false);
|
|
234
265
|
if(found.length>1)fail('duplicate matching enrollments');
|
|
235
266
|
const id=record.enrollmentId??record.steps.enrollment?.value??entry?.enrollmentId;if(id&&(!found[0]||found[0].enrollmentId!==id))fail('saved enrollment missing or changed');
|
|
@@ -244,9 +275,8 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
244
275
|
const matches=review.snapshot.reviews.filter(r=>r.type==='Update'&&restDate(r.reviewDate)===restDate(at)&&r.clientIds.includes(input.clientId));if(matches.length!==1||record.reviewId&&record.reviewId!==matches[0].reviewId)fail('interim did not verify');record.reviewId=matches[0].reviewId;await persist();
|
|
245
276
|
}
|
|
246
277
|
if(kind==='exit'){
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if(record.exitSubgroupId&&record.exitSubgroupId!==exit.exitSubgroupId)fail('exit identity changed');
|
|
278
|
+
const exit=standaloneExit?await step('exit',inspectStandaloneExit,()=>exitUi.saveExit(exitInput)):await internal.readExit({clientId:input.clientId,enrollmentId:record.enrollmentId});
|
|
279
|
+
if(Object.hasOwn(record,'exitSubgroupId')&&record.exitSubgroupId!==exit.exitSubgroupId)fail('exit identity changed');
|
|
250
280
|
record.projectExitDate=exit.projectExitDate;record.exitSubgroupId=exit.exitSubgroupId;await persist();
|
|
251
281
|
}
|
|
252
282
|
for(const end of plan.ends){
|
|
@@ -565,7 +595,7 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
|
|
|
565
595
|
return {key:record.key,kind:record.kind,stage:record.stage,householdId:record.context.householdId,headClientId:input.headClientId,members:record.clientIds.map(clientId=>({clientId,key:memberKey(input.key,clientId),...structuredClone(record.members[clientId]??{})}))};
|
|
566
596
|
});
|
|
567
597
|
},
|
|
568
|
-
discover,createEnrollment:input=>run('entry',input),createInterim:input=>run('interim',input),
|
|
598
|
+
discover,createEnrollment:input=>run('entry',input),createInterim:input=>run('interim',input),exitEnrollment:input=>run('exit',input,{standaloneExit:true}),
|
|
569
599
|
async getStatus(input){
|
|
570
600
|
fields(input,['clientId','key']);if(!positive(input.clientId))fail('client ID required');key(input.key);
|
|
571
601
|
return store.withLock(scope(input.clientId),async()=>{
|
|
@@ -27,7 +27,7 @@ export interface HouseholdExitUi {
|
|
|
27
27
|
/** Reopen the saved member exit and read the persisted conditional fields, then cancel. */
|
|
28
28
|
readExitDetails(target:HouseholdExitTarget):Promise<HouseholdExitStoredDetails>;
|
|
29
29
|
validateExit(input:HouseholdExitUiInput):Promise<void>;
|
|
30
|
-
/** One save attempt only. Use through the durable exitHousehold workflow. */
|
|
30
|
+
/** One save attempt only. Use through the durable exitEnrollment or exitHousehold workflow. */
|
|
31
31
|
saveExit(input:HouseholdExitUiInput):Promise<void>;
|
|
32
32
|
}
|
|
33
33
|
/** UI port with fresh conditional choices and per-member readback. */
|
|
@@ -5,7 +5,7 @@ import {expectedExitDetails,rentalDestination} from './exit-details.mjs';
|
|
|
5
5
|
|
|
6
6
|
const label=value=>value.replace(/\s*\(HUD\)\s*$/,'').trim();
|
|
7
7
|
const id=value=>Number.isSafeInteger(value)&&value>0;
|
|
8
|
-
/** Low-level UI port. Use through exitHousehold, which
|
|
8
|
+
/** Low-level UI port. Use through exitEnrollment or exitHousehold, which journal before save.
|
|
9
9
|
* This port makes at most one Save click and never retries an uncertain write. */
|
|
10
10
|
export function createHouseholdExitUi({page}){
|
|
11
11
|
const connection=pageConnection(page),base=connection.moduleBaseUrl;
|
|
@@ -80,7 +80,9 @@ export function createHouseholdExitUi({page}){
|
|
|
80
80
|
if((await box.isChecked())!==desired.includes(clientId))throw Error('Exit household selection mismatch');
|
|
81
81
|
}
|
|
82
82
|
const selected=await page.locator('[id^="HouseholdsCheckboxTree.householdCheckbox."]:visible input:checked').evaluateAll(es=>es.map(e=>Number(e.parentElement.id.match(/\.client-(\d+)$/)?.[1])).sort((a,b)=>a-b));
|
|
83
|
-
|
|
83
|
+
// A standalone person's form can bind the target without rendering a household tree.
|
|
84
|
+
const implicit=await boxes.count()===0&&snapshot.members.length===1&&snapshot.members[0].clientId===input.clientId&&snapshot.members[0].enrollmentId===input.enrollmentId&&desired.length===1&&desired[0]===input.clientId;
|
|
85
|
+
if(!implicit&&JSON.stringify(selected)!==JSON.stringify(desired))throw Error('Exit household selection incomplete');
|
|
84
86
|
const parts=await page.evaluate(value=>{const d=new Date(value),local=new Date(d.getFullYear(),d.getMonth(),d.getDate(),d.getHours(),d.getMinutes(),d.getSeconds());if(+d!==+local)throw Error('Ambiguous local exit time');return {Month:String(d.getMonth()+1).padStart(2,'0'),Day:String(d.getDate()).padStart(2,'0'),Year:String(d.getFullYear()),Hour:String(d.getHours()%12||12),Minute:String(d.getMinutes()).padStart(2,'0'),Second:String(d.getSeconds()).padStart(2,'0'),'AM/PM':d.getHours()<12?'AM':'PM'};},input.exitDate);
|
|
85
87
|
for(const [part,value]of Object.entries(parts)){const field=page.getByLabel(`Exit Date ${part}`,{exact:true});if(['Month','Day','Year'].includes(part))await field.fill(value);else await field.selectOption({label:value});if(await field.inputValue()!==value)throw Error('Exit date did not match');}
|
|
86
88
|
for(const [field,name]of [['destination','Destination'],['reasonForLeaving','Reason for Leaving']])if(await page.getByLabel(name,{exact:true}).inputValue()!==chosen[field])throw Error('Exit choice did not match');
|
package/docs/capabilities.md
CHANGED
|
@@ -21,6 +21,7 @@ The generated client covers the pinned V2/V3 Swagger operations. The following f
|
|
|
21
21
|
| Later child or member | `addHouseholdMembers` | Must already belong to household; own later entry date, assessment, and DOB; existing dates preserved |
|
|
22
22
|
| Interims | Workflow `createInterim`, `createHouseholdInterim` or low-level GWT `createInterimUpdate` | Workflow populates answers; low-level helper creates only review/participant links. Selected household members supported |
|
|
23
23
|
| Household exit | `exitHousehold` with exit UI port | Selected members, staggered dates, subsidy/Other details, per-member exit assessments; all-member metadata uses one GWT save, assessments follow separately |
|
|
24
|
+
| Individual enrollment exit | `exitEnrollment` with enrollment reader and exit UI port | Standalone participant, verified entry journal, exit details and assessment, same-key reconciliation; synthetic regression coverage only, live acceptance pending |
|
|
24
25
|
| Head of household | Enrollment workflow guards | Missing or mid-enrollment replacement head rejected. Head cannot exit before remaining members. Retroactive replacement is not implemented |
|
|
25
26
|
| Verification records at entry | `readEnrollmentVerifications` | Reads generic assessment and filters intervals; complete-enrollment entry arrays can omit valid UI rows |
|
|
26
27
|
| Existing verification end dates | `api.subassessments.setEndDate` | Preserves row ID and other values, verifies result; no blind resend or delete/recreate |
|
package/docs/development.md
CHANGED
|
@@ -53,7 +53,7 @@ See [CHANGELOG.md](../CHANGELOG.md) for compatibility changes. This package is a
|
|
|
53
53
|
|
|
54
54
|
GitLab is the source of truth. Its push mirror copies branches and tags to `nmbgeek/communityservices-sdk` on GitHub. Commit changes, including `.github/workflows/publish.yml`, in GitLab. Do not edit the mirror or push independent commits there.
|
|
55
55
|
|
|
56
|
-
The GitHub workflow checks Node 22, Node 24, and Chromium using synthetic fixtures. A matching `v<version>` tag publishes the tested Chromium-job tarball as public `@choiceapps/communityservices-sdk`. Main pushes only verify. To recover an interrupted release, run `publish.yml` manually against the existing version tag. An identical existing artifact is verified without republishing; a different artifact under the same version fails. npm versions cannot be replaced.
|
|
56
|
+
The GitHub workflow uses Ubuntu 24.04 runners and checks Node 22, Node 24, and Chromium using synthetic fixtures. The runner version is explicit because the pinned Playwright version cannot install Chromium on Ubuntu 26.04. A matching `v<version>` tag publishes the tested Chromium-job tarball as public `@choiceapps/communityservices-sdk`. Main pushes only verify. To recover an interrupted release, run `publish.yml` manually against the existing version tag. An identical existing artifact is verified without republishing; a different artifact under the same version fails. npm versions cannot be replaced.
|
|
57
57
|
|
|
58
58
|
Configure the npm package's trusted publisher for GitHub owner `nmbgeek`, repository `communityservices-sdk`, workflow filename `publish.yml`, with publishing allowed and no environment name. The workflow runs on GitHub-hosted runners, grants `id-token: write` only to the publish job, and uses npm 11.19.1. It needs no `NPM_TOKEN` or `NODE_AUTH_TOKEN` secret. The package's repository URL points to this public mirror so npm can attach provenance. GitLab publication remains a separate job using `CI_JOB_TOKEN`.
|
|
59
59
|
|
package/docs/enrollments.md
CHANGED
|
@@ -164,6 +164,30 @@ Client locks coordinate overlapping household and single-client operations. Dura
|
|
|
164
164
|
|
|
165
165
|
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.
|
|
166
166
|
|
|
167
|
+
## Individual enrollment exits
|
|
168
|
+
|
|
169
|
+
Use `exitEnrollment` to end a standalone person's program enrollment. It requires a verified `createEnrollment` entry in the same durable store. Supply `enrollmentSession: session.enrollmentSession` and `exitUi: createHouseholdExitUi({page: session.page})` when constructing the workflow. The same exit UI port serves individual and household forms.
|
|
170
|
+
|
|
171
|
+
Call `discoverExit` with the person's client, provider, type and entry date, then build their assessment plan from `catalog.exit.definition`.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const exit = await workflow.exitEnrollment({
|
|
175
|
+
key: sourceExitId,
|
|
176
|
+
enrollmentKey: sourceEnrollmentId,
|
|
177
|
+
clientId,
|
|
178
|
+
exitDate,
|
|
179
|
+
destination: 'Rental by client, no ongoing housing subsidy',
|
|
180
|
+
reasonForLeaving: 'Completed program',
|
|
181
|
+
assessment: exitPlan,
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The workflow verifies the exact enrollment and sole participant through fresh GWT reads, checks exit dates against entry and interim dates, and validates the assessment before saving. It journals one exit save attempt and verifies the date, destination, reason and conditional details before writing the assessment. Individual exits can have a null `exitSubgroupId`; the result preserves that value. The journal pins any returned subgroup identity on retries.
|
|
186
|
+
|
|
187
|
+
Retain the same key, intent and store after failures. A lost save response triggers readback, never an automatic resend. Use `getStatus({clientId, key})` to inspect local progress. Exit metadata and assessment writes are separate stages, so a saved exit can remain pending assessment verification. `endDates` closes only the explicitly selected verification rows; enrollment exit does not automatically end every verification record.
|
|
188
|
+
|
|
189
|
+
For a person in a linked household enrollment, use `exitHousehold` with that person as the only selected member. `exitEnrollment` rejects linked groups to preserve household head coverage. Neither method ends household membership. See the [typed individual exit example](../examples/individual-exit-workflow.mts). Standalone exit coverage is synthetic; it has not yet been accepted against a live tenant.
|
|
190
|
+
|
|
167
191
|
## Household exits
|
|
168
192
|
|
|
169
193
|
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.
|
|
@@ -183,6 +207,8 @@ const exit = await workflow.exitHousehold({
|
|
|
183
207
|
|
|
184
208
|
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.
|
|
185
209
|
|
|
210
|
+
To end the entire household's program enrollment, include every enrolled member who has not already exited. Already exited members retain their earlier dates. The existing head-coverage checks still apply.
|
|
211
|
+
|
|
186
212
|
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.
|
|
187
213
|
|
|
188
214
|
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.
|
package/docs/reference.md
CHANGED
|
@@ -36,7 +36,7 @@ Runtime exports: `AttachmentConflictError`, `CoordinatedEntryEventConflictError`
|
|
|
36
36
|
| Profile fields and Sex | [Client profile](../browser/client-profile.d.mts) | `read`, `update` |
|
|
37
37
|
| Calendar DOB | [Birth dates](../browser/date-of-birth.d.mts) | `read`, `validate`, `verify`, `update` |
|
|
38
38
|
| Provider assessment defaults | [Discovery](../browser/program-discovery.d.mts) | `listAssessments`, `getProviderAssessments` |
|
|
39
|
-
| Enrollment and household lifecycle | [Workflow](../browser/enrollment-workflow.d.mts) | Discovery, entry, member additions, interim,
|
|
39
|
+
| Enrollment and household lifecycle | [Workflow](../browser/enrollment-workflow.d.mts) | Discovery, entry, member additions, interim, `exitEnrollment`, `exitHousehold`, journal status; `EntryExitType` and per-type `assessmentLocations` in [enrollments](enrollments.md) |
|
|
40
40
|
| Entry-date and exit UI ports | [Member entry](../browser/member-entry-date.d.mts), [exit](../browser/household-exit.d.mts) | Fresh reads and targeted completion |
|
|
41
41
|
| Services and recovery diagnostics | [Service recording](../browser/service-recording.d.mts) | `record`, `recordBulk`, Funding Sources, `RecoveryDiagnostic` |
|
|
42
42
|
| SSVF conditional rules | [SSVF types](../browser/ssvf-rules.d.mts) | [Field rules](../SSVF.md) and dated option snapshots |
|
package/examples/README.md
CHANGED
|
@@ -14,6 +14,7 @@ Every file here ships with the package and imports the public `@choiceapps/commu
|
|
|
14
14
|
| [verification-snapshot.ts](verification-snapshot.ts) | Read assessment records effective at enrollment | Exported function; caller supplies assessment context |
|
|
15
15
|
| [program-discovery.ts](program-discovery.ts) | Program and VA-entry assessment discovery | Exported function; authenticated page required |
|
|
16
16
|
| [enrollment-workflow.mts](enrollment-workflow.mts) | Single entry and interim assessment plans | Exported function; caller supplies plans and dependencies |
|
|
17
|
+
| [individual-exit-workflow.mts](individual-exit-workflow.mts) | Standalone program enrollment exit and assessment | Verified entry key, enrollment reader, exit UI port and durable store required |
|
|
17
18
|
| [household-enrollment-workflow.mts](household-enrollment-workflow.mts) | Per-member enrollment and review | Existing household and per-member answer plans required |
|
|
18
19
|
| [household-lifecycle-workflow.mts](household-lifecycle-workflow.mts) | Selected exits and a later child entry | Household membership precedes program attachment |
|
|
19
20
|
| [household-exit-workflow.mts](household-exit-workflow.mts) | Household exit with destination details | Explicit exit dates and per-member plans required |
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEnrollmentWorkflow,
|
|
3
|
+
type EnrollmentWorkflowOptions,
|
|
4
|
+
type ExitEnrollmentInput,
|
|
5
|
+
type ExitEnrollmentResult,
|
|
6
|
+
} from '@choiceapps/communityservices-sdk/browser';
|
|
7
|
+
|
|
8
|
+
/** Supply enrollmentSession and exitUi, and the durable store containing the
|
|
9
|
+
* verified createEnrollment operation identified by input.enrollmentKey.
|
|
10
|
+
* Build input.assessment from workflow.discoverExit before calling this function. */
|
|
11
|
+
export async function exitIndividualEnrollment(
|
|
12
|
+
dependencies:EnrollmentWorkflowOptions,
|
|
13
|
+
input:ExitEnrollmentInput,
|
|
14
|
+
):Promise<ExitEnrollmentResult> {
|
|
15
|
+
return createEnrollmentWorkflow(dependencies).exitEnrollment(input);
|
|
16
|
+
}
|
package/package.json
CHANGED