@chrischall/pickuppatrol-mcp 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +35 -0
- package/.claude-plugin/plugin.json +22 -0
- package/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/auth.d.ts +72 -0
- package/dist/auth.js +147 -0
- package/dist/bundle.js +32156 -0
- package/dist/client.d.ts +57 -0
- package/dist/client.js +168 -0
- package/dist/dates.d.ts +28 -0
- package/dist/dates.js +51 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +24 -0
- package/dist/lib.d.ts +11 -0
- package/dist/lib.js +10 -0
- package/dist/plans.d.ts +57 -0
- package/dist/plans.js +165 -0
- package/dist/tools/_confirm.d.ts +13 -0
- package/dist/tools/_confirm.js +23 -0
- package/dist/tools/_errors.d.ts +14 -0
- package/dist/tools/_errors.js +26 -0
- package/dist/tools/account.d.ts +8 -0
- package/dist/tools/account.js +97 -0
- package/dist/tools/defaults.d.ts +9 -0
- package/dist/tools/defaults.js +169 -0
- package/dist/tools/plans.d.ts +15 -0
- package/dist/tools/plans.js +133 -0
- package/dist/tools/school.d.ts +10 -0
- package/dist/tools/school.js +77 -0
- package/dist/types.d.ts +142 -0
- package/dist/types.js +9 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.js +10 -0
- package/package.json +71 -0
- package/server.json +20 -0
- package/skills/pickuppatrol-api/SKILL.md +107 -0
- package/skills/pickuppatrol-api/references/api.md +168 -0
package/dist/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types for the PickUp Patrol ServiceStack API.
|
|
3
|
+
*
|
|
4
|
+
* Every field here was read off the shipped SPA's own request/response DTO
|
|
5
|
+
* classes or observed on a live response — see `docs/PICKUPPATROL-API.md` for
|
|
6
|
+
* the capture. Responses are typed loosely on purpose: the API is
|
|
7
|
+
* undocumented, so an unexpected extra field must not break a read.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the server's self-reported version.
|
|
3
|
+
*
|
|
4
|
+
* The literal on the VERSION line below is rewritten by release-please (it is
|
|
5
|
+
* registered in `release-please-config.json` under `extra-files`). Everything
|
|
6
|
+
* that needs a version imports this constant rather than repeating the
|
|
7
|
+
* annotation, so there is exactly one line to keep in sync and
|
|
8
|
+
* `versionSyncTest` has exactly one line to check.
|
|
9
|
+
*/
|
|
10
|
+
export declare const VERSION = "0.0.0";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the server's self-reported version.
|
|
3
|
+
*
|
|
4
|
+
* The literal on the VERSION line below is rewritten by release-please (it is
|
|
5
|
+
* registered in `release-please-config.json` under `extra-files`). Everything
|
|
6
|
+
* that needs a version imports this constant rather than repeating the
|
|
7
|
+
* annotation, so there is exactly one line to keep in sync and
|
|
8
|
+
* `versionSyncTest` has exactly one line to check.
|
|
9
|
+
*/
|
|
10
|
+
export const VERSION = '0.0.0'; // x-release-please-version
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chrischall/pickuppatrol-mcp",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"mcpName": "io.github.chrischall/pickuppatrol-mcp",
|
|
6
|
+
"description": "PickUp Patrol MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
7
|
+
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/chrischall/pickuppatrol-mcp.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"mcp",
|
|
14
|
+
"model-context-protocol",
|
|
15
|
+
"claude",
|
|
16
|
+
"ai",
|
|
17
|
+
"pickuppatrol",
|
|
18
|
+
"school",
|
|
19
|
+
"dismissal",
|
|
20
|
+
"carpool",
|
|
21
|
+
"parents"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22.0.0"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"pickuppatrol-mcp": "dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"main": "dist/lib.js",
|
|
31
|
+
"types": "dist/lib.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/lib.d.ts",
|
|
35
|
+
"import": "./dist/lib.js"
|
|
36
|
+
},
|
|
37
|
+
"./package.json": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
".claude-plugin",
|
|
45
|
+
"skills",
|
|
46
|
+
".mcp.json",
|
|
47
|
+
"server.json"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc && npm run bundle",
|
|
51
|
+
"bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js",
|
|
52
|
+
"dev": "node dist/index.js",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:coverage": "vitest run --coverage",
|
|
56
|
+
"test:watch": "vitest"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@chrischall/mcp-utils": "^0.14.0",
|
|
60
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
61
|
+
"dotenv": "^17.4.0",
|
|
62
|
+
"zod": "^4.4.3"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@types/node": "^26.1.1",
|
|
66
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
67
|
+
"esbuild": "^0.28.0",
|
|
68
|
+
"typescript": "^7.0.2",
|
|
69
|
+
"vitest": "^4.1.7"
|
|
70
|
+
}
|
|
71
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.chrischall/pickuppatrol-mcp",
|
|
4
|
+
"description": "Read and change your children's school dismissal plans in PickUp Patrol",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/chrischall/pickuppatrol-mcp",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.0.0",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "npm",
|
|
13
|
+
"identifier": "@chrischall/pickuppatrol-mcp",
|
|
14
|
+
"version": "0.0.0",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pickuppatrol-api
|
|
3
|
+
description: "Read and change your children's school dismissal plans on PickUp Patrol (app.pickuppatrol.net) from a shell with curl — students, weekly defaults, day-by-day changes, school cutoff times."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PickUp Patrol from a shell
|
|
7
|
+
|
|
8
|
+
`app.pickuppatrol.net` is a ServiceStack JSON API behind an Ionic/Vue SPA. It is
|
|
9
|
+
reachable **server-side with plain `curl`** — no bot wall, no captcha, no
|
|
10
|
+
browser bridge. Use this when you want PickUp Patrol data in a script or on a
|
|
11
|
+
machine without the `pickuppatrol-mcp` server.
|
|
12
|
+
|
|
13
|
+
Full captured shapes: `references/api.md`.
|
|
14
|
+
|
|
15
|
+
## Sign in once per shell
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
export PUP_USER='you@example.com'
|
|
19
|
+
read -rs PUP_PASS && export PUP_PASS # never put the password in a command line
|
|
20
|
+
export PUP=https://app.pickuppatrol.net/api/json/reply
|
|
21
|
+
|
|
22
|
+
pup_login() {
|
|
23
|
+
local jar; jar=$(mktemp -t pupjar)
|
|
24
|
+
curl -sS -c "$jar" -X POST "$PUP/Authenticate" \
|
|
25
|
+
-H 'Content-Type: application/json' \
|
|
26
|
+
-d "$(jq -nc --arg u "$PUP_USER" --arg p "$PUP_PASS" \
|
|
27
|
+
'{provider:"credentials",UserName:$u,Password:$p,RememberMe:true}')" \
|
|
28
|
+
-o /tmp/pup_auth.json -w '%{http_code}' >/tmp/pup_code
|
|
29
|
+
[ "$(cat /tmp/pup_code)" = 200 ] || { jq -r '.ResponseStatus.Message' /tmp/pup_auth.json >&2; return 1; }
|
|
30
|
+
export PUP_JAR="$jar"
|
|
31
|
+
PUP_TOKEN=$(jq -r '.BearerToken // empty' /tmp/pup_auth.json); export PUP_TOKEN
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Every call goes through this: it sends the session cookies, and the bearer
|
|
35
|
+
# token as well when the deployment issued one.
|
|
36
|
+
pup() {
|
|
37
|
+
local method=$1 dto=$2; shift 2
|
|
38
|
+
local -a auth=(-b "$PUP_JAR")
|
|
39
|
+
[ -n "$PUP_TOKEN" ] && auth+=(-H "Authorization: Bearer $PUP_TOKEN")
|
|
40
|
+
curl -sS -X "$method" "$PUP/$dto" -H 'Content-Type: application/json' "${auth[@]}" "$@"
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Never re-run a rejected login.** PickUp Patrol answers bad credentials with
|
|
45
|
+
`ResponseStatus.ErrorCode` starting `LOGIN-ERROR`, counts the attempt against
|
|
46
|
+
the account, and a lockout clears only through their support desk. One failure
|
|
47
|
+
= stop and fix the credentials.
|
|
48
|
+
|
|
49
|
+
## Reads
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pup GET GetSession | jq '{name:.DisplayName, email:.Email, kids:[.Children[].StudentId]}'
|
|
53
|
+
pup GET GetChildren | jq -r '.[] | "\(.StudentId) \(.FirstName) — \(.SchoolName)"'
|
|
54
|
+
|
|
55
|
+
# Weekly default plan for one student. DayId is 1-based, SUNDAY = 1.
|
|
56
|
+
pup GET GetChildren | jq '.[] | select(.StudentId==1050046) | .DefaultPlans
|
|
57
|
+
| sort_by(.DayId) | map({day:.WeekDayName, option:.TransportationName, note:.Note})'
|
|
58
|
+
|
|
59
|
+
# The dismissal options a school offers, with the rules each imposes.
|
|
60
|
+
pup GET "GetTransportations?SchoolId=1703" | jq -r '.[] | select(.IsActive)
|
|
61
|
+
| "\(.TransportationId)\t\(.Name)\tnote=\(.IsNoteRequired)\tcar=\(.UseCarNumbers)\tearly=\(.IsEarlyDismissal)"'
|
|
62
|
+
|
|
63
|
+
pup GET "GetPlanEdit?PlanDate=2026-08-17&StudentId=1050046" | jq '{TransportationName,Note,IsLocked}'
|
|
64
|
+
pup GET "GetInvalidPlanDates?SchoolId=1703" | jq 'length' # non-school days
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Writes
|
|
68
|
+
|
|
69
|
+
Writes change how a child actually leaves school. Read
|
|
70
|
+
`GetTransportations` first and obey the option's rules, or the request is
|
|
71
|
+
rejected (or worse, silently ignored):
|
|
72
|
+
|
|
73
|
+
- `IsNoteRequired` → `Note` must be non-empty
|
|
74
|
+
- `UseCarNumbers` → send `CarNumber`; otherwise omit the key
|
|
75
|
+
- `IsEarlyDismissal` → send `EarlyDismissalTime` as `HH:MM:SS`; otherwise omit it
|
|
76
|
+
- `IsLimited` → only usable if the id is in the student's `LimitedIds`
|
|
77
|
+
|
|
78
|
+
**Change specific dates** — `Plans` is an array, one entry per date:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pup PUT UpdatePlans -d '{"Plans":[{
|
|
82
|
+
"StudentId":1050046,"SchoolId":1703,"PlanDate":"2026-08-17",
|
|
83
|
+
"TransportationId":41246,"TransportationName":"PickUp","Note":"Chris Hall"}]}'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Clear a date back to the weekly default** — a null transportation:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pup PUT UpdatePlans -d '{"Plans":[{
|
|
90
|
+
"StudentId":1050046,"SchoolId":1703,"PlanDate":"2026-08-17",
|
|
91
|
+
"TransportationId":null,"TransportationName":"Default plan","Note":null}]}'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Change the weekly defaults** — there is no default-plans endpoint. Read the
|
|
95
|
+
whole student, edit `DefaultPlans`, PUT the whole record back
|
|
96
|
+
(`references/api.md` has the read-modify-write recipe).
|
|
97
|
+
|
|
98
|
+
### Always re-read to verify
|
|
99
|
+
|
|
100
|
+
A 2xx is not proof. The school silently ignores a change past its cutoff time.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pup GET "GetPlanEdit?PlanDate=2026-08-17&StudentId=1050046" | jq '.TransportationId'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Compare `TransportationId` — **not** `ModifiedDate`, which advances on its own
|
|
107
|
+
and would make every write look successful.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# PickUp Patrol API reference
|
|
2
|
+
|
|
3
|
+
Every shape here was read off the shipped SPA's own request DTO classes or
|
|
4
|
+
observed on a live signed-in session (2026-08-16). No credential, cookie or
|
|
5
|
+
token value is recorded.
|
|
6
|
+
|
|
7
|
+
- Base: `https://app.pickuppatrol.net/api/json/reply/<DtoName>`
|
|
8
|
+
- GET args on the query string; POST/PUT/PATCH args in a JSON body
|
|
9
|
+
- Auth: session cookies from `Authenticate`, plus `Authorization: Bearer <jwt>`
|
|
10
|
+
when the deployment issues one
|
|
11
|
+
- Errors: ServiceStack `{"ResponseStatus":{"ErrorCode","Message","Errors":[…]}}`;
|
|
12
|
+
an unauthenticated call to any authorised DTO is a bare `401`
|
|
13
|
+
|
|
14
|
+
Assumes the `pup` / `pup_login` shell functions from `SKILL.md`.
|
|
15
|
+
|
|
16
|
+
## Authentication
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
curl -sS -c jar -X POST "$PUP/Authenticate" -H 'Content-Type: application/json' \
|
|
20
|
+
-d '{"provider":"credentials","UserName":"…","Password":"…","RememberMe":true}'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Response: `{UserId, SessionId, UserName, DisplayName, BearerToken, RefreshToken,
|
|
24
|
+
Roles, Permissions, ResponseStatus}`.
|
|
25
|
+
|
|
26
|
+
Two-factor accounts (`User.OtpTypeId` set) need `POST /CreateOtp {OtpTypeId}`
|
|
27
|
+
then `PUT /VerifyOtp {Otp, RememberMe}`. Token refresh is
|
|
28
|
+
`POST /GetAccessToken {refreshToken}` or `GET /RequestNewtoken?OldToken=…`.
|
|
29
|
+
|
|
30
|
+
## Read DTOs
|
|
31
|
+
|
|
32
|
+
| DTO | Args |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `GetSession` | — |
|
|
35
|
+
| `GetSessionInfo` | — |
|
|
36
|
+
| `GetChildren` | — |
|
|
37
|
+
| `GetStudent` | `StudentId`, `MergeTimeWithNote?` |
|
|
38
|
+
| `GetDefaultPlansReviewNeeded` | — |
|
|
39
|
+
| `GetParentPlans` | `StartDate`, `EndDate` |
|
|
40
|
+
| `GetPlanEdit` | `PlanDate`, `StudentId` |
|
|
41
|
+
| `GetBoldedDates` | `StartDate`, `EndDate` (dates differing from the default) |
|
|
42
|
+
| `GetInvalidPlanDates` | `SchoolId` (non-school days) |
|
|
43
|
+
| `GetTransportations` | `SchoolId` |
|
|
44
|
+
| `GetCarNumbers` | `SchoolId` |
|
|
45
|
+
| `GetSchool` | `SchoolId` |
|
|
46
|
+
| `GetSchoolNotifyTimes` | `SchoolId` |
|
|
47
|
+
| `GetSchoolSettings` | `SchoolId` |
|
|
48
|
+
| `GetMiddayPlan` | `StudentId`, `PlanDate`, `TypeId?` |
|
|
49
|
+
| `GetParentHealthScreenAudits` | `ScreenDate` |
|
|
50
|
+
| `GetTermsOfUse` | — |
|
|
51
|
+
|
|
52
|
+
### Student
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
StudentId SchoolId SchoolName FirstName LastName IsActive SASId TeacherId
|
|
56
|
+
AllowPlans CreateDate CreatedBy ModifiedDate ModifiedBy DefaultsModifiedDate
|
|
57
|
+
DefaultPlanModifiedBy DefaultsReviewedDate DefaultsReviewedBy DefaultPlans
|
|
58
|
+
SafetyFlag DefaultCarNumber LimitedIds
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`DefaultPlans[]`:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{"StudentId":1050046,"DayId":2,"TransportationId":41246,"Note":"Chris Hall",
|
|
65
|
+
"TransportationName":"PickUp","WeekDayName":"Monday","EarlyDismissalTime":null,
|
|
66
|
+
"CarNumber":null,"UseCarNumbers":false}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`DayId` is 1-based with **Sunday = 1** — verified from a live record where
|
|
70
|
+
`DayId: 2` carried `WeekDayName: "Monday"`.
|
|
71
|
+
|
|
72
|
+
### Transportation
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
TransportationId SchoolId Name NoteHint IsNoteRequired UseCarNumbers
|
|
76
|
+
IsNotePrivate IsActive Sequence IsEarlyDismissal IsLimited CutoffTime
|
|
77
|
+
CutoffTime{Sunday…Saturday} AllowParentCheck
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### GetPlanEdit
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
PlanDate StudentId FirstName LastName SchoolId TransportationId Note IsLocked
|
|
84
|
+
TransportationName SchoolName BusRouteUrl ValidationErrors EarlyDismissalTime
|
|
85
|
+
CarNumber LimitedIds IsNotePrivate
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Write DTOs
|
|
89
|
+
|
|
90
|
+
| DTO | Verb | Body |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `UpdatePlans` | PUT | `{Plans:[…]}` |
|
|
93
|
+
| `Student` | PUT | the whole student record |
|
|
94
|
+
| `SetDefaultsReviewed` | PUT | `{StudentId, Reviewed}` |
|
|
95
|
+
| `UpdateMiddayPlanCheck` | PUT | `{StudentId, PlanDate, TypeId, Action}` |
|
|
96
|
+
| `MiddayPlan` | PUT | full midday-plan record |
|
|
97
|
+
| `UpdateSendPlanConfirmationEmail` | PUT | `{Send}` |
|
|
98
|
+
| `UpdateUserLanguage` | PATCH | `{Language}` |
|
|
99
|
+
| `AcceptTerms` | PUT | — |
|
|
100
|
+
| `ChangePassword` | PUT | `{CurrentPassword, NewPassword, ConfirmPassword}` |
|
|
101
|
+
|
|
102
|
+
### UpdatePlans element
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{"StudentId":1050046,"SchoolId":1703,"PlanDate":"2026-08-17",
|
|
106
|
+
"TransportationId":41246,"TransportationName":"PickUp","Note":"Chris Hall",
|
|
107
|
+
"EarlyDismissalTime":"13:00:00","CarNumber":"12"}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Omit `EarlyDismissalTime` unless the option has `IsEarlyDismissal`, and
|
|
111
|
+
`CarNumber` unless it has `UseCarNumbers` — the web app leaves both keys off
|
|
112
|
+
otherwise.
|
|
113
|
+
|
|
114
|
+
Build several dates at once:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
jq -nc --argjson dates '["2026-08-17","2026-08-18"]' '
|
|
118
|
+
{Plans: ($dates | map({
|
|
119
|
+
StudentId:1050046, SchoolId:1703, PlanDate:.,
|
|
120
|
+
TransportationId:41246, TransportationName:"PickUp", Note:"Chris Hall"}))}' \
|
|
121
|
+
| xargs -0 -I{} sh -c 'pup PUT UpdatePlans -d "$1"' _ {}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Weekly defaults: read-modify-write the whole student
|
|
125
|
+
|
|
126
|
+
There is no default-plans endpoint. Read the student, replace only
|
|
127
|
+
`DefaultPlans`, PUT the whole record to the `Student` DTO.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
STUDENT=1050046
|
|
131
|
+
pup GET "GetStudent?StudentId=$STUDENT" > /tmp/pup_student.json
|
|
132
|
+
|
|
133
|
+
# Set Monday (DayId 2) and Tuesday (3) to Bus (41245).
|
|
134
|
+
jq '.DefaultPlans = (
|
|
135
|
+
(.DefaultPlans // []) as $p
|
|
136
|
+
| [2,3] as $days
|
|
137
|
+
| ($p | map(select(.DayId as $d | $days | index($d) | not)))
|
|
138
|
+
+ ($days | map({DayId:., TransportationId:41245, TransportationName:"Bus", Note:null}))
|
|
139
|
+
| sort_by(.DayId))' /tmp/pup_student.json > /tmp/pup_student_new.json
|
|
140
|
+
|
|
141
|
+
pup PUT Student -d @/tmp/pup_student_new.json
|
|
142
|
+
|
|
143
|
+
# Verify: compare TransportationId per DayId, never DefaultsModifiedDate.
|
|
144
|
+
pup GET "GetStudent?StudentId=$STUDENT" \
|
|
145
|
+
| jq '.DefaultPlans | sort_by(.DayId) | map({DayId, TransportationId})'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Clear every default with `jq '.DefaultPlans = []'`.
|
|
149
|
+
|
|
150
|
+
Send every other field back verbatim — the whole record round-trips, so a
|
|
151
|
+
dropped field is a field cleared.
|
|
152
|
+
|
|
153
|
+
## Gates
|
|
154
|
+
|
|
155
|
+
- `GetSchoolSettings(...).General.AllowDefaultPlans` — school allows default plans
|
|
156
|
+
- `Student.AllowPlans` — this student's plans may be changed
|
|
157
|
+
- `GetSchoolNotifyTimes` — a weekday with no `NotifyTime<Day>` is not a school day
|
|
158
|
+
- `GetPlanEdit(...).IsLocked` — the cutoff for that date has passed
|
|
159
|
+
|
|
160
|
+
## Verifying a write
|
|
161
|
+
|
|
162
|
+
Re-read and compare the field that proves the change:
|
|
163
|
+
|
|
164
|
+
- one-off plan → `GetPlanEdit(PlanDate, StudentId).TransportationId`
|
|
165
|
+
- weekly defaults → `GetStudent(StudentId).DefaultPlans[].TransportationId`
|
|
166
|
+
|
|
167
|
+
Exclude `ModifiedDate` and `DefaultsModifiedDate`: they advance on their own, so
|
|
168
|
+
including them makes every write look successful.
|