@clipboard-health/ai-rules 2.15.6 → 2.15.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/ai-rules",
3
- "version": "2.15.6",
3
+ "version": "2.15.8",
4
4
  "description": "Pre-built AI agent rules for consistent coding standards.",
5
5
  "keywords": [
6
6
  "ai",
@@ -0,0 +1,962 @@
1
+ ---
2
+ name: clipboard-testing
3
+ description: End-to-end testing playbook for Clipboard Health changes. Use when the user wants to verify, exercise, or set up test data for a backend or frontend change against a live environment — "test my change end-to-end", "verify this works in dev", "create a test workplace / worker / shift", "get a shift through to paid / invoiced", "prove the API change works". Defaults to the `development` AWS environment, API-first (cbh CLI tokens + curl). The skill knows enough to run the core happy-path flow (workplace → worker → shift → clock in/out → pay → invoice) autonomously; for anything else, it orients around the codebase and asks the user for missing directories.
4
+ allowed-tools: Bash, Read, Grep, Glob
5
+ ---
6
+
7
+ # Clipboard Testing
8
+
9
+ This skill lets you verify Clipboard Health changes end-to-end against `development`. It is opinionated about two things:
10
+
11
+ - **API-first.** curl against the dev gateway with tokens from `cbh auth gentoken`. No packages to install.
12
+ - **Concepts over memorized payloads.** Field shapes and validation rules change. The skill teaches you _what owns what_ and _where to read current truth_ — not a fixed cookbook.
13
+
14
+ The one area where the skill carries enough detail to run alone is the **core happy-path flow** (create workplace → create worker → create shift → book → clock in/out → trigger pay → generate invoice). Everything else is concept + controller pointer + "read the file before you call it".
15
+
16
+ ## Hard guardrails
17
+
18
+ `development` only. The recipes here mint S2S tokens, create Cognito users, and move test-mode money — running them in any other environment is out of scope and can have real consequences.
19
+
20
+ 1. **Env literal must be `development`.** If the user asks for `staging`, `prod-shadow`, `prod-recreated`, `production`, or any other env, refuse and stop. Pin in scripts:
21
+
22
+ ```bash
23
+ ENV=development
24
+ [ "$ENV" = "development" ] || { echo "REFUSE: dev-only" >&2; exit 1; }
25
+ ```
26
+
27
+ 2. **HTTPS host allowlist.** Every curl or browser request must match one of:
28
+ - `*.development.clipboardhealth.org` (gateway + admin-webapp + hcp-webapp + home-health-api)
29
+ - `mailpit.tools.cbh.rocks`
30
+ - `sandbox.invoiced.com` (Invoiced.com sandbox)
31
+
32
+ ```bash
33
+ case "$URL" in
34
+ https://*.development.clipboardhealth.org/*|https://mailpit.tools.cbh.rocks/*|https://sandbox.invoiced.com/*) ;;
35
+ *) echo "REFUSE: URL outside dev allowlist: $URL" >&2; exit 1 ;;
36
+ esac
37
+ ```
38
+
39
+ 3. **S2S tokens are dev-only.** `cbh auth gentoken client <env> <service>` bypasses user auth — a `payment-service` token can move real money. Hard-code `development` in the call; never parameterize the env across envs.
40
+
41
+ 4. **Mailpit links.** The trusted sender in dev is `no-reply+dev@updates.clipboardworks.com`. Filter on that `from:` AND verify the link host against rule 2 before navigating. Untrusted senders can plant lookalike magic-links into the shared mailbox.
42
+
43
+ 5. **Token output.** Don't paste tokens or full decoded JWT payloads into chat; pipe JWT decode through `jq` to project only the claims you need.
44
+
45
+ 6. **Privileged primitives** — confirm IDs before invoking, even in dev: `POST /api/user/create`, `POST /payment/accounts` (with `gatewayAccountCreationIsEnabled: false`), `POST /payment/accounts/:agentId/transfers`, `POST /payment/sendInvoices`. Use throwaway emails like `+test-<timestamp>@clipboardhealth.com` for user creation.
46
+
47
+ ## What this skill is for
48
+
49
+ Orient around the Clipboard codebase and:
50
+
51
+ 1. Pick the right **service** for a given change.
52
+ 2. Pick the right **actor / token** for a given endpoint.
53
+ 3. Find the right **controller file** for the current payload shape and guard.
54
+ 4. Run the **core flow** on your own to set up baseline test data.
55
+ 5. **Verify** a change via API read-back, Mailpit, Datadog, or the admin webapp.
56
+
57
+ ## When to ask the user
58
+
59
+ - If a recipe in the "concepts" section doesn't include a curl and you need one, **ask the user whether to grep the controller in their workspace, or whether they can paste a known-working request.**
60
+ - If a sibling repo referenced in the repo map is not present under `$CBH_ROOT`, **ask the user to point you to its path** (they may have it somewhere else).
61
+ - If the skill instructs you to "read the controller to confirm the body shape" and you can't find the file, **ask** before guessing.
62
+ - **Never** fabricate endpoint paths, field names, or guard decorators. When uncertain, grep or ask.
63
+
64
+ ## Repository map
65
+
66
+ Default root assumed: `$CBH_ROOT=/Users/<me>/repos/cbh` (adjust — the skill should detect `$CWD` and ask if the path differs).
67
+
68
+ | Repo | What lives there |
69
+ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
70
+ | `clipboard-health/` | **Main backend monolith (aka backend-main).** Shifts, workers (HCP), workplaces (facilities), invites, shift blocks, bookability rules, invoicing triggers. Mongo. |
71
+ | `payment-service/` | Payments + bonuses. Transfers (Clipboard Stripe → worker Express), payouts (Express → external), bonus entities, external payment accounts, payment blockers. **Own Mongo — source of truth for payment state.** |
72
+ | `home-health-api/` | Home Health product: cases, visits (typed), visit occurrences. **Postgres.** Standalone NestJS. Not behind the gateway. |
73
+ | `documents-service-backend/` | Documents (presigned uploads, approval) and licenses (incl. NLC `multiState` flag). Own deployment + Datadog service (`document-service`). |
74
+ | `shift-reviews-service/` | Post-shift ratings + **preferred workers** (reasons: `FAVORITE`, `RATING`, `INTERNAL_CRITERIA`). Postgres. |
75
+ | `attendance-policy/` | Clock-in windows **plus** attendance scores, score adjustments, restrictions, market-level config. Controllers: `/policies`, `/restrictions`, `/scores`, `/workers`, `/markets`. |
76
+ | `urgent-shifts/` | **Archived** — repo is read-only. Urgency-tier constants (`NCNS`, `LATE_CANCELLATION`, `LAST_MINUTE`) now live in `clipboard-health/src/services/urgentShifts/`. |
77
+ | `worker-app-bff/` | Worker-facing BFF. **Read-only / proxy** for most domain data. Don't send writes here. |
78
+ | `worker-service-backend/` | Worker-service endpoints (worker-side reads and some writes). |
79
+ | `cbh-api-gateway/` | API gateway config — routes `/api`, `/payment`, `/worker`, `/license-manager`, `/reviews`, etc. |
80
+ | `license-manager/` | License lifecycle + state sync (backing the documents license flow). |
81
+ | `cbh-backend-notifications/` | Notification dispatch (push, email, SMS). |
82
+ | `cbh-chat-service/` | In-app chat between workers and workplaces. |
83
+ | `cbh-admin-frontend/` | **admin-webapp** — serves both **CBH employees** and **facility users** (mobile-friendly). UI branches on who's logged in. |
84
+ | `admin-app/` | Legacy admin frontend (being superseded by `cbh-admin-frontend`). Check this only if something is missing above. |
85
+ | `cbh-mobile-app/` | Worker mobile app (Ionic + native). Also exposes a dev PWA at `hcp-webapp.development.clipboardhealth.org`. |
86
+ | `clipboard-facility-app/` | Legacy Flutter facility app (being phased out; replaced by `admin-webapp`). Usually don't need this. |
87
+ | `cbh-core/packages/cli/` | The `cbh` CLI — `auth gentoken`, `seed-data`, `local-package`, `dev up`. |
88
+ | `cbh-core/packages/testing-e2e-admin-app/` | Canonical **reference payloads** and **AdminService method shapes** — read but do not import at runtime; shapes can be stale. |
89
+ | `cbh-infrastructure/` | Terraform. URLs, Cognito pools, SES/Mailpit, network firewalls. |
90
+
91
+ **Long-tail repos** that might be relevant for specific features — `open-shifts/`, `shift-verification/`, `pricing-service/`, `cbh-location-service/`, `authentication/`, `cbh-evidence/`, `invite-generator/`. Ask the user which domain a change touches before guessing.
92
+
93
+ If any of these aren't present in `$CBH_ROOT`, ask the user.
94
+
95
+ ## Actors, tokens, apps
96
+
97
+ Three human Cognito App Clients + one impersonation variant + S2S.
98
+
99
+ | clientName | Actor | App surface |
100
+ | ------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------- |
101
+ | `admin-app` | CBH employee | `admin-webapp.development.clipboardhealth.org` (also serves facility users) |
102
+ | `worker-app` | Worker (HCP) | `hcp-webapp.development.clipboardhealth.org` + native mobile |
103
+ | `workplace-app` | Facility user (legacy) | Flutter app being phased out. New facility users log into `admin-webapp` via `admin-app` flow. |
104
+ | `worker-app-impersonated` | Employee acting as a worker | admin-webapp impersonation mode |
105
+
106
+ Token flavours via `cbh auth gentoken`:
107
+
108
+ ```bash
109
+ # Human (Cognito user)
110
+ cbh auth gentoken user development <email> [-n <clientName>] # default -n admin-app
111
+
112
+ # Firebase ID token (frontend AUTH_TOKEN handoff — rarely needed for API tests)
113
+ cbh auth gentoken email development <email>
114
+
115
+ # Service-to-service
116
+ cbh auth gentoken client development backend-main
117
+ cbh auth gentoken client development payment-service
118
+ ```
119
+
120
+ All dev signup and login emails land in **Mailpit** at `https://mailpit.tools.cbh.rocks/` (basic-auth; creds in 1Password). Trusted sender: `no-reply+dev@updates.clipboardworks.com`. Useful subjects: `"Your Clipboard sign-in link"` (magic link), `"Your Clipboard login code"` (OTP for phone-aliased emails like `<10-digits>.phone.email@testmail.com`), `"Your <workplace name> Shift is Booked!"`, `"... was cancelled by the workplace."`.
121
+
122
+ **`USER_TYPE_NOT_ALLOWED` minting cross-type tokens** — `cbh auth gentoken user … -n worker-app` (or `-n workplace-app`) for an EMPLOYEE-typed Cognito user returns `Error initiating custom challenge MAKE_TEST_TOKEN: DefineAuthChallenge failed with error USER_TYPE_NOT_ALLOWED`. You can't reuse your admin email as a worker or workplace-user; create a dedicated account for that role.
123
+
124
+ ## Environment reference — `development` only
125
+
126
+ | Service | Base URL / mount |
127
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
128
+ | API gateway | `https://apigateway.development.clipboardhealth.org` (`$API_BASE`) |
129
+ | backend-main | `$API_BASE/api` |
130
+ | payment-service | `$API_BASE/payment` |
131
+ | worker-service | `$API_BASE/worker` |
132
+ | license-manager | `$API_BASE/license-manager` |
133
+ | documents (REST) | `$API_BASE/api/documents` |
134
+ | documents (GraphQL) | `$API_BASE/docs/graphql` |
135
+ | shift-reviews | `$API_BASE/reviews` |
136
+ | attendance-policy | `$API_BASE/attendance-policy/` (gateway-rewritten in dev) |
137
+ | home-health-api | `$API_BASE/home-health-api` (gateway-rewritten). Controllers mount both `/api/v1/...` and `/home-health-api/api/v1/...`; through the gateway, use the `/home-health-api/...` form. |
138
+ | Invoiced.com sandbox | `https://sandbox.invoiced.com` (fully stubbed in dev) |
139
+ | Mailpit | `https://mailpit.tools.cbh.rocks` |
140
+ | Admin webapp | `https://admin-webapp.development.clipboardhealth.org` |
141
+ | Worker PWA | `https://hcp-webapp.development.clipboardhealth.org` |
142
+
143
+ ## Prerequisites
144
+
145
+ ```bash
146
+ # Current cbh CLI
147
+ cbh --version # expect 8.x
148
+ # upgrade: npm install --global @clipboard-health/cli@latest
149
+
150
+ # Dev VPN — required for direct development MongoDB access; gateway and Mailpit are reachable without it
151
+
152
+ # Smoke test
153
+ cbh auth gentoken client development backend-main -q | head -c 40 ; echo
154
+
155
+ # Install jq
156
+ jq --version
157
+ ```
158
+
159
+ Ask the user for:
160
+
161
+ - The admin email they want to act as (e.g. `e2e@clipboardhealth.com` or their own).
162
+ - Mailpit credentials if you need to drive signup/login.
163
+ - `$CBH_ROOT` if not `/Users/<me>/repos/cbh/`.
164
+
165
+ ---
166
+
167
+ # Core flow — self-sufficient
168
+
169
+ This section carries enough to drive the happy path **without reading further**. Each step captures an ID used by later steps. Pointed at workplace type **LTC** (the dominant marketplace segment).
170
+
171
+ ## Setup
172
+
173
+ ```bash
174
+ export API_BASE=https://apigateway.development.clipboardhealth.org
175
+ export ADMIN_WEBAPP=https://admin-webapp.development.clipboardhealth.org
176
+
177
+ # Admin (CBH employee) — used for most setup writes
178
+ export ADMIN_EMAIL=<ask user>
179
+ export ADMIN_TOKEN=$(cbh auth gentoken user development "$ADMIN_EMAIL" -q)
180
+
181
+ # Service-to-service — used for payment-service writes initiated from backend-main
182
+ export S2S_BACKEND_MAIN=$(cbh auth gentoken client development backend-main -q)
183
+
184
+ # Admin userId — claim on the token, no API call needed
185
+ PAYLOAD=$(echo "$ADMIN_TOKEN" | cut -d. -f2); LEN=$(( ${#PAYLOAD} % 4 ))
186
+ [ $LEN -ne 0 ] && PAYLOAD="$PAYLOAD$(printf '=%.0s' $(seq 1 $((4-LEN))))"
187
+ export ADMIN_USERID=$(echo "$PAYLOAD" | tr '_-' '/+' | { base64 -d 2>/dev/null || base64 -D; } | jq -r '."custom:cbh_user_id"')
188
+ ```
189
+
190
+ The worker token is minted **after** Step 2 (see Step 5). `POST /api/user/create` creates the Cognito user as a side effect, so no hcp-webapp signup + Mailpit dance is needed.
191
+
192
+ `ADMIN_USERID` is used as `addedBy` / `sessionUser` / `adminId` in many downstream calls (the `constants.ts` default is stale). The token-claim path above is preferred — if you ever need an API fallback, hit `GET /api/user/getByEmail?email=…` (URL-encoded) with `$ADMIN_TOKEN`.
193
+
194
+ **Pick an admin with an `EmployeeProfile` doc.** The plain `@AllowClipboardHealthEmployees()` decorator passes for any JWT with `custom:user_types: EMPLOYEE` (so workplace creation works for almost any CBH email). But `ShiftCreateAuthorizer` (`src/modules/shifts/entrypoints/internal/shift-create.authorizer.ts:54`) additionally requires an `EmployeeProfile` keyed by your `userId`, and many real CBH dev users don't have one. Symptom: shift create returns generic `403 {"code":"PermissionDenied","detail":"Forbidden resource"}` with no detail. **Default to `e2e@clipboardhealth.com` for shift writes** unless the user explicitly hands you another admin email and confirms it has an EmployeeProfile.
195
+
196
+ **Invoice-only fast path:** if all the user wants is a billable shift to invoice (no real money flow, no Stripe, no payouts), you can skip Steps 4 (Stripe), 5 (worker token isn't needed), and 9 (transfer/payout). Minimum chain: Step 1 → 2 → 3 → 6 → 7 → "test-data shortcut" in Step 8 (`PUT /api/shift/put` with `verified:true`) → re-run Step 7 if `agentId` got cleared → Step 10 with `includeUnverifiedInErrors:true` and segment `not-generated-with-errors`.
197
+
198
+ ## Step 1 — Create a workplace (LTC)
199
+
200
+ Validator at `clipboard-health/src/modules/facilityProfile/services/middlewares/createFacilityProfileValidator.middleware.ts`. Required: `rushFee`, `lateCancellation`, `netTerms`, `disputeTerms`, `ratesTable`, `holidayFee`, `sentHomeChargeHours`, plus a rate entry for **every** qualification enabled for the workplace type (the validator iterates qualifications and `check("rates.{q}").exists()` — missing rate keys are reported as `"Invalid rate - X doesn't exist"`, which means _missing_, not _unrecognized_). For LTC today that's also: `NP`, `QMAP`, `Server`, `Janitor`, `Site Lead`, `Medical Aide`, `Medical Technician`, `Respiratory Therapist`, `Dental Hygienist`, `Dental Assistant`, `CNA On Call`. **`salesforceID` regex: exactly 15 or 18 alphanumeric chars** (`/^[0-9a-zA-Z]{15}([0-9a-zA-Z]{3})?$/`). Response uses `id`, not `_id`.
201
+
202
+ **1099 coverage testing**: if your test needs 1099 policy coverage, the workplace must be in `CA / FL / MI / NJ` (the four convalescent-home states with the doc-requirement feature flag enabled) and you'll then `POST /api/workplaces/:workplaceId/1099-policy/entities` separately — see the "1099 Policy coverage" concept section.
203
+
204
+ ```bash
205
+ SFID=$(printf "INVTEST%08d" $((RANDOM))) # 15 chars
206
+ curl -sS -X POST "$API_BASE/api/facilityprofile/create" \
207
+ -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
208
+ -d @- <<JSON | jq '{workplaceId: .id, message}'
209
+ {
210
+ "name": "test facility core-flow",
211
+ "email": "core-flow@clipboardhealth.com",
212
+ "phone": "4155550100",
213
+ "type": "Long Term Care",
214
+ "fullAddress": {
215
+ "streetNumber": "14", "streetName": "Grove St", "city": "San Francisco",
216
+ "region": "San Francisco County", "state": "California", "stateCode": "CA",
217
+ "country": "United States", "countryCode": "US", "postalCode": "94102",
218
+ "formatted": "14 Grove St, San Francisco, CA 94102, USA"
219
+ },
220
+ "geoLocation": {"type": "Point", "coordinates": [-122.4153089, 37.7791078]},
221
+ "metropolitanStatisticalArea": "San Francisco-Oakland-Berkeley, CA",
222
+ "manualMsa": false,
223
+ "tmz": "America/Los_Angeles",
224
+ "rates": {
225
+ "CNA": 30, "LVN": 45, "NURSE": 50, "RN": 55, "CAREGIVER": 25,
226
+ "CHEF": 39, "Chef": 39, "COOK": 28, "HOUSEKEEPER": 22,
227
+ "Dental Hygienist": 61, "Dietary Aide": 18, "Dining Assistant": 20,
228
+ "Dental Assistant": 55, "Dishwasher": 18, "Janitor": 25,
229
+ "Medical Aide": 22, "Medical Technician": 26,
230
+ "NP": 60, "QMAP": 30, "Respiratory Therapist": 40,
231
+ "Server": 18, "Site Lead": 28, "CNA On Call": 32
232
+ },
233
+ "rushFee": {"differential": 26, "period": "9"},
234
+ "lateCancellation": {"period": "2", "feeHours": "9"},
235
+ "netTerms": "26",
236
+ "disputeTerms": "98",
237
+ "ratesTable": {
238
+ "sunday": {"am": "94","pm": "47","noc": "53"},
239
+ "monday": {"am": "68","pm": "29","noc": "45"},
240
+ "tuesday": {"am": "57","pm": "88","noc": "62"},
241
+ "wednesday": {"am": "29","pm": "99","noc": "12"},
242
+ "thursday": {"am": "100","pm": "64","noc": "44"},
243
+ "friday": {"am": "60","pm": "93","noc": "19"},
244
+ "saturday": {"am": "7","pm": "5","noc": "20"}
245
+ },
246
+ "holidayFee": [],
247
+ "salesforceID": "$SFID",
248
+ "sentHomeChargeHours": "92"
249
+ }
250
+ JSON
251
+ ```
252
+
253
+ Capture `WORKPLACE_ID`. Reference body: `cbh-core/packages/testing-e2e-admin-app/src/lib/constants.ts → DEFAULT_CREATE_LONG_TERM_CARE_FACILITY_REQUEST_BODY`. The qualification key strings must match `HcpWorkerType` enum values in `cbh-core/packages/testing-e2e-admin-app/src/lib/admin-interface.ts` (e.g. `MEDICAL_TECHNICIAN = "Medical Technician"`).
254
+
255
+ ## Step 2 — Create a worker
256
+
257
+ Endpoint is **`POST /api/user/create`** (handler: `clipboard-health/src/modules/user/controllers/user.controller.ts → createUser`, contract: `userContract.createUser`). The legacy `/api/agentProfile/bulk` is gone.
258
+
259
+ The controller's `userManipulationService.createAgent` calls `cognitoService.createCognitoUser` at the end of the flow, so you do **not** need to pre-sign-up via hcp-webapp + Mailpit. The Cognito user is created in the same call. Note the contract schema (`createUserRequestSchema`) doesn't actually declare `password` — the field is optional today, but mint the worker token via `cbh auth gentoken user development $WORKER_EMAIL -n worker-app -q` immediately after creation to confirm Cognito is set up.
260
+
261
+ **Alternative — `POST /api/testHelpers/createUser`** (`src/tests/helpers/api/testHelpers/controller.ts:785`, dev-only via `meta().dev`). Same `userManipulationService.createAgent` underneath, so it produces an equivalent `agentProfile` + Cognito user. Use it when you don't have an admin Cognito user handy: auth is the literal `TEST_HELPER_API_KEY` shared secret as the `Authorization` header (no `Bearer` re-wrapping — pass it verbatim), sourced from 1Password vault **"Engineering - Shared"** → item `REACT_APP_TEST_HELPER_API_KEY`. The value lives in the item's `notesPlain` field (not `password`); strip the `REACT_APP_TEST_HELPER_API_KEY=` prefix and pass the remaining `Bearer …` string as the `Authorization` header. Body shape is the `CreateUserBody` interface in the same controller file (looser than `/api/user/create`'s contract).
262
+
263
+ `ADMIN_USERID` is already exported from Setup. (Used as `addedBy` and `sessionUser` here — the constants.ts default `60841c3970071101613e1c50` is stale.)
264
+
265
+ Then:
266
+
267
+ ```bash
268
+ export WORKER_EMAIL=invoice-test-$(date +%s)@clipboardhealth.com
269
+ PHONE=$(printf "415555%04d" $((RANDOM % 10000)))
270
+ REFCODE=$(LC_ALL=C tr -dc A-Z0-9 < /dev/urandom | head -c 8)
271
+ # Paste the full encrypted SSN blob from:
272
+ # cbh-core/packages/testing-e2e-admin-app/src/lib/constants.ts (DEFAULT_CREATE_HCP_REQUEST_BODY.fullSocialSecurityNumber)
273
+ SSN_BLOB='<paste full blob here>'
274
+
275
+ curl -sS -X POST "$API_BASE/api/user/create" \
276
+ -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
277
+ -d @- <<JSON | jq '{workerId: ._id, email, phone: .agent.phone}'
278
+ {
279
+ "email": "$WORKER_EMAIL",
280
+ "type": "AGENT",
281
+ "addedBy": "$ADMIN_USERID",
282
+ "tmz": "America/Los_Angeles",
283
+ "firstName": "Test", "lastName": "Worker", "name": "Test Worker",
284
+ "phone": "$PHONE",
285
+ "dob": "1998-08-06",
286
+ "employmentStatus": "1099",
287
+ "referralCode": "$REFCODE",
288
+ "fullSocialSecurityNumber": "$SSN_BLOB",
289
+ "address": {
290
+ "streetNumber": "12", "streetName": "Grove Street", "city": "San Francisco",
291
+ "region": "San Francisco County", "state": "California", "stateCode": "CA",
292
+ "country": "United States", "countryCode": "US", "postalCode": "94102",
293
+ "formatted": "12 Grove St, San Francisco, CA 94102, USA",
294
+ "metropolitanStatisticalArea": "San Francisco-Oakland-Berkeley, CA",
295
+ "manualMsa": false
296
+ },
297
+ "geoLocation": {"coordinates": [-122.4152307, 37.7788487], "type": "Point"},
298
+ "license": {"state": "California"}
299
+ }
300
+ JSON
301
+ ```
302
+
303
+ Capture `WORKER_ID`. **All workers are 1099 today** — but the field IS still required by the request schema; pass `"employmentStatus": "1099"`. The error path `BadRequestException("Error while creating user")` swallows the underlying reason — to debug, check Datadog (the controller logs the full error before re-throwing).
304
+
305
+ **Workers start in `ONBOARDING` stage**, not `ENROLLED`. Bookability rules block `/api/shifts/claim` for ONBOARDING workers. For test data, prefer the admin-assign path (Step 7) which can override most rules.
306
+
307
+ **"Email address already in use" — recover the existing worker.** If `/api/user/create` returns `400 "Email address already in use"`, the worker already exists. Look it up by email — `agentprofile/search` and `agentProfile/search` reject email input with `"Must be a valid ObjectId"` (they take a userId), the right one is `GET /api/user/agentSearch?searchInput=<email>` (CBH-employee gated). Response surfaces both `_id` (agentProfile document) and `userId` (User document). **For shift `agentId`, use `userId`** — that's what the shift's `agentId` field references.
308
+
309
+ ```bash
310
+ WORKER_USERID=$(curl -sS "$API_BASE/api/user/agentSearch" -G \
311
+ --data-urlencode "searchInput=$WORKER_EMAIL" \
312
+ -H "Authorization: Bearer $ADMIN_TOKEN" | jq -r '.list[0].userId')
313
+ ```
314
+
315
+ ## Step 3 — Set qualification + create license
316
+
317
+ Two separate writes. Both required before assignment — `WORKER_MISSING_LICENSE` ("agent qualification doesn't meet requirements") fires if either is missing, even when assigning with `override: true`.
318
+
319
+ ```bash
320
+ # (a) Set the agentProfile.qualification field
321
+ curl -sS -X PUT "$API_BASE/api/agentprofile/put" \
322
+ -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
323
+ -d "{\"agentId\":\"$WORKER_ID\",\"qualification\":\"CNA\",\"licensedStates\":[\"CA\"]}" | jq
324
+
325
+ # (b) Create a real license in license-manager (state "Any" + multiState=false works for any-state shifts)
326
+ curl -sS -X POST "$API_BASE/license-manager/workers/$WORKER_ID/licenses" \
327
+ -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
328
+ -d '{"multiState":false,"status":"ACTIVE","state":"Any","qualification":"CNA","number":"1234567890","expiresAt":"2027-09-17T23:59:59-07:00"}' | jq
329
+ ```
330
+
331
+ The contractor-agreement signing (`PATCH $API_BASE/worker/agentprofile/signContractorAgreement` — note: lives on **worker-service**, takes the worker token, not admin) is only needed for the worker-self-claim path. The admin-assign override path skips it. If you need it later: body is `{agreementVersion:"V6", signature:"<name>"}`.
332
+
333
+ ## Step 4 — Connect Stripe (or skip it explicitly)
334
+
335
+ Two choices in dev. **Ask the user which they want** before proceeding. Either is fine for most flows; pick based on whether you need to exercise transfers/payouts.
336
+
337
+ **Option A (default for non-payout flows): bypass Stripe with `gatewayAccountCreationIsEnabled: false`.**
338
+
339
+ `POST /payment/accounts` with the bypass flag creates the `Account` doc directly with `status: "Instant Payouts Enabled"` and `enabled: true` — no Stripe call, no Express onboarding. Bonus creation works against it; transfers and payouts will fail downstream because there's no real Stripe account behind it. Mongoose validation still requires a non-empty `accountId`, so pass any synthetic value.
340
+
341
+ The `allowWorkerToCreateAccount` authorizer requires `principal.userId === bodyData.agentId`, so call this **as the worker** (uses `$WORKER_TOKEN` from Step 5 — mint it first if you haven't):
342
+
343
+ ```bash
344
+ curl -sS -X POST "$API_BASE/payment/accounts" \
345
+ -H "Authorization: Bearer $WORKER_TOKEN" -H "Content-Type: application/json" \
346
+ -d "{\"agentId\":\"$WORKER_USERID\",\"email\":\"$WORKER_EMAIL\",\"gatewayAccountCreationIsEnabled\":false,\"accountId\":\"acct_test_$WORKER_USERID\"}" | jq
347
+ ```
348
+
349
+ **Option B: real Stripe Express onboarding (needed if you're testing transfers/payouts).**
350
+
351
+ Call `POST /payment/accounts` _without_ the bypass flag (creates a real Stripe Express account in the sandbox), then `POST /payment/accounts/:id/generate-express-account-link` with `{ "returnUrl": "<any-https>" }`. Surface the returned URL to the user — they complete Express onboarding in the browser (you cannot fill the Stripe-hosted form on their behalf). Once they're back, retry the original flow.
352
+
353
+ Reference: `AdminService.createPaymentAccountForHcp` in `cbh-core/packages/testing-e2e-admin-app/src/lib/admin-service.ts` is the canonical orchestration; `payment-service/src/app/accounts/accounts.controller.ts:204,360` for the create + link-generation endpoints.
354
+
355
+ ## Step 5 — Mint the worker token
356
+
357
+ ```bash
358
+ export WORKER_TOKEN=$(cbh auth gentoken user development "$WORKER_EMAIL" -n worker-app -q)
359
+ ```
360
+
361
+ ## Step 6 — Create a shift
362
+
363
+ Two endpoints work today:
364
+
365
+ - **Legacy `POST /api/shift/create`** (singular `shift`) — flat body, schema `shiftCreateBodySchema` in `clipboard-health/packages/contract-backend-main/src/lib/shifts.contract.ts`. Required: `start`, `end`, `agentReq`, `admin`, `facilityId`. Max 17h between start and end. Response is the raw shift document with `_id`.
366
+ - **New `POST /api/v3/shifts`** — JSON:API body, contract `shiftContract.create` in `node_modules/@clipboard-health/contract-backend-main/src/lib/shiftV3.contract.ts`, controller `src/modules/shifts/entrypoints/shift-create.controller.ts`. The comment in that controller says it's "going to replace the old POST /api/shift/create endpoint". Response is JSON:API `{ data: { id, attributes: { schedule, requirements, charges, pay, ... } } }`.
367
+
368
+ Both routes are gated by the same `ShiftCreateAuthorizer` — see the **EmployeeProfile gotcha** in Setup. If your admin lacks an `EmployeeProfile` you'll get `403 {"code":"PermissionDenied","detail":"Forbidden resource"}` regardless of which path you pick.
369
+
370
+ Legacy (still works, no JSON:API ceremony):
371
+
372
+ ```bash
373
+ START=$(date -u -v-10H +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -u -d '-10 hours' +%Y-%m-%dT%H:%M:%S.000Z)
374
+ END=$(date -u -v-2H +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -u -d '-2 hours' +%Y-%m-%dT%H:%M:%S.000Z)
375
+ # Use a past window for invoice/test-data flows; future window for live booking.
376
+
377
+ curl -sS -X POST "$API_BASE/api/shift/create" \
378
+ -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
379
+ -d "{\"start\":\"$START\",\"end\":\"$END\",\"agentReq\":\"CNA\",\"admin\":true,\"facilityId\":\"$WORKPLACE_ID\"}" \
380
+ | jq '{shiftId: ._id, charge, pay, time}'
381
+ ```
382
+
383
+ v3 (preferred for new flows; if your change touches the new endpoint, exercise it directly):
384
+
385
+ ```bash
386
+ curl -sS -X POST "$API_BASE/api/v3/shifts" \
387
+ -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
388
+ -d "{\"data\":{\"type\":\"shift\",\"attributes\":{\"schedule\":{\"startAt\":\"$START\",\"endAt\":\"$END\",\"timeSlot\":\"am\"},\"requirements\":{\"qualificationName\":\"CNA\"}},\"relationships\":{\"workplace\":{\"data\":{\"id\":\"$WORKPLACE_ID\",\"type\":\"workplace\"}}}}}" \
389
+ | jq '{shiftId: .data.id, charges: .data.attributes.charges, pay: .data.attributes.pay}'
390
+ ```
391
+
392
+ Capture `SHIFT_ID`. Source for legacy: `clipboard-health/src/modules/shifts/controllers/shifts.controller.ts:744 @Post("/create")` (note `@Controller(["/api/shifts","/api/shift"])` — both prefixes match). Source for v3: `entrypoints/shift-create.controller.ts`. The shifts module now has many controllers (`shifts.controller.ts`, `shifts-v1.controller.ts`, `shifts-v1-legacy.controller.ts`, `shifts-v2.controller.ts`, `shifts-v2-legacy.controller.ts`, `entrypoints/shift-create.controller.ts`); when you can't find a route, grep across **all** of them and prefer `entrypoints/` for newer code.
393
+
394
+ ## Step 7 — Book/assign the shift
395
+
396
+ `POST /api/shifts/claim` is dual-purpose: workers self-claim, employees admin-assign (via `AdminShiftService.adminShiftAssign`). The body schema is the same in both cases:
397
+
398
+ ```ts
399
+ { agentId, shiftId, offerId, sessionUser, override?, missingDocs? }
400
+ ```
401
+
402
+ **`offerId` is required and must be a real offer.** Pass a random UUID and you get `400 "The offered rate for this shift is no longer valid"` — that's `ClaimShiftOfferService` doing a curated-shifts lookup. Two-step flow for admin:
403
+
404
+ ```bash
405
+ # (a) Create the offer (admin-only, JSON:API body — yes, this one is JSON:API even though shift create isn't)
406
+ OFFER_ID=$(curl -sS -X POST "$API_BASE/api/shifts/$SHIFT_ID/offers" \
407
+ -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
408
+ -d "{\"data\":{\"type\":\"shift-offer\",\"attributes\":{}},\"relationships\":{\"worker\":{\"data\":{\"type\":\"worker\",\"id\":\"$WORKER_ID\"}}}}" \
409
+ | jq -r '.data.id')
410
+
411
+ # (b) Claim/assign — override:true bypasses non-fatal bookability rules (still checks license + qualification)
412
+ curl -sS -X POST "$API_BASE/api/shifts/claim" \
413
+ -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
414
+ -d "{\"agentId\":\"$WORKER_ID\",\"shiftId\":\"$SHIFT_ID\",\"offerId\":\"$OFFER_ID\",\"override\":true,\"sessionUser\":\"$ADMIN_USERID\"}" | jq
415
+ ```
416
+
417
+ Worker self-claim (skip the offer creation if the worker's app already requested an offer; otherwise the worker token can also POST `/api/shifts/:id/offers`):
418
+
419
+ ```bash
420
+ curl -sS -X POST "$API_BASE/api/shifts/claim" \
421
+ -H "Authorization: Bearer $WORKER_TOKEN" -H "Content-Type: application/json" \
422
+ -d "{\"agentId\":\"$WORKER_ID\",\"shiftId\":\"$SHIFT_ID\",\"offerId\":\"$OFFER_ID\",\"sessionUser\":\"$WORKER_ID\"}" | jq
423
+ ```
424
+
425
+ If admin-assign 4xx's with a bookability error, `override:true` covers most rules but **not** `WORKER_MISSING_LICENSE` (you must complete Step 3) or `FACILITY_CHARGE_RATE_MISSING` (the workplace's `rates.{qualification}` must exist). Bookability rule list: `clipboard-health/src/modules/shifts/rules/bookability/constants/constants.ts`. Per-rule debug: `GET /api/shifts/:id/state` (worker token).
426
+
427
+ ## Step 8 — Clock in, then clock out (or skip via verified=true)
428
+
429
+ Live worker path (use this for "real" booking flows in a _future_ shift window):
430
+
431
+ ```bash
432
+ NOW=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
433
+ curl -sS -X POST "$API_BASE/api/shifts/record_timekeeping_action/$SHIFT_ID" \
434
+ -H "Authorization: Bearer $WORKER_TOKEN" -H "Content-Type: application/json" \
435
+ -d "{\"stage\":\"CLOCK_IN\",\"location\":[-122.4153089,37.7791078],\"locationType\":\"LIVE\",\"appType\":\"APP\",\"connectivityMode\":\"ONLINE\",\"shiftActionTime\":\"$NOW\",\"shiftActionCheck\":\"LOCATION\"}" | jq
436
+ ```
437
+
438
+ Two important traps for **test data flows**:
439
+
440
+ - A freshly-created worker has **no background check** → worker-token clock-in returns `422 "You can't work as you don't have a valid background check."` (`WORKER_HAS_INVALID_BACKGROUND_CHECK`). No way around this from the API surface alone.
441
+ - Calling the same endpoint with an admin token does **not** retroactively clock in a past shift — `getActionTimestamp` always uses `startOfMinute(new Date())` for admin-driven actions, ignoring `body.shiftActionTime`. Admin-stamped clock actions only make sense during an active shift window.
442
+
443
+ **Test-data shortcut — mark verified directly.** For invoice generation and most billing flows you don't need real clock times; you need a shift with `agentId` set, `verified: true`, and a non-zero `time`:
444
+
445
+ ```bash
446
+ CLOCK_OUT=$(date -u -v-3H +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -u -d '-3 hours' +%Y-%m-%dT%H:%M:%S.000Z)
447
+ curl -sS -X PUT "$API_BASE/api/shift/put" \
448
+ -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
449
+ -d "{\"shiftId\":\"$SHIFT_ID\",\"adminId\":\"$ADMIN_USERID\",\"updatedInfo\":{\"time\":5.5,\"verified\":true,\"clockInOut\":{\"end\":\"$CLOCK_OUT\"}}}" | jq
450
+ ```
451
+
452
+ ⚠️ **Side effect:** setting `verified:true` via this update has been observed to clear `agentId`. After the verify-update, **read the shift back and re-run Step 7 (offer + claim)** if `agentId` is null. Confirm before invoice work — the invoice segments query has `agentId: { $exists: true, $ne: null }` so a wiped agent silently filters the workplace out.
453
+
454
+ The dedicated `POST /api/shift/verification/verify` endpoint exists but takes a more involved `signatory` shape (`{name, role, phone, email, signedAt, method, signatoryId}`) and 500s easily — `PUT /api/shift/put` with `updatedInfo.verified=true` is more reliable for setup.
455
+
456
+ Other verification types beyond LOCATION (`NFC`, `MANUAL`, `INTEGRATION`) — see the Concepts section. Source: `clipboard-health/src/modules/shift-timekeeping/timekeeping-actions/controllers/shift-time-keeping-action-v2.controller.ts`.
457
+
458
+ `shiftAdjustmentSchema.adjustmentType` enum (in case you need it for a real adjustment): `preInvoicePreference | preInvoiceDispute | postInvoiceDispute | other` — **not** `TIME` / `PAY` etc. And the adjustment endpoint refuses to run on an unverified shift (`"Cannot adjust shift because it is not yet verified."`), so verify first.
459
+
460
+ ## Step 9 — Pay the worker (transfer → payout)
461
+
462
+ **Payments are 2-step and live in `payment-service`.** backend-main initiates; payment-service owns the record of truth.
463
+
464
+ ```bash
465
+ # Transfer: Clipboard Stripe → worker's Express account (S2S from backend-main)
466
+ curl -sS -X POST "$API_BASE/payment/accounts/$WORKER_ID/transfers" \
467
+ -H "Authorization: Bearer $S2S_BACKEND_MAIN" -H "Content-Type: application/json" \
468
+ -d "{\"shiftId\": \"$SHIFT_ID\", \"idempotencyKey\": \"skill-$(date +%s)\"}" | jq
469
+
470
+ # Verify transfer landed
471
+ curl -sS "$API_BASE/payment/accounts/$WORKER_ID/transfers" \
472
+ -H "Authorization: Bearer $S2S_BACKEND_MAIN" \
473
+ | jq --arg sid "$SHIFT_ID" '.[] | select(.shiftId == $sid) | {status, amount, stripeTransferId: .transferObject.id}'
474
+
475
+ # Payout: Express → external account (worker-initiated)
476
+ curl -sS -X POST "$API_BASE/payment/accounts/authenticated/payout" \
477
+ -H "Authorization: Bearer $WORKER_TOKEN" -H "Content-Type: application/json" \
478
+ -d '{}' | jq
479
+
480
+ # Read worker's latest payout
481
+ curl -sS "$API_BASE/payment/payouts/workers/$WORKER_ID" \
482
+ -H "Authorization: Bearer $WORKER_TOKEN" | jq
483
+ ```
484
+
485
+ Transfer amount is driven by the shift payout calculation — if the request body shape complains, grep `payment-service/src/app/payments/transfers/accounts-transfers.controller.ts` and confirm. For a per-shift breakdown (including bonuses/reversals), use `POST /payment/payments/shift-payment-details`.
486
+
487
+ ## Step 10 — Generate an invoice for the workplace
488
+
489
+ Invoices are **manually triggered by a CBH employee**, not scheduled. In dev they target the Invoiced.com sandbox (fully stubbed — no real money). Path is **`/api/payment/sendInvoices`** (`/api/`-prefixed, not `/payment/...`).
490
+
491
+ The body is **not** `{workplaceIds: [...]}`. Schema (`sendInvoicesBodySchema` in `paymentInvoice.contract.ts`) requires `start`, `end`, `includeUnverifiedInErrors`, `selectedIdsMap`, `appUrl`, `sentBy`, `sentByEmail`. The `selectedIdsMap` is keyed by **invoice segment type** (`not-generated | not-generated-with-errors | invoices-with-errors | invoices-without-errors`), **not** workplace ID — workplace IDs go into the `includedIds` array under the relevant segment.
492
+
493
+ A shift with no clockInOut times lands in `not-generated-with-errors`. Pass `includeUnverifiedInErrors: true` to include it.
494
+
495
+ ```bash
496
+ START_DATE=$(date -u -v-30d +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -u -d '-30 days' +%Y-%m-%dT%H:%M:%S.000Z)
497
+ END_DATE=$(date -u -v+1d +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -u -d '+1 day' +%Y-%m-%dT%H:%M:%S.000Z)
498
+
499
+ # Dry-run with preview:true to confirm the workplace is picked up before enqueueing the job
500
+ curl -sS -X POST "$API_BASE/api/payment/sendInvoices" \
501
+ -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
502
+ -d @- <<JSON | jq
503
+ {
504
+ "start": "$START_DATE",
505
+ "end": "$END_DATE",
506
+ "includeUnverifiedInErrors": true,
507
+ "selectedIdsMap": {
508
+ "not-generated-with-errors": {
509
+ "selectAll": false,
510
+ "includedIds": ["$WORKPLACE_ID"],
511
+ "excludedIds": []
512
+ }
513
+ },
514
+ "appUrl": "$ADMIN_WEBAPP",
515
+ "sentBy": "$ADMIN_USERID",
516
+ "sentByEmail": "$ADMIN_EMAIL",
517
+ "preview": true
518
+ }
519
+ JSON
520
+
521
+ # Real trigger: same body with "preview": false (or omit). Returns 202; the job runs async.
522
+ # List invoices for this workplace (wait ~30–60s for the job to land)
523
+ curl -sS "$API_BASE/api/invoice?pageNumber=1&facilityId=$WORKPLACE_ID" \
524
+ -H "Authorization: Bearer $ADMIN_TOKEN" | jq '.invoiceList'
525
+ ```
526
+
527
+ Pre-check segments to find which bucket the workplace lands in (helpful when preview returns 0 workplaces):
528
+
529
+ ```bash
530
+ curl -sS "$API_BASE/api/payment/invoiceSegments/list?start=$START_DATE&end=$END_DATE&segmentType=not-generated-with-errors&facilityId=$WORKPLACE_ID&includeUnverifiedInErrors=true" \
531
+ -H "Authorization: Bearer $ADMIN_TOKEN" | jq '.[] | {_id, name: .facility.name}'
532
+ ```
533
+
534
+ Mongo match for invoice eligibility (`InvoiceSegmentsRepository`): `agentId: {$exists: true, $ne: null}`, `start` in date range, not deleted (or deleted-with-`isBillable:true`). With `includeUnverifiedInErrors:false`, also requires `profit > 0` and verified=true with non-null clockInOut. So an "ideal" billable shift has `agentId` set, `verified:true`, both `clockInOut.start` and `clockInOut.end` populated, and `charge > pay`.
535
+
536
+ For deep verification, read `clipboard-health/src/modules/payment/controllers/payment-invoice.controller.ts` and `.../modules/invoice/services/invoice-segments.service.ts`.
537
+
538
+ ## Full orchestration — one-shot smoke
539
+
540
+ Run steps 1–10 in order. Halt on the first non-200. Each step captures the ID the next step needs. Assert at the end:
541
+
542
+ - Shift has `agentId` populated, `verified:true`, no `cancelledAt`. (Re-read after Step 8 — the `verified:true` update can clear `agentId`.)
543
+ - Transfer row exists in `payment-service` with `status = "COMPLETED"`. (Skipped on the invoice-only fast path.)
544
+ - Payout row exists for this worker, recent. (Skipped on the invoice-only fast path.)
545
+ - Invoice row exists for the workplace via `GET /api/invoice?pageNumber=1&facilityId=$WORKPLACE_ID` with a non-null `invoicedComId`.
546
+
547
+ ---
548
+
549
+ # Concepts — everything beyond the core flow
550
+
551
+ Short. Pointers, not recipes. When you need a concrete call, open the controller cited and read the guard + DTO.
552
+
553
+ ## Worker lifecycle (stages)
554
+
555
+ Enum in `clipboard-health/src/workers/constants/workerStages/index.ts`. Stages: `ONBOARDING → ENROLLED → PROBATION → RESTRICTED → DEACTIVATED → SOFT_DELETED`. Only `ENROLLED` and `PROBATION` can book freely. `RESTRICTED` books only at **preferred** workplaces. `DEACTIVATED` can still log in unless `loginBlocked=true`.
556
+
557
+ **Worker mobile-app earnings/bookings tabs filter on two things admin-webapp ignores:** worker stage (`ONBOARDING` → empty state) and `shift.start < now` (future-dated shifts hidden, even verified + paid). So a shift can be paid in payment-service and still missing from Earnings. To make a verified shift visible there, its `start` / `end` must already be in the past at verify time — see the PUT-on-verified-shift gotcha in Troubleshooting.
558
+
559
+ ## Shift state is derived, not stored
560
+
561
+ There is no single `ShiftStatus` enum. Infer from `agentId`, `cancelledAt`, clock actions, `deleted`, `isSentHome`. The worker-facing derived state is at `GET /api/shifts/:id/state` — use that, don't build your own.
562
+
563
+ ## Bookability — introspect, don't guess
564
+
565
+ ~50 rules in `clipboard-health/src/modules/shifts/rules/bookability/constants/constants.ts`. When "worker cannot book", always hit `GET /api/shifts/:shiftId/state` first; the response names the failing rule. Common causes: no Stripe, no signed agreement, missing/expired license (check `multiState` flag and NLC states), missing document requirement, deactivated/restricted stage, shift in past, worker already on an overlapping shift.
566
+
567
+ ## Shift assignment variants
568
+
569
+ Four paths to "assigned":
570
+
571
+ 1. **Instant book** — `POST /api/shifts/claim` (default for almost all shifts).
572
+ 2. **Invite** — `POST /api/shift-invites` (workplace/admin creates; worker `PATCH` to accept). Controller: `clipboard-health/src/modules/shifts-invites/shift-invite.controller.ts`.
573
+ 3. **Shift block** — facility/admin creates a bundle (`POST /api/shift-blocks`); worker claims the block via `POST /api/shift-blocks/:id/booking-requests`. Controllers: `clipboard-health/src/modules/shift-blocks/controllers/{shift-blocks, booking-requests}.controller.ts`.
574
+ 4. **Admin manual assign** — CBH employee assigns from admin-webapp; backend endpoint in `shifts.controller.ts`. Handy for test data setup.
575
+
576
+ ## Clock-in variants beyond LOCATION
577
+
578
+ `shiftActionCheck ∈ {LOCATION, NFC, MANUAL, INTEGRATION}`.
579
+
580
+ - **NFC** — scan a facility-provided tag, requires a `complianceProof` S3 upload (`timeclock-compliance-proof-upload-url`).
581
+ - **MANUAL** — timekeeping disabled at that workplace. Worker submits a timecard afterward (`PUT /api/v2/shifts/timecard/:shiftId`); admin approves.
582
+ - **INTEGRATION** — external timeclock vendor. Vendor carried in `verificationMethod`: `UKG_INTEGRATION | UKG_READY_INTEGRATION | ATTENDANCE_ON_DEMAND_INTEGRATION | MESH_INTEGRATION` (the four `EXTERNAL_TIMEKEEPING_VERIFICATION_METHODS`), or video-based `TAKE_A_VIDEO | AI_TAKE_A_VIDEO`. Backend queries the vendor's API via an adapter to fetch the real punch timestamp.
583
+
584
+ Stages include `CLOCK_IN | LUNCH_OUT | LUNCH_IN | SKIP_LUNCH | CLOCK_OUT | SHIFT_TIME_DONE`.
585
+
586
+ ## Missed-punch (fully functional, phased rollout)
587
+
588
+ Works in production in some MSAs; phased rollout across the marketplace. Not a stub. Notification keys include `missedPunchRequestApproved | Declined`. REST surface can be sparse depending on rollout state — grep `clipboard-health/src/**/missed*Punch*` for current endpoints before relying on them.
589
+
590
+ ## Urgency tiers
591
+
592
+ Driven by lead time + reason, not a hand-settable flag. Enum in `clipboard-health/src/services/urgentShifts/constants.ts`. Tier 1 `NCNS` (previous worker no-showed), Tier 2 `LATE_CANCELLATION`, Tier 3 `LAST_MINUTE` (posted within 12h of start). Drives notification cadence. Assigned internally by `SetShiftUrgencyService`.
593
+
594
+ ## Cancellation — billing is timing+contract, not reason
595
+
596
+ Billing check (from `FcfWorkerPayoutService.getCancellationPaymentParams`):
597
+
598
+ ```ts
599
+ isBillable =
600
+ leadTime < facility.lateCancellation.period &&
601
+ facility.lateCancellation.feeHours !== 0 &&
602
+ !isWorkerLateBeyondThreshold && // >20min late
603
+ !hasInvalidPayoutValues;
604
+ ```
605
+
606
+ No reason code exempts the workplace by itself. Escape hatches: outside late-cancel window, `feeHours=0`, worker very late.
607
+
608
+ Paths:
609
+
610
+ - Worker cancel — `POST /api/shifts/worker-cancel-request` (reasons: `SICK`, `TRANSPORTATION`, `BABYSITTER_ISSUE`, …).
611
+ - Facility cancel (two-step) — `PATCH /api/shifts/:id/facility-cancelled-me/request` → `.../approve` (or `.../reject`). Reasons: `LOW_CENSUS`, `STAFFED_IN_HOUSE`, `STAFFED_OTHER_REGISTRY`, `NO_CALL_NO_SHOW`, `FACILITY_USER_SUBMIT_SENT_HOME`, `WORKER_IS_LATE`, `OTHER`.
612
+ - Sent home (mid-shift) — routed through the same facility-cancel flow with `FACILITY_USER_SUBMIT_SENT_HOME`; separate `getSentHomePayoutParams` computes partial pay.
613
+ - Left early (recorded _about_ a worker, **not** filed _by_ one) — `POST /api/worker-left-early-requests`. Body is **JSON:API** with `type: "worker-left-early-request"` and attributes `{shiftId, replacementRequested, leftWithPermission, comment}`. Contract: `node_modules/@clipboard-health/contract-backend-main/src/lib/workerLeftEarlyRequests.contract.ts`. Controller: `src/modules/worker-left-early/entrypoints/worker-left-early.controller.ts`. The route's decorator is `@AllowAnyAuthenticatedUser`, **but** `WorkerLeftEarlyAuthorizer` only accepts: (1) `EMPLOYEE` with an `EmployeeProfile`, or (2) `WORKPLACE_USER` who `worksAt(facilityId)`, is verified+non-suspicious, AND has role `ADMIN | SHIFT_MANAGEMENT | DOCUMENTS` or the `POST_SHIFT_PERMISSION` permission. **Worker tokens get 403 "Unauthorized user".** Use the admin (e2e) token or a facility-user token. Returns the request with optional `replacementShiftId` if `replacementRequested:true`. Read-back: `GET /api/worker-left-early-requests/:id?include[]=shift&include[]=worker` (path param is the WLE request id, not the shift id, despite the contract's `ShiftIdSchema` typing).
614
+ - Admin delete — `POST /api/shift/:id/delete` with `ADMIN_EDIT_SHIFT | ADMIN_MIGRATION`.
615
+
616
+ Always dry-run with `GET /api/shifts/:id/cancellationParams` before asserting `isBillable` / `isPayable`.
617
+
618
+ ## Bonuses — the entity lives in payment-service
619
+
620
+ Initiated from backend-main for many reasons (shift completion bonuses, sent-home fees, cancellation fees, **Home Health occurrence payouts**, discretionary admin bonuses). worker-app-bff is read-only.
621
+
622
+ DTO at `payment-service/src/app/bonuses/bonuses.dtos.ts → CreateBonusPaymentDto`. Schema fields that matter:
623
+
624
+ - `amount` (paid to worker), `charge` (billed to workplace; `0` or `null` = no charge), `billable` (default `true`; `false` skips from upcoming charges).
625
+ - `facilityId`, `agentId`, `shiftId`, `reason`, `status`. **No `chargesFacility` field.**
626
+ - **`description` vs `invoiceLine` are two distinct fields with two audiences.** `description` is the worker-facing string (used in transfer descriptors etc.). `invoiceLine` is the workplace-invoice line item label and is what `buildBonusLineItem` writes to `lineItem.description` on the upcoming-charges record. Setting only `description` does **not** put the string on the workplace invoice. See `clipboard-health/src/modules/upcoming-charges/services/upcomingChargesUpdateBonus.job.ts → buildBonusLineItem`.
627
+ - `type` literal values are dotted: `TBIO.SINGLE | TBIO.BACK_TO_BACK | TBIO.OVER_FORTY | HOME_HEALTH` — these are the enum _values_ in `BonusesPayment/types.ts`. Producer code uses `BonusPaymentType.single` (the enum _key_) but on the wire the string is `"TBIO.SINGLE"`. Sending `"single"` returns 500 with a Mongoose enum validation error.
628
+ - `createdBy` must be a Mongo ObjectId (the service does `new Types.ObjectId(createdBy)` and 500s on bad input). **Ask the user for a userId if you don't have one; otherwise fall back to the current admin's `custom:cbh_user_id` claim from the admin token.** Or omit the field — it's optional.
629
+ - **`agentId` is the worker's `userId`, NOT the agentProfile `_id`.** Grab `userId` from the `/api/user/create` response (or `/api/user/agentSearch` lookup) and pass that everywhere payment-service expects an agentId. Account doc `_id` is `Types.ObjectId(agentId)`, so `accounts.findById(agentId)` matches by `_id === userId`.
630
+
631
+ Reversal: `POST /api/bonus-payments/:id/reversals`.
632
+
633
+ Upcoming-charges integration is async: `BonusPaymentCreated` SQS → consumer → job inserts if `charge > 0 && billable !== false`, removes if `charge === 0`. Wait ~30s between bonus creation and checking upcoming charges.
634
+
635
+ To inspect the resulting line item, call `GET $API_BASE/api/upcoming-charges?facilityId=<workplaceId>&periodStartDate=<...>&periodEndDate=<...>` (admin token). The record is keyed by **Sunday-aligned week** (`moment.utc(payPeriodStart).startOf("week")`), so query a range that covers the whole week containing your bonus's `payPeriodStart`. If the response is `[]`, widen to Sunday-of-week → Saturday-end-of-week. The relevant line item shows up under `[0].otherCharges[]` with `bonusId` matching what you sent and `description` matching the `invoiceLine` you sent.
636
+
637
+ ## 1099 Policy coverage
638
+
639
+ "1099 coverage" is workers' comp / GL insurance issued by **1099Policy.com** for individual contractors per shift. Two-sided opt-in: workplace registers as a contracting entity, worker walks the application + binds a policy. Per shift, an _assignment_ is created tying the worker's policy to a specific job.
640
+
641
+ **Three IDs to keep straight (all returned by 1099Policy):**
642
+
643
+ - `ten99PolicyEntityId` (`en_*`) on the workplace.
644
+ - `ten99PolicyContractorId` (`cn_*`) on the worker.
645
+ - `ten99PolicyAssignmentId` (`an_*`) per shift+worker, plus a `ten99PolicyJobId` (`jb_*`) wrapping it.
646
+
647
+ **Workplace setup (CBH employee).** `POST /api/workplaces/:workplaceId/1099-policy/entities` with `{data:{type:"ten99-policy-entity",attributes:{categoryCode},relationships:{workplace}}}`. Read config via `GET /api/workplaces/:workplaceId/1099-policy/configuration` — `stateOnboarded:true` + a `documentRequirementId` means the LD flag has the workers'-comp doc requirement enabled for that workplace's state. Eligible states only: **CA, FL, MI, NJ** (per `TEN99_POLICY_CONVALESCENT_HOME_STATES` in `ten99-policy.constants.ts`).
648
+
649
+ **Worker setup (worker token).** `POST /api/1099-policy/application-sessions` returns a `url` like `https://apply.1099policy.com/sandbox/ias_*` — **must be walked in a browser** to bind a policy. Internally, our system creates a `Ten99PolicyJob` row with `ten99PolicySessionId`. After the user walks the sandbox UI to "You've successfully signed up for coverage", 1099Policy fires `application.complete` (and later `policy.active`) back to `POST /api/1099-policy/events`. Without walking through to policy binding, downstream assignment creation 400s.
650
+
651
+ **Per-shift assignment (worker token).** `POST /api/1099-policy/assignments` with `{data:{type:"ten99-policy-assignment",relationships:{shift,worker}}}`. Returns `netRate` (cents per $100 earned, e.g. `572` = 5.72%). The assignment's `effective_date` (the shift's `start`) **must be in the future** at request time — past-window shifts always 400. Workflow: create the assignment while the shift is still future, then `PUT /api/shift/put` to fast-forward `time` / `clockInOut` / `verified`.
652
+
653
+ **Fee math.** `getTen99PolicyAssignmentFeeInCents` (called from `triggerShiftPayment`) computes `feeInCents = round((basePayInCents × time / 100 / 100) × netRate)`. Records it on the shift document and on the `Ten99PolicyJob` row. Read the recorded value via `GET /api/shifts/:shiftId/1099-policy/assignments` — that endpoint surfaces `assignmentFeeInCents`. The `/api/workers/:workerId/1099-policy/jobs` endpoint **does not** surface the fee field (DTO mapper at `ten99-policy.dto.mapper.ts:135-175` omits it).
654
+
655
+ **Deduction from worker pay is `TIMESHEET_UPLOAD`-only.** Per `shift-payment.domain.ts:120-168` and the explicit assertion at `shift-payment.domain.spec.ts:199-225` (VAU-1106): the fee is **only subtracted** from the payable amount on `PAYMENT_EVENTS.TIMESHEET_UPLOAD` and `RETRY_MISSING_PAYMENT_TIMESHEET_UPLOAD`. On `SHIFT_VERIFICATION` / `SHIFT_SENT_HOME` / `SHIFT_MISSED_PUNCH_REQUEST_APPROVAL` the fee is recorded but the worker gets full pay; the fee is recovered through the 1099Policy invoice that gets enqueued via `CREATE_TEN99_POLICY_INVOICE_FOR_ASSIGNMENT_JOB_NAME`. So if you're testing "fee deducted from worker pay," you need a manual-timekeeping flow ending in `PUT /api/v2/shifts/timecard/:shiftId` + admin approval, not a regular admin verify.
656
+
657
+ **Hidden prereq: `TIMESHEET_UPLOAD` deduction needs an `InstantPayShift` row.** `doInstantPay` (`helpers/instantPay.ts:847`) short-circuits when `getInstantShift(shiftId)` returns null. That row is created by the worker self-clock-in flow (`POST /api/shifts/record_timekeeping_action/:shiftId` with `stage: CLOCK_IN`) — **not** by admin-override claim. With admin-claimed shifts the timecard endpoint auto-verifies and surfaces the fee (`ten99PolicyAssignmentFeeInCents`), but no Stripe transfer with the deduction fires — the hourly retry-missing-payment cron eventually transfers the **full** gross via SHIFT_VERIFICATION instead. To prove the deduction at the Stripe level, the worker must self-clock-in (requires a valid background check on file).
658
+
659
+ ## Home Health — Case → Visit → Occurrence
660
+
661
+ Separate backend (`home-health-api`, Postgres), same mobile and admin apps. Reach it through the gateway at `$API_BASE/home-health-api/api/v1/...`.
662
+
663
+ - **Case** = a patient owned by a Home Health agency workplace.
664
+ - **Visit** = a scheduled appointment, **typed** (e.g. admission visit which must be done by an RN, regular visits, etc.).
665
+ - **Occurrence** = a completed instance of a visit. For recurring visit types (e.g. "regular visits, X per week for X weeks"), one visit produces multiple occurrences.
666
+
667
+ **Workplace identity gotcha — `workplaceId` is the workplace User.\_id, NOT the FacilityProfile.\_id.** `POST /api/facilityprofile/create` returns top-level `id: <facilityProfile.userId>` (`facilityProfile.service.ts:456`). That's the value HH-API expects in URL paths (`workplaceId: workplace.userId` in `visits.service.ts`). `GET /api/facilityprofile/:id` returns a different `_id` (the FacilityProfile doc's). Use the create-response `id`.
668
+
669
+ Workplace type for Home Health is **`"Home Healthcare"`** (mapped to `TypeOfCare.HOME_HEALTH` in `cbh-core/utils.ts → stringTypeOfCareToEnum`). Only 4 qualifications enabled: `CAREGIVER, RN, CNA, LVN` — no other rates needed in the workplace `rates` map.
670
+
671
+ **Visit creation requires account-pricing to exist first.** `visits.service.ts → ensurePricingExistsForVisit` looks up the tuple `(workplaceId, agentReq, visitType, pricingType, typeOfCare)` in `AccountPricing` (Prisma `@@unique unique_charge_rate_combination`). Missing row → throws `"Account pricing for visit type does not exist"`, which surfaces as a 500 from the visit-create endpoint. The `agentReq` part of the tuple is server-coerced to `RN` for `RN_VISIT_TYPES` (`ADMISSION`, `ADMISSION_AND_FIRST_VISIT`, `EVALUATION`, `RECERTIFICATION`, `RESUMPTION_OF_CARE`, `NURSING_EVAL`); for `REGULAR`, `DISCHARGE`, `SUPERVISORY` it uses the request's `workerReq` verbatim. Seed accordingly.
672
+
673
+ **Seeding via API** — `POST /home-health-api/api/v1/:workplaceId/account-pricing` (`AccountPricingController`, `@AllowClipboardHealthEmployees()`):
674
+
675
+ ```json
676
+ {
677
+ "data": {
678
+ "type": "accountPricing",
679
+ "attributes": {
680
+ "visitType": "REGULAR",
681
+ "agentReq": "CNA",
682
+ "payRateInMinorUnits": 5500,
683
+ "chargeRateInMinorUnits": 7500,
684
+ "pricingType": "PER_VISIT",
685
+ "typeOfCare": "HOME_HEALTH"
686
+ }
687
+ }
688
+ }
689
+ ```
690
+
691
+ Constraints (`accountPricing.service.ts → createAccountPricing`):
692
+
693
+ - Workplace must NOT be in `INACTIVE_WORKPLACE_STATUSES`. Newly-created `onboarding` workplaces pass; `closed`/`paused`/`terminated` get `BadRequestException("You cannot create account pricing for an inactive workplace")`.
694
+ - `payRateInMinorUnits` and `chargeRateInMinorUnits` capped at `100_000` (= $1,000) by the DTO.
695
+ - Duplicates on the unique tuple → Prisma `P2002` mapped to `400 "There is another account pricing with the same data."` Use `PATCH /home-health-api/api/v1/:workplaceId/account-pricing/:id` to update an existing row.
696
+ - `pricingType ∈ {PER_VISIT, PER_HOUR}`, `typeOfCare ∈ {HOME_HEALTH, HOSPICE, PRIVATE_DUTY}`.
697
+
698
+ **Seed once per `(visitType, agentReq)` you intend to create visits for.** Typical e2e seed is one row: `REGULAR` × <worker's qualification> × `PER_VISIT` × `HOME_HEALTH`. For an admission visit, seed `ADMISSION` × `RN` × `PER_VISIT` × `HOME_HEALTH`.
699
+
700
+ When LD flag `2026-01-configure-visit-rate-setting` is `{enabled: true}` for the workplace, `checkForAccountPricing` swallows the missing-pricing error and the visit is created using fallback rates from `home-health-api/src/modules/visits/fallback-rates/rate-configuration.json`. Seeding is still the cleaner path.
701
+
702
+ Visit DTO quirks (`createVisit.dto.ts`):
703
+
704
+ - `pricingType` is decorated `@IsOptional()` but the service signature is `Required<Pick<…, "pricingType">>` — pass it.
705
+ - `typeOfCare` is **not** in the request DTO — it's derived from `workplace.type` server-side.
706
+ - `visitsPerWeek` and `durationInWeeks` need numeric values (often `0` is fine for one-off visits).
707
+ - Full `VisitType` enum: `ADMISSION | ADMISSION_AND_FIRST_VISIT | REGULAR | EVALUATION | RECERTIFICATION | DISCHARGE | SUPERVISORY | RESUMPTION_OF_CARE | NURSING_EVAL`.
708
+
709
+ Patient + case shapes:
710
+
711
+ - `POST /home-health-api/api/v1/:workplaceId/patients` — JSON:API; attributes need `externalPatientIdentifier`, `formattedAddress`, `latitude`, `longitude`, `oasis: boolean`, `workplaceId`.
712
+ - `POST /home-health-api/api/v1/:workplaceId/cases` — JSON:API; attributes `{workplaceId, patientId, specialties: string[], description?}`.
713
+
714
+ Worker paths to a visit:
715
+
716
+ 1. **Discover + book** — `GET /api/v1/in-home-cases?filter[booked]=false&...` → `PATCH /api/v1/visits/:id` with `{data:{attributes:{bookedWorkerId}}}`. No invite required.
717
+ 2. **Invite** — workplace/admin `POST /api/v1/:workplaceId/visits/:id/invites`; worker accepts.
718
+ 3. **Test-data shortcut** — set `bookedWorkerId` directly in the create body (status `FILLED`) when seeding via admin token. The worker doesn't need to be invited or have signed any agreement.
719
+
720
+ There is **no "book the case" operation** — always per-visit. Some visit types commit the worker to a multi-week cadence, but the data model reflects that via multiple occurrences on the same visit, not a case-level booking.
721
+
722
+ Worker logs the occurrence on arrival/completion via worker token: `POST /home-health-api/api/v1/visits/:visitId/occurrences` with `{data:{type:"visitOccurrence", attributes:{completedAt, pricingType, estimatedDuration?}}}`. **No background-check or Stripe gate here** — unlike shift clock-in. `PER_HOUR` requires `estimatedDuration`. New occurrences default to `status: PENDING`.
723
+
724
+ Workplace verifies via `PATCH /home-health-api/api/v1/:workplaceId/visit-occurrences/:id` with `{data:{type:"visitOccurrence", attributes:{status, rejectionReason?, paid?, instantPay?}}}`. Setting `status: REJECTED` requires `rejectionReason`.
725
+
726
+ **APPROVE side effect — bonus payment to payment-service.** When LD flag `2024-05-home-health-bonus-payment` (keyed by workplaceId) resolves true, the approve calls `POST /payment/bonuses`, which 400s if the worker has no `Account` in payment-service. Surface symptom on the HH-API patch is generic `500 "Internal server error. detail: Request failed with status code 400"`. The whole patch rolls back inside a Prisma transaction, so the occurrence stays PENDING.
727
+
728
+ Lower-env workaround (no Stripe): `POST /payment/accounts` with `{agentId, gatewayAccountCreationIsEnabled: false, accountId: "acct_e2etest_<rand>"}`. `accountId` is required at the Mongoose layer even though `CreateAccountDto` marks it optional — pass any non-empty string. Creates an `Account` with status `Instant Payouts Enabled`, `isOnboardingCompleted: true`. `gatewayAccountCreationIsEnabled` is rejected in production.
729
+
730
+ Visit status enum: `OPEN | CANCELED | FILLED | CLOSED | PENDING | CONFIRMED | LOGGED`. Occurrence: `PENDING | APPROVED | REJECTED`.
731
+
732
+ ## Preferred workers
733
+
734
+ Owned by `shift-reviews-service` (Postgres, `PreferredWorker` table). Three reasons: `FAVORITE` (workplace user favorited), `RATING` (high rating post-shift, default), `INTERNAL_CRITERIA` (system signal).
735
+
736
+ Read endpoints: `GET /reviews/preferred-workers`, `/preferred-workers/:workerId/statistics`, `/preferred-workers/:workerId/workplaces`. Upserts authorized via `@AllowClients("backend-main")` — not directly writable with a user token. Matters because `RESTRICTED` workers can only book at preferred workplaces.
737
+
738
+ ## Documents + licenses
739
+
740
+ Owned by `documents-service-backend`. License fields: `state`, `multiState` (boolean; true = valid in any NLC-member state), `number`, `expiresAt`, `status`. Bookability rule `isLicenseValidForState` validates against the shift's state + NLC + expiry.
741
+
742
+ Document upload is 3-step: presigned URL → PUT S3 → register via `POST /api/documents`. Admin approves via `PATCH /api/documents/:id`. Requirements are state × qualification specific.
743
+
744
+ ## Attendance-policy scope
745
+
746
+ Mounted at `$API_BASE/attendance-policy/` in dev (gateway-rewritten). Owns more than clock windows — also **attendance scores**, **restrictions** (suspensions tied to scores), and **market-level config**. Controllers: `/policies`, `/restrictions`, `/scores` (`/scores/adjust`, `/scores/:id/reversals`), `/workers`, `/workers/:workerUserId/profile`, `/markets`. If you're touching no-show or late-arrival logic, this is the service.
747
+
748
+ ---
749
+
750
+ # Test data — beyond the core flow
751
+
752
+ For anything outside the core happy path, use one of:
753
+
754
+ 1. **`core:seed-data` skill** — triggers the `Generate Seed Data` GitHub Action, which creates realistic scenarios (HCPs with Stripe, facilities, shifts, etc.). Preferred for one-off setup at scale.
755
+ 2. **Admin manual endpoints in `shifts.controller.ts`** — useful for assigning a worker to a shift directly, bypassing bookability.
756
+ 3. **`cbh-core/packages/testing-e2e-admin-app/src/lib/admin-service.ts`** — the authoritative orchestration reference. Read `createHcpHcfAndShift` for the happy-path orchestration; do NOT import at runtime.
757
+
758
+ Ask the user which path they prefer before wiring a lot of curl.
759
+
760
+ # Verification patterns
761
+
762
+ - **API read-back** — primary. Mutation → GET resource → assert a field changed.
763
+ - **Bookability probe** — `GET /api/shifts/:id/state`.
764
+ - **Payment truth** — always `payment-service`, not backend-main (which can be stale). Cleanest read for worker payment history: `GET /payment/payment-logs?filter[agentId]=<workerId>` (fields: `sourceEvent`, `amount` (cents), `status`, `paymentTypeId`, `createdAt`). More useful than `GET /payment/accounts/:workerId/transfers`, which needs `startDate`+`endDate` and only returns the raw Stripe transfer object.
765
+ - **Auto-retry of failed transfers** — payment-service has an hourly cron that re-fires missing/failed transfers once the underlying account is healthy. If you trigger SHIFT_VERIFICATION before the worker has a Stripe account, the transfer fails silently; set up the account and wait up to ~1h. The retry's `payment-logs` row uses `sourceEvent: adjustMissingPayment-<original>` (e.g. `adjustMissingPayment-verification`) — same amount, same destination, just a delayed `createdAt`. So you usually don't need to manually re-trigger after fixing a prereq.
766
+ - **Datadog traces** — see the "Datadog service map" section below; **the actual service tag is never `backend-main`**, it's one of ~18 pseudo-services that all run the same monolith code. Hand off to `core:datadog-investigate` for deep dives.
767
+ - **Mailpit** — `curl -sS -G -u "$MAILPIT_USER:$MAILPIT_PASS" --data-urlencode "query=from:no-reply+dev@updates.clipboardworks.com to:\"$EMAIL\"" "$MAILPIT_BASE/api/v1/search"` — let `--data-urlencode` handle the `+` and other specials. Combine with `subject:"Your Clipboard sign-in link"` (magic links), `subject:"Your Clipboard login code"` (OTPs), or a workplace-name fragment for shift-booked / cancellation notices.
768
+ - **UI sanity check** — `$ADMIN_WEBAPP` (serves both employees and facility users) is the last resort for "did this render".
769
+
770
+ # Datadog service map
771
+
772
+ Several Clipboard repos are deployed as **multiple Datadog services running the same code**, each scoped to a different concern (HTTP path, queue, cron, etc.). Searching `service:backend-main` returns nothing — the bare repo name is not the service tag. You need to query the _specific_ pseudo-service for the path/job you care about, or fan out across the whole group.
773
+
774
+ **Backend-main monolith (`clipboard-health` repo) → 18 pseudo-services.** Same NestJS code, different deployments:
775
+
776
+ ```text
777
+ cbh-backend-main cbh-backend-main-invoices cbh-agentprofile
778
+ cbh-bg-jobs cbh-bg-jobs-slow cbh-bg-services
779
+ cbh-calendar cbh-cron cbh-db-triggers
780
+ cbh-employees cbh-facility cbh-lastminute
781
+ cbh-payment cbh-pricing cbh-services
782
+ cbh-shiftmonitor cbh-shifts cbh-user
783
+ ```
784
+
785
+ The service-tag boundary tends to track the controller/job module name (e.g. `cbh-shifts` for shift HTTP routes, `cbh-cron` for scheduled tasks, `cbh-bg-jobs*` for queue consumers, `cbh-payment` for the _backend-main_ `/api/payment/*` routes — **not** payment-service, which is `cbh-payment-service`). When unsure which one logged your event, query across all 18 with `service:(cbh-backend-main OR cbh-agentprofile OR cbh-bg-jobs OR …)` or use a wildcard `service:cbh-*` and narrow by other tags.
786
+
787
+ **Other repos with multiple pseudo-services:**
788
+
789
+ - `clipboard-facility-app` → `cbh-facility-app`, `hcf_android_mobile_app`, `hcf_ios_mobile_app`
790
+ - `documents-service-backend` → `cbh-documents-service-backend`, `cbh-docs-merge-worker`
791
+ - `document-verification-service` → `cbh-document-verification-service`, `cbh-docverify-worker`
792
+ - `oig-automatization` → `cbh-oig-automatization`, `cbh-oig-crawler`, `cbh-oig-leie`
793
+ - `open-shifts` → `cbh-curated-shifts-web`, `cbh-backfill-service`, `cbh-migration-runner`
794
+
795
+ **Single-deployment repos (one repo, one DD service):**
796
+
797
+ `payment-service` → `cbh-payment-service`. `home-health-api` → `cbh-home-health-api-web`. `attendance-policy` → `cbh-attendance-policy`. `worker-service-backend` → `cbh-worker-service-backend`. `shift-reviews-service` → `cbh-shift-reviews-service`. `cbh-shifts-bff` → `cbh-shifts-bff`. `urgent-shifts` → `cbh-urgent-shifts` _(repo archived)_. `license-manager` → `cbh-license-manager`. `chat` → `cbh-chat`. `cbh-location-service` → `cbh-location-service`. `worker-eta` → `cbh-worker-eta` _(repo archived)_. `cbh-employee-lifecycle` → `cbh-employee-lifecycle-web`. `clipboard-staffing-api` → `cbh-staffing-api`. `pricing-parameters` → `cbh-pricing-parameters`. `shift-verification` → `cbh-shiftverify`. `billterms` → `cbh-billterms-api`. `files-storage-service` → `cbh-files-storage-service`. `identity-doc-autoverification-service` → `cbh-identity-doc-autoverification-service`. `cbh-pricing` → `facility-msa-classification`. `zendesk-jwt-service` → `cbh-zendesk-jwt-service`. `cbh-mobile-app` → `hcp_mobile_app`. `cbh-admin-frontend` → `admin_web_app`.
798
+
799
+ **To re-derive this map** (the list drifts as new pseudo-services are added — re-run when you suspect it's stale):
800
+
801
+ ```bash
802
+ DD_API=$(awk -F= '/apikey/{print $2}' ~/.dogrc | tr -d ' ')
803
+ DD_APP=$(awk -F= '/appkey/{print $2}' ~/.dogrc | tr -d ' ')
804
+ curl -sS "https://api.datadoghq.com/api/v2/services/definitions?page%5Bsize%5D=200" \
805
+ -H "DD-API-KEY: $DD_API" -H "DD-APPLICATION-KEY: $DD_APP" \
806
+ | jq -r '
807
+ .data
808
+ | map({
809
+ svc: .attributes.schema."dd-service",
810
+ repo: ((.attributes.meta."github-html-url" // "")
811
+ | capture("ClipboardHealth/(?<r>[^/]+)/").r // "—")
812
+ })
813
+ | group_by(.repo)
814
+ | map({repo: .[0].repo, services: [.[].svc] | sort})
815
+ | .[]
816
+ | "## \(.repo) (\(.services | length))\n \(.services | join("\n "))\n"
817
+ '
818
+ ```
819
+
820
+ The `meta.github-html-url` field on each service definition points to the source repo path (`https://github.com/ClipboardHealth/<repo>/blob/main/service.datadog.yaml`), which is how multi-deployment repos reveal their grouping. Definitions without that URL are dd-trace integration sub-services (e.g. `cbh-payment-service-worker`, `cbh-pricing-parameters-aws-sqs`) — same code, just APM-tagged by integration type.
821
+
822
+ # Troubleshooting by failure mode
823
+
824
+ - **401 Unauthorized** — token expired (Cognito ID tokens are 5 min) or wrong actor. Regenerate.
825
+ - **403 Forbidden** — **first try re-minting the token.** Cognito ID tokens expire after ~5 min and stale tokens return 403 with `"Forbidden resource"` — _identical_ to a real permission failure (not 401). After re-minting, if it's still 403: wrong App Client (`-n` flag), wrong facility-user role (`ADM | SFT | DMT | INV`), or wrong employee permission (e.g. `DELETE_HCP_DATA` needed for admin payout).
826
+ - **403 `{"code":"PermissionDenied","detail":"Forbidden resource"}` on shift create** — your admin user has `custom:user_types: EMPLOYEE` (so workplace creation worked) but no `EmployeeProfile` doc. `ShiftCreateAuthorizer` (`shift-create.authorizer.ts:54`) bounces the request without a useful error. Switch to `e2e@clipboardhealth.com` (or another admin known to have an `EmployeeProfile`) and retry.
827
+ - **400 "The offered rate for this shift is no longer valid"** on `/api/shifts/claim` — `offerId` is required and must point at a real `shift-offer` (the rate-negotiation guard runs even with `override:true`). Create one first via `POST /api/shifts/:shiftId/offers` (JSON:API body) and pass that `id`. Without this two-step, admin manual-assign always 400s.
828
+ - **400 on write** — grep the controller named in the concept's "source" and read the DTO. Payload shapes drift.
829
+ - **`PUT /api/shift/put` 400 `"Cannot update shift because it is already verified."`** Verified shifts are immutable to PUT — edit `start` / `end` / `clockInOut` **before** verifying. Sequence for a past-dated, verified, 1099-fee-bearing shift: create at `start = +1h` → claim → create 1099 assignment (`effective_date` must be future) → PUT to move `start` / `end` / `clockInOut` into the past while still unverified → `POST /api/shift/verification/verify`.
830
+ - **HH-API `PATCH visit-occurrences/:id` returns 500 "Request failed with status code 400"** on `status:APPROVED` — bonus-payment side effect to payment-service is failing. Most common cause: worker has no `Account` in payment-service. Fix in dev: create a Stripe-skipped account via `POST /payment/accounts` with `gatewayAccountCreationIsEnabled:false` and a non-empty synthetic `accountId`. Then retry the approve. REJECTED and PENDING don't trigger this path.
831
+ - **HH-API "Account pricing for visit type does not exist"** (500) on visit create — `AccountPricing` row missing for the tuple `(workplaceId, agentReq, visitType, pricingType, typeOfCare)`. Seed via `POST /home-health-api/api/v1/:workplaceId/account-pricing` before the visit. Remember `agentReq` is auto-coerced to `RN` for admission-family visit types; seed accordingly. See the Home Health concept section for the full body and constraints.
832
+ - **HH-API "Workplace not found" / 403 on `/home-health-api/api/v1/:workplaceId/...`** — you're passing the FacilityProfile `_id` instead of the workplace User.\_id. Use the top-level `id` returned by `POST /api/facilityprofile/create`.
833
+ - **`POST /api/1099-policy/assignments` returns 500 with `"Request failed with status code 400"` in dev.** The underlying 400 is from the 1099Policy.com sandbox; check Datadog `service:backend-main @logger.logContext:Ten99PolicyGateway` for the `error.response.data` body. Three common causes:
834
+ 1. **Production category codes don't exist in the sandbox account.** `ten99-policy.constants.ts` defaults to `jc_LsJrariN5V` (CA convalescent home) and `jc_o5vABnBQRj` — both are _production_ codes. The dev sandbox has its own set. Fetch the sandbox's category list from the 1099Policy sandbox dashboard (Categories tab — graphql query `jobCategory`), then **`PATCH /api/workplaces/:workplaceId`** with `data.attributes.ten99PolicyJobCategoryCode = "<sandbox jc_*>"` (CBH-employee only) to swap. Sandbox equivalents as of Apr 2026: `jc_H6HrYFmcRS` (Convalescent Home, CA), `jc_gFzGNVvd24` (Skilled Nursing), `jc_aKhKXXKTGc` (Retirement), `jc_LAK75HcvTk` (Home Health), `jc_RdPg4vejMw` (Hospital).
835
+ 2. **Assignment `effective_date` must be in the future.** Per the 1099Policy API (see `ten99-policy-api.types.ts:333`). Past-window shifts always 400 here. Create the assignment while the shift's `start` is still in the future, then `PUT /api/shift/put` to fast-forward `time`/`clockInOut`/`verified`.
836
+ 3. **Worker hasn't bound a policy in the sandbox.** Walking the application URL only marks our internal `applicationSubmissionDate` if you fire the webhook ourselves; the policy is only bound after walking through the sandbox UI all the way to "You've successfully signed up for coverage". Until then, no `policy.active` event fires and assignments 400.
837
+ - **Stale read** — payment or upcoming-charges state read from backend-main may lag. Read from the owning service (payment-service, shift-reviews-service, documents-service, home-health-api).
838
+ - **Async delays** — bonus → upcoming-charges is ~30s via SQS; invoice generation is ~30–60s; Stripe sandbox occasionally blips.
839
+ - **"Can't find endpoint"** — grep `clipboard-health/openapi.json` for the path (2.1 MB, grep only — never read in full). Or grep `@Controller`, `@Post`, `@Patch` in the owning service.
840
+
841
+ # Browser fallback (Mailpit magic-link)
842
+
843
+ Use only when no API exists (the login form itself; visual-only changes; phone-OTP-gated worker signup).
844
+
845
+ 1. Navigate to `$ADMIN_WEBAPP` (employees and facility users) or `https://hcp-webapp.development.clipboardhealth.org` (workers).
846
+ 2. Enter email → trigger magic-link.
847
+ 3. Poll Mailpit, scoped to the trusted sender + the right subject:
848
+
849
+ ```bash
850
+ # magic link (admin / facility / worker email-link login)
851
+ curl -sS -G -u "$MAILPIT_USER:$MAILPIT_PASS" \
852
+ --data-urlencode "query=from:no-reply+dev@updates.clipboardworks.com to:\"$EMAIL\" subject:\"Your Clipboard sign-in link\"" \
853
+ "$MAILPIT_BASE/api/v1/search" \
854
+ | jq '.messages[0].ID'
855
+
856
+ # OTP (phone-aliased emails like 4153445892.phone.email@testmail.com)
857
+ curl -sS -G -u "$MAILPIT_USER:$MAILPIT_PASS" \
858
+ --data-urlencode "query=from:no-reply+dev@updates.clipboardworks.com to:\"$EMAIL\" subject:\"Your Clipboard login code\"" \
859
+ "$MAILPIT_BASE/api/v1/search" \
860
+ | jq '.messages[0].ID'
861
+ ```
862
+
863
+ 4. Fetch the message and extract the link/code:
864
+
865
+ ```bash
866
+ curl -sS -u "$MAILPIT_USER:$MAILPIT_PASS" "$MAILPIT_BASE/api/v1/message/<ID>" \
867
+ | jq -r '.HTML // .Text' \
868
+ | grep -Eo 'https://[^"[:space:]]+' # or grep the 6-digit OTP
869
+ ```
870
+
871
+ 5. Validate the link host against the allowlist (Hard guardrails rule 2) before navigating.
872
+
873
+ **Address conventions in the dev mailbox:**
874
+
875
+ - `<10-digits>.phone.email@testmail.com` — phone-aliased OTP recipients
876
+ - `playwright-<id>@playwright-hcp.com` and `playwright-<id>-email-link-<rand>@playwright-hcp.com` — Playwright e2e workers
877
+ - `seeddata-<name>+<num>@seeddata.com` — seed-data scenarios
878
+ - `<name>+<suffix>@clipboardhealth.com` — manual dev users
879
+
880
+ Don't try to inject Cognito `localStorage` values — the magic-link flow is simpler. Worker signup may require an OTP autofill key (`REACT_APP_TEST_HELPER_API_KEY`, 1Password → Shared Engineering) in dev.
881
+
882
+ # Out-of-scope for v1
883
+
884
+ - Non-`development` environments.
885
+ - Mobile-native automation (iOS/Android app binaries).
886
+ - Legacy Flutter facility app.
887
+ - Load / performance testing.
888
+ - Deep Cognito lifecycle repair → `core:cognito-user-analysis`.
889
+ - Production incident investigation → `core:datadog-investigate`.
890
+ - CI debugging → `core:fix-ci`.
891
+ - Direct Stripe sandbox fixtures — if Stripe is misbehaving, ask the user.
892
+ - Other verticals beyond healthcare (education, etc.) — ask the user for repo/domain pointers when relevant.
893
+
894
+ # Reference appendix — files to open for current truth
895
+
896
+ Ordered by how often you'll open them:
897
+
898
+ - `clipboard-health/src/modules/shifts/controllers/shifts.controller.ts` — legacy: `/api/shifts/claim` (worker self-claim AND admin-assign), `/api/shifts/unassign`, `/api/shift/put` (legacy update), `/api/shift/create`. Note path inconsistency: create+put are under `/api/shift/` (singular), most reads under `/api/shifts/`. Mounted with `@Controller(["/api/shifts","/api/shift"])` so both prefixes match.
899
+ - `clipboard-health/src/modules/shifts/entrypoints/shift-create.controller.ts` — **v3** `POST /api/v3/shifts` (JSON:API), the documented replacement for the legacy create. Goes through the same `ShiftCreateAuthorizer`.
900
+ - `clipboard-health/src/modules/shifts/entrypoints/internal/shift-create.authorizer.ts` — the gate behind generic 403 _"Permission to resource denied"_ when an admin lacks an `EmployeeProfile`.
901
+ - `clipboard-health/src/modules/shifts/services/adminShiftAssign.service.ts` — `adminShiftAssign` method; the bookability decisions and `getAdminShiftAssignResponseWithSideEffects` map non-bookable criteria to errors.
902
+ - `clipboard-health/src/modules/shift-offers/admin-shift-offer.controller.ts` + `claim-shift-offer.service.ts` — `POST /api/shifts/:id/offers` (must be created before /claim, even with `override:true`).
903
+ - `clipboard-health/packages/contract-backend-main/src/lib/shifts.contract.ts` — `shiftCreateBodySchema` (flat, legacy), `shiftClaimBodySchema` (`offerId` required), `shiftUpdteInfoSchema` (sic), `shiftAdjustmentSchema` (`adjustmentType` enum: `preInvoicePreference|preInvoiceDispute|postInvoiceDispute|other`).
904
+ - `clipboard-health/node_modules/@clipboard-health/contract-backend-main/src/lib/shiftV3.contract.ts` — v3 `shiftContract.create` body (`createShiftDto` JSON:API shape from `shiftCreate.contract.ts`).
905
+ - `clipboard-health/node_modules/@clipboard-health/contract-backend-main/src/lib/workerLeftEarlyRequests.contract.ts` — left-early body and response schemas (JSON:API).
906
+ - `clipboard-health/src/modules/worker-left-early/entrypoints/worker-left-early.controller.ts` — left-early create + read endpoints.
907
+ - `clipboard-health/packages/contract-backend-main/src/lib/shiftOffers.contract.ts` — offer create body shape.
908
+ - `clipboard-health/src/modules/shifts/rules/bookability/constants/constants.ts` — bookability rules.
909
+ - `clipboard-health/src/modules/shift-timekeeping/timekeeping-actions/controllers/shift-time-keeping-action-v2.controller.ts` — clock in/out (admin path uses `startOfMinute(new Date())`, ignoring body's `shiftActionTime`).
910
+ - `clipboard-health/src/modules/facilityProfile/services/middlewares/createFacilityProfileValidator.middleware.ts` — workplace required-field list and `salesforceID` regex.
911
+ - `clipboard-health/src/modules/user/controllers/user.controller.ts` — `/api/user/create` (worker-create), `/api/user/getByEmail`, `/api/user/agentSearch?searchInput=<email>` (find existing worker by email — returns both `_id` (agentProfile) and `userId`; for shift `agentId` use `userId`).
912
+ - `clipboard-health/src/modules/user/services/user-manipulation.service.ts → createAgent` — creates agentProfile + Cognito user in one call (no Mailpit pre-signup needed).
913
+ - `clipboard-health/packages/contract-backend-main/src/lib/user.contract.ts → createUserRequestSchema` — body shape.
914
+ - `clipboard-health/packages/contract-backend-main/src/lib/agentProfile.contract.ts → updateWorkerRequestSchema` — `PUT /api/agentprofile/put` (used to set `qualification`, `licensedStates`, etc).
915
+ - `clipboard-health/src/modules/shifts-invites/shift-invite.controller.ts` — shift invites.
916
+ - `clipboard-health/src/modules/shift-blocks/controllers/{shift-blocks, booking-requests}.controller.ts` — shift blocks.
917
+ - `clipboard-health/src/modules/payment/controllers/payment-invoice.controller.ts` — invoice generation (`POST /api/payment/sendInvoices`).
918
+ - `clipboard-health/src/modules/payment/helpers/payment-invoice.helper.ts → getFacilityByInvoiceSegments` — explains the `selectedIdsMap` keyed-by-segment-type structure.
919
+ - `clipboard-health/src/modules/invoice/services/invoice-segments.service.ts` — `segmentTypes` constants (`invoices-without-errors|invoices-with-errors|not-generated|not-generated-with-errors`).
920
+ - `clipboard-health/src/modules/invoice/repositories/invoice-segments.repository.ts` — Mongo match conditions for billable shifts (`agentId: $exists+$ne null`, etc).
921
+ - `clipboard-health/packages/contract-backend-main/src/lib/paymentInvoice.contract.ts` — `sendInvoicesBodySchema`, segment list query schema.
922
+ - `clipboard-health/src/models/shiftCancellationReason.type.ts` — cancellation reasons enum.
923
+ - `clipboard-health/src/workers/constants/workerStages/index.ts` — worker stages enum.
924
+ - `clipboard-health/src/services/urgentShifts/constants.ts` — urgency tier/reason enums.
925
+ - `clipboard-health/openapi.json` — full spec (grep only, 2.1 MB).
926
+ - `payment-service/src/app/payments/transfers/accounts-transfers.controller.ts` — transfers. GET needs `startDate`+`endDate`; transfers are fired via SQS events from backend-main's `triggerShiftPayment`, no direct POST.
927
+ - `payment-service/src/app/payments/payment-logs/payment-logs.controller.ts` — `GET /payment/payment-logs` is the canonical worker payment history read (see Verification patterns).
928
+ - `payment-service/src/app/accounts/accounts.controller.ts` — account reads.
929
+ - `payment-service/src/app/payouts/payouts.controller.ts` — payouts.
930
+ - `payment-service/src/app/bonuses/bonuses.service.ts` — bonus entity and `createBonusPayment`.
931
+ - `home-health-api/src/modules/cases/cases.worker.controller.ts` — worker browse.
932
+ - `home-health-api/src/modules/cases/cases.workplace.controller.ts` — workplace/admin case CRUD (`POST /api/v1/:workplaceId/cases`).
933
+ - `home-health-api/src/modules/patients/patients.controller.ts` — patient CRUD (`POST /api/v1/:workplaceId/patients`).
934
+ - `home-health-api/src/modules/visits/visits.{worker,workplace}.controller.ts` — visit endpoints; worker controller hosts `POST /api/v1/visits/:visitId/occurrences`.
935
+ - `home-health-api/src/modules/visits/visits.service.ts` — `checkForAccountPricing` enforces account-pricing precondition; `typeOfCare` derived from workplace.
936
+ - `home-health-api/src/modules/visits/dtos/createVisit.dto.ts` — note `pricingType` is `@IsOptional` but service requires it; no `typeOfCare` in body.
937
+ - `home-health-api/src/modules/visits-occurrences/visitsOccurrences.workplace.controller.ts` — occurrence approval (`PATCH /api/v1/:workplaceId/visit-occurrences/:id`).
938
+ - `home-health-api/src/modules/visits-occurrences/visitsOccurrences.service.ts` — `triggerPayment` is the bonus-payment side effect on APPROVED; gated by LD flag `2024-05-home-health-bonus-payment` (keyed by workplaceId).
939
+ - `home-health-api/src/modules/account-pricing/accountPricing.workplace.controller.ts` — `POST /api/v1/:workplaceId/account-pricing` to seed pricing. Service enforces inactive-workplace + P2002-duplicate guards.
940
+ - `home-health-api/src/modules/visits/fallback-rates/rate-configuration.json` — fallback charge rates used when LD flag `2026-01-configure-visit-rate-setting` is on and no pricing row exists.
941
+ - `@clipboard-health/flag-backend-main/src/lib/feature-flags/inHome.featureFlags.ts` — HH LD flag string constants (`VISIT_RATE_SETTING_FEATURE_FLAG`, `BONUS_PAYMENT_HH_ENABLED_FEATURE_FLAG`).
942
+ - `home-health-api/src/modules/cbh-core/utils.ts → stringTypeOfCareToEnum` — workplace.type ("Home Healthcare", "Hospice", …) → `TypeOfCare` enum mapping.
943
+ - `home-health-api/src/modules/cbh-core/cbhPayment.service.ts` — bonus-payment HTTP call to payment-service (`POST /bonuses`).
944
+ - `home-health-api/prisma/schema.prisma` — visit/occurrence/case/patient models, full `VisitType` and `TypeOfCare` and `PricingType` enums.
945
+ - `payment-service/src/app/accounts/accounts.controller.ts:204` — `POST /accounts` create flow; `gatewayAccountCreationIsEnabled:false` skips Stripe in non-prod, but Mongoose validation still requires non-empty `accountId`.
946
+ - `documents-service-backend/src/app/rest-api/controllers/hcp-documents.controller.ts` — documents.
947
+ - `documents-service-backend/src/app/rest-api/licenses/dtos/license-data.dto.ts` — license schema.
948
+ - `shift-reviews-service/src/...` — preferred workers.
949
+ - `attendance-policy/src/...` — attendance policies/scores/restrictions.
950
+ - `cbh-core/packages/cli/src/oclif/auth/gentoken.ts` — CLI token flow.
951
+ - `cbh-core/packages/testing-e2e-admin-app/src/lib/constants.ts` — **reference payloads** (`DEFAULT_*` bodies).
952
+ - `cbh-core/packages/testing-e2e-admin-app/src/lib/admin-service.ts` — `AdminService` orchestration reference.
953
+ - `cbh-core/packages/testing-e2e-admin-app/src/lib/service-urls.ts` — env → URL map.
954
+ - `clipboard-health/src/modules/ten99-policy/ten99-policy.constants.ts` — production-only category codes; CONVALESCENT_HOME state list.
955
+ - `clipboard-health/src/modules/ten99-policy/types/ten99-policy-api.types.ts` — 1099Policy.com API constraints (wage min/max, address shape, `effective_date` must be future).
956
+ - `clipboard-health/src/modules/ten99-policy/logic/ten99-policy.service.ts` — `createAssignment` orchestration (createJob → createAssignment in 1099Policy → record netRate).
957
+ - `clipboard-health/src/modules/ten99-policy/logic/ten99-policy-assignment-fee-calculation.service.ts` + `utils/compute-shift-ten99-policy-fee.ts` — fee math (uses BASE_PAY-named offer rule outputs when present, else falls back to `shift.pay × 100`).
958
+ - `clipboard-health/src/modules/ten99-policy/entrypoints/ten99-policy.dto.mapper.ts` — note: GET-jobs mapper does NOT surface `assignmentFeeInCents`; use `GET /api/shifts/:id/1099-policy/assignments` for the fee read.
959
+ - `clipboard-health/src/modules/worker-payment/domain/shift-payment.domain.ts` (and `.spec.ts`) — deduction code path; **only `TIMESHEET_UPLOAD` and `RETRY_MISSING_PAYMENT_TIMESHEET_UPLOAD` subtract the fee from payable amount** (VAU-1106).
960
+ - `clipboard-health/node_modules/@clipboard-health/contract-backend-main/src/lib/ten99Policy.contract.ts` — endpoints: `POST /api/1099-policy/application-sessions`, `POST /api/1099-policy/assignments`, `POST /api/workplaces/:wpId/1099-policy/entities`, `GET /api/shifts/:id/1099-policy/assignments`, `GET /api/workers/:wId/1099-policy/jobs`, `POST /api/1099-policy/events`.
961
+
962
+ If any path 404s on your machine, ask the user for the correct location — repos may be under a different root.
@@ -1,5 +1,5 @@
1
1
  ---
2
- allowed-tools: Bash(git checkout --branch:*), Bash(git add:*), Bash(git status:*), Bash(git push:*), Bash(git commit:*), Bash(gh pr view:*), Bash(gh pr create:*), Bash(git diff:*)
2
+ allowed-tools: Bash(git checkout --branch:*), Bash(git add:*), Bash(git status:*), Bash(git push:*), Bash(git commit:*), Bash(gh pr view:*), Bash(gh pr create:*), Bash(git diff:*), Bash(git merge-base:*)
3
3
  description: Commit, push, and open a PR. Use when the user wants to ship changes, create a pull request, or says things like 'commit and push', 'open a PR', 'ship it', 'send it', 'create a PR for this', or 'push this up'.
4
4
  ---
5
5
 
@@ -17,16 +17,17 @@ description: Commit, push, and open a PR. Use when the user wants to ship change
17
17
  **First, decide from the context above. If `Commits ahead of default branch` is `(unknown)`, skip this decision and use the full flow below.**
18
18
 
19
19
  - If `Git status` is empty AND `Commits ahead of default branch` is empty AND `Existing PR` is `none`: stop. Reply with `nothing to ship.` and do nothing else.
20
- - If `Git status` is empty but there are `Commits ahead of default branch` or an `Existing PR`: skip step 2 only (no changes to commit). Still run step 1 its "if on main" check needs to fire so local commits on main are moved to a new branch rather than pushed directly to main. Then continue with step 3 and step 4.
20
+ - If `Git status` is empty but there are `Commits ahead of default branch` or an `Existing PR`: still run step 1 and step 2. Then re-check `git status --short`; skip step 3 only if it remains empty. Then continue with step 4 and step 5.
21
21
  - Otherwise: proceed with all steps below.
22
22
 
23
23
  Based on the above changes:
24
24
 
25
25
  1. Create a new branch if on main (e.g., `feat/add-user-validation`, `fix/null-check-in-parser`)
26
- 2. Create a single conventional commit message
27
- 3. Push the branch to origin
28
- 4. Check for an existing PR with `gh pr view`.
29
- - No PR exists: Create with `gh pr create`. Title = commit subject line. Description = brief explanation of **why**, not what.
26
+ 2. Run the `simplify` skill on the files included in the PR before committing, pushing, or opening the PR. If the working tree is clean, run `simplify` against `git diff $(git merge-base HEAD origin/HEAD)..HEAD`. If the working tree has changes, run it against `git diff HEAD`; when local commits also exist, include `git diff $(git merge-base HEAD origin/HEAD)..HEAD` as additional PR context. Invoke the skill by name using this agent's normal skill invocation mechanism. Wait for the skill to finish, then include any resulting fixes in the commit step below.
27
+ 3. Re-check `git status --short`. If there are changes, create a single conventional commit message.
28
+ 4. Push the branch to origin
29
+ 5. Check for an existing PR with `gh pr view`.
30
+ - No PR exists: Create with `gh pr create`. Title = commit subject line. Description = brief explanation of **why**, not what. Append `<!-- commit-push-pr:created v1 -->` on its own line at the end of the PR description so skill-created PRs can be identified later.
30
31
  - PR exists: Report the URL and move on.
31
- 5. You have the capability to call multiple tools in a single response. You MUST do all of the above in a single message. Do not use any other tools or do anything else.
32
- 6. After tool calls complete, send one short final text response with the branch name and the full PR URL (e.g., `https://github.com/clipboardhealth/core-utils/pull/123`). Never use shorthand like `repo#123` — always output the complete URL so it is clickable.
32
+ 6. You have the capability to call multiple tools in a single response. After the `simplify` skill completes, do the remaining git and PR operations in a single message. Do not use any other tools or do anything else.
33
+ 7. After tool calls complete, send one short final text response with the branch name and the full PR URL (e.g., `https://github.com/clipboardhealth/core-utils/pull/123`). Never use shorthand like `repo#123` — always output the complete URL so it is clickable.
@@ -0,0 +1,62 @@
1
+ ---
2
+ description: Implement a plan file or direct request end-to-end, then hand off to commit-push-pr to ship it. Use when the user says 'go', 'execute the plan', 'implement the plan', or gives an implementation request.
3
+ ---
4
+
5
+ # Go: Execute Work and Ship It
6
+
7
+ Given a plan file or direct implementation request, implement the requested work, then invoke the `commit-push-pr` skill to commit, push, and open a PR.
8
+
9
+ This skill is the bridge between intent and shipping. It does not re-plan or re-design unless the user asked for that. If the request is wrong or cannot be implemented safely, surface that; do not silently improvise.
10
+
11
+ ## Phase 1: Understand the Input
12
+
13
+ The user may invoke this skill with either a plan file path/identifier or a direct implementation request such as "add a button to the homepage to log in." Resolve the input in this order:
14
+
15
+ 1. **Absolute path** (starts with `/`): read it directly only when it is inside the current workspace/repo root or the host's plans directory. If it is outside those roots, ask the user to confirm before reading it; if they do not confirm, stop and ask for an approved path.
16
+ 2. **Relative path** (contains `/` or starts with `./`): resolve from the current working directory.
17
+ 3. **Bare name** (no path separators, e.g. `my-plan` or `my-plan.md`): if the host exposes a plans directory, look there first. Use whatever directory the current host stores plan files in — do not hard-code a host-specific path. If no plan is found and the input is ambiguous, ask whether it is a plan identifier or an implementation request.
18
+ 4. **Natural-language request** (for example, contains spaces or reads like an implementation task): treat it as the implementation request. There may be no separate plan file.
19
+ 5. **No argument given**: ask the user to provide either the plan file path or the implementation request. Do not guess.
20
+
21
+ If a path-like input does not exist or cannot be read, stop and tell the user. Do not reinterpret a missing path as a direct request.
22
+
23
+ When using a plan, read the entire plan before starting. Note the **Critical files**, **Approach**, and **Verification** sections (or their equivalents).
24
+
25
+ ## Phase 2: Implement the Request
26
+
27
+ Treat the plan or direct request as the source of truth for scope:
28
+
29
+ - Make exactly the changes requested — no more, no less. Do not add extra refactors, tests, or cleanup the request did not ask for.
30
+ - If a plan references files, functions, or utilities that no longer exist, stop and surface the discrepancy before guessing.
31
+ - If you discover the request is wrong (the codebase has moved, an assumption is invalid, a constraint was missed), stop and report. Do not silently rewrite the approach.
32
+ - For direct implementation requests, inspect the relevant code before editing and use the repo's existing patterns.
33
+ - Do **not** modify the plan file itself when working from a plan.
34
+
35
+ ## Phase 3: Validate
36
+
37
+ Validation should follow the repo's instructions and the cost profile of the repo. Some repos intentionally leave slow checks to CI; do not run broad or slow suites unless the repo instructions explicitly require them.
38
+
39
+ Run validation in this order:
40
+
41
+ 1. **Repo instructions.** Look up the repo's standard validation guidance. Common signals, in priority order:
42
+ - An explicit instruction in the repo's agent or contributor instructions, such as `AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md`, or an equivalent host-specific instruction file (e.g. "MUST run before opening PRs"). This wins over everything else.
43
+ - A relevant `package.json` script when the repo instructions point to it, such as `affected`, `precommit`, `validate`, `check`, `verify`, `test`, `lint`, `typecheck`, or repo-equivalent.
44
+ - A repo-specific equivalent in the rare case the project does not use `package.json`.
45
+
46
+ If the repo primarily relies on git pre-commit or pre-push hooks and does not mandate a manual validation command, do not invent one. Let the hooks run during the `commit-push-pr` handoff.
47
+
48
+ 2. **Request-specific verification.** If the plan or user request names specific checks, run them when they are practical and consistent with the repo instructions. If a requested check is clearly a slow CI-only suite, ask before running it.
49
+
50
+ If any check you run fails, fix the failures and re-run the same check. Do not hand off to `commit-push-pr` with known failing checks unless the user explicitly accepts a pre-existing or unrelated failure.
51
+
52
+ ## Phase 4: Hand Off to commit-push-pr
53
+
54
+ Once implementation is complete and Phase 3 validation is satisfied, invoke the `commit-push-pr` skill by name using this agent's normal skill invocation mechanism.
55
+
56
+ Do **not** commit, push, or open the PR yourself in this skill. Let `commit-push-pr` own the shipping flow.
57
+
58
+ If `commit-push-pr` reports `nothing to ship.` (the work resulted in no actual file changes), surface that to the user — it usually means the request was already implemented or was a no-op.
59
+
60
+ ## Phase 5: Final Output
61
+
62
+ After `commit-push-pr` returns, ensure the user sees one short final response with the branch name and the full PR URL it produced. If the current host already surfaced that response, do not duplicate it. If `commit-push-pr` reported nothing to ship, say so plainly.
@@ -12,7 +12,7 @@ Run \`git diff\` (or \`git diff HEAD\` if there are staged changes) to see what
12
12
 
13
13
  ## Phase 2: Launch Three Review Agents in Parallel
14
14
 
15
- If you can, use the ${AGENT_TOOL_NAME} tool to launch all three agents concurrently in a single message; otherwise, perform the following yourself. Pass each agent the full diff so it has the complete context.
15
+ If the current host supports parallel delegated reviewers, use that mechanism to launch all three reviews concurrently; otherwise, perform the reviews yourself. Pass each reviewer the full diff so it has the complete context.
16
16
 
17
17
  ### Agent 1: Code Reuse Review
18
18