@chrischall/pickuppatrol-mcp 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +20 -5
- package/dist/bundle.js +25 -16
- package/dist/lib.d.ts +2 -1
- package/dist/lib.js +2 -1
- package/dist/tools/defaults.js +12 -6
- package/dist/tools/plans.d.ts +20 -7
- package/dist/tools/plans.js +34 -20
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/pickuppatrol-api/SKILL.md +9 -2
- package/skills/pickuppatrol-api/references/api.md +19 -6
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "PickUp Patrol school-dismissal tools for Claude Code",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.1.1"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"displayName": "PickUp Patrol",
|
|
15
15
|
"source": "./",
|
|
16
16
|
"description": "Read and change your children's school dismissal plans in PickUp Patrol — defaults, day-by-day changes and school cutoff times — via MCP",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.1.1",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pickuppatrol",
|
|
3
3
|
"displayName": "PickUp Patrol",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.1.1",
|
|
5
5
|
"description": "Read and change your children's school dismissal plans in PickUp Patrol — defaults, day-by-day changes and school cutoff times — via MCP",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Chall"
|
package/README.md
CHANGED
|
@@ -16,7 +16,8 @@ no captcha. The server signs in with your own email and password and holds the
|
|
|
16
16
|
resulting session in memory.
|
|
17
17
|
|
|
18
18
|
Every request shape is captured in [`docs/PICKUPPATROL-API.md`](docs/PICKUPPATROL-API.md),
|
|
19
|
-
read off the shipped client rather than guessed
|
|
19
|
+
read off the shipped client rather than guessed, and exercised against a real
|
|
20
|
+
account — reads, a write, and its restore.
|
|
20
21
|
|
|
21
22
|
## Install
|
|
22
23
|
|
|
@@ -78,10 +79,12 @@ a validation message rather than an opaque 400.
|
|
|
78
79
|
|
|
79
80
|
**Writes are verified by re-reading.** A 2xx from PickUp Patrol is not proof: a
|
|
80
81
|
change made after the school's cutoff is accepted and silently ignored. Every
|
|
81
|
-
write re-reads the affected dates and compares the transportation id
|
|
82
|
-
`ModifiedDate`, which advances on its own and would make every
|
|
83
|
-
successful. The
|
|
84
|
-
|
|
82
|
+
write re-reads the affected dates and compares the transportation id **and the
|
|
83
|
+
note** — never `ModifiedDate`, which advances on its own and would make every
|
|
84
|
+
write look successful. The note is part of the proof because every dismissal
|
|
85
|
+
option seen so far requires one, which makes a note-only edit ordinary: an
|
|
86
|
+
id-only comparison would report success from a field that never had to move.
|
|
87
|
+
The result says `verified: true/false`, and names the dates that did not move.
|
|
85
88
|
|
|
86
89
|
## Without the MCP server
|
|
87
90
|
|
|
@@ -100,6 +103,18 @@ npm run test:coverage # coverage-enforced at 100%
|
|
|
100
103
|
Tests never touch the network: the transport is injected, and the MCP tools run
|
|
101
104
|
through a real in-memory client/server pair.
|
|
102
105
|
|
|
106
|
+
## Two things about the API worth knowing
|
|
107
|
+
|
|
108
|
+
**`GetPlanEdit` returns a date's override, not the effective plan.** A date with
|
|
109
|
+
no specific plan reads back `TransportationId: null` even when the student has a
|
|
110
|
+
weekly default for that weekday. `pup_get_plan` passes that through as-is;
|
|
111
|
+
`pup_list_students` is where the weekly defaults live.
|
|
112
|
+
|
|
113
|
+
**Sign-in is a session cookie, not a JWT.** `Authenticate` returns
|
|
114
|
+
`BearerToken: null` on this deployment and sets `ss-id`/`ss-pid`/`ss-opt`. The
|
|
115
|
+
client keeps whichever the server returns and sends both, so a future switch to
|
|
116
|
+
JWTs needs no change here.
|
|
117
|
+
|
|
103
118
|
## Safety notes
|
|
104
119
|
|
|
105
120
|
- A rejected sign-in is **never retried**. PickUp Patrol counts failed attempts
|
package/dist/bundle.js
CHANGED
|
@@ -31503,7 +31503,7 @@ var PickUpPatrolClient = class {
|
|
|
31503
31503
|
var client = new PickUpPatrolClient();
|
|
31504
31504
|
|
|
31505
31505
|
// src/version.ts
|
|
31506
|
-
var VERSION = "0.
|
|
31506
|
+
var VERSION = "0.1.1";
|
|
31507
31507
|
|
|
31508
31508
|
// src/dates.ts
|
|
31509
31509
|
var WEEKDAY_NAMES = [
|
|
@@ -31878,12 +31878,11 @@ Hint: ${err.hint}`);
|
|
|
31878
31878
|
}
|
|
31879
31879
|
|
|
31880
31880
|
// src/tools/plans.ts
|
|
31881
|
-
function
|
|
31882
|
-
|
|
31883
|
-
|
|
31884
|
-
|
|
31885
|
-
|
|
31886
|
-
return fallback === void 0 ? void 0 : fallback.TransportationId ?? null;
|
|
31881
|
+
function proofsMatch(a, b) {
|
|
31882
|
+
return a.transportationId === b.transportationId && (a.note ?? "").trim() === (b.note ?? "").trim();
|
|
31883
|
+
}
|
|
31884
|
+
function expectedPlanState(requested) {
|
|
31885
|
+
return requested.transportationId === null ? { transportationId: null, note: null } : requested;
|
|
31887
31886
|
}
|
|
31888
31887
|
async function resolveTransportation(client2, schoolId, transportationId) {
|
|
31889
31888
|
const options = await client2.getTransportations(schoolId);
|
|
@@ -31951,24 +31950,30 @@ function registerPlanTools(server, client2) {
|
|
|
31951
31950
|
const gate = previewUnlessConfirmed(confirm, action, "PUT", "UpdatePlans", { Plans: plans });
|
|
31952
31951
|
if (gate) return gate;
|
|
31953
31952
|
await client2.updatePlans(plans);
|
|
31953
|
+
const expected = expectedPlanState({
|
|
31954
|
+
transportationId: transportation_id,
|
|
31955
|
+
note: plans[0]?.Note ?? null
|
|
31956
|
+
});
|
|
31954
31957
|
const verification = await Promise.all(
|
|
31955
31958
|
dates.map(async (date5) => {
|
|
31956
31959
|
const after = await client2.getPlanEdit(date5, student_id);
|
|
31957
|
-
const
|
|
31958
|
-
|
|
31960
|
+
const actual = {
|
|
31961
|
+
transportationId: after.TransportationId ?? null,
|
|
31962
|
+
note: after.Note ?? null
|
|
31963
|
+
};
|
|
31959
31964
|
return {
|
|
31960
31965
|
date: date5,
|
|
31961
31966
|
weekday: weekdayOf(date5),
|
|
31962
|
-
transportationId: actual,
|
|
31967
|
+
transportationId: actual.transportationId,
|
|
31963
31968
|
transportation: after.TransportationName ?? null,
|
|
31964
|
-
note:
|
|
31969
|
+
note: actual.note,
|
|
31965
31970
|
earlyDismissalTime: after.EarlyDismissalTime ?? null,
|
|
31966
31971
|
locked: after.IsLocked ?? false,
|
|
31967
|
-
verified:
|
|
31972
|
+
verified: proofsMatch(actual, expected)
|
|
31968
31973
|
};
|
|
31969
31974
|
})
|
|
31970
31975
|
);
|
|
31971
|
-
const failed = verification.filter((v) => v.verified
|
|
31976
|
+
const failed = verification.filter((v) => !v.verified);
|
|
31972
31977
|
return textResult({
|
|
31973
31978
|
action,
|
|
31974
31979
|
applied: verification,
|
|
@@ -32093,10 +32098,14 @@ function registerDefaultPlanTools(server, client2) {
|
|
|
32093
32098
|
if (gate) return gate;
|
|
32094
32099
|
await client2.updateStudent(payload);
|
|
32095
32100
|
const after = await client2.getStudent(student_id);
|
|
32096
|
-
const
|
|
32097
|
-
const unchanged = [...
|
|
32101
|
+
const sentNote = payload.DefaultPlans?.find((p) => p.DayId === dayIds[0])?.Note ?? null;
|
|
32102
|
+
const unchanged = [...new Set(dayIds)].filter((dayId) => {
|
|
32098
32103
|
const plan = (after.DefaultPlans ?? []).find((p) => p.DayId === dayId);
|
|
32099
|
-
|
|
32104
|
+
if (plan === void 0) return true;
|
|
32105
|
+
return !proofsMatch(
|
|
32106
|
+
{ transportationId: plan.TransportationId ?? null, note: plan.Note ?? null },
|
|
32107
|
+
{ transportationId: transportation.TransportationId, note: sentNote }
|
|
32108
|
+
);
|
|
32100
32109
|
});
|
|
32101
32110
|
return textResult({
|
|
32102
32111
|
action,
|
package/dist/lib.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export { PickUpPatrolClient, type ClientOptions } from './client.js';
|
|
7
7
|
export { PickUpPatrolAuth, describeResponseStatus, collectCookieHeader, BASE_URL, BASE_PATH, type AuthOptions, type PupSession, type FetchLike, } from './auth.js';
|
|
8
8
|
export { buildPlanUpdates, applyDefaultPlans, clearDefaultPlans, assertTransportationAllowed, normalizeNote, normalizeEarlyDismissal, normalizeCarNumber, DEFAULT_PLAN_LABEL, type PlanInput, type DefaultPlanInput, } from './plans.js';
|
|
9
|
-
export {
|
|
9
|
+
export { expectedPlanState, proofsMatch, resolveTransportation, type PlanProof, } from './tools/plans.js';
|
|
10
|
+
export { dayIdToName, nameToDayId, dateToDayId, weekdayOf, WEEKDAY_NAMES, type WeekdayName } from './dates.js';
|
|
10
11
|
export { VERSION } from './version.js';
|
|
11
12
|
export type * from './types.js';
|
package/dist/lib.js
CHANGED
|
@@ -6,5 +6,6 @@
|
|
|
6
6
|
export { PickUpPatrolClient } from './client.js';
|
|
7
7
|
export { PickUpPatrolAuth, describeResponseStatus, collectCookieHeader, BASE_URL, BASE_PATH, } from './auth.js';
|
|
8
8
|
export { buildPlanUpdates, applyDefaultPlans, clearDefaultPlans, assertTransportationAllowed, normalizeNote, normalizeEarlyDismissal, normalizeCarNumber, DEFAULT_PLAN_LABEL, } from './plans.js';
|
|
9
|
-
export {
|
|
9
|
+
export { expectedPlanState, proofsMatch, resolveTransportation, } from './tools/plans.js';
|
|
10
|
+
export { dayIdToName, nameToDayId, dateToDayId, weekdayOf, WEEKDAY_NAMES } from './dates.js';
|
|
10
11
|
export { VERSION } from './version.js';
|
package/dist/tools/defaults.js
CHANGED
|
@@ -3,7 +3,7 @@ import { McpToolError, textResult } from '@chrischall/mcp-utils';
|
|
|
3
3
|
import { applyDefaultPlans, clearDefaultPlans } from '../plans.js';
|
|
4
4
|
import { dayIdToName, nameToDayId } from '../dates.js';
|
|
5
5
|
import { summarizeDefaultPlans } from './account.js';
|
|
6
|
-
import { resolveTransportation } from './plans.js';
|
|
6
|
+
import { proofsMatch, resolveTransportation } from './plans.js';
|
|
7
7
|
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
8
8
|
import { withHints } from './_errors.js';
|
|
9
9
|
/**
|
|
@@ -125,13 +125,19 @@ export function registerDefaultPlanTools(server, client) {
|
|
|
125
125
|
if (gate)
|
|
126
126
|
return gate;
|
|
127
127
|
await client.updateStudent(payload);
|
|
128
|
-
// Re-read and check the weekdays we changed actually hold the new
|
|
129
|
-
//
|
|
128
|
+
// Re-read and check the weekdays we changed actually hold the new option
|
|
129
|
+
// AND the new note. The note alone is a normal edit here — every option at
|
|
130
|
+
// this school requires one — so comparing the id by itself would report
|
|
131
|
+
// success without observing the change. DefaultsModifiedDate stays out of
|
|
132
|
+
// the comparison entirely: it advances by itself, which would make every
|
|
133
|
+
// write look successful.
|
|
130
134
|
const after = await client.getStudent(student_id);
|
|
131
|
-
const
|
|
132
|
-
const unchanged = [...
|
|
135
|
+
const sentNote = payload.DefaultPlans?.find((p) => p.DayId === dayIds[0])?.Note ?? null;
|
|
136
|
+
const unchanged = [...new Set(dayIds)].filter((dayId) => {
|
|
133
137
|
const plan = (after.DefaultPlans ?? []).find((p) => p.DayId === dayId);
|
|
134
|
-
|
|
138
|
+
if (plan === undefined)
|
|
139
|
+
return true;
|
|
140
|
+
return !proofsMatch({ transportationId: plan.TransportationId ?? null, note: plan.Note ?? null }, { transportationId: transportation.TransportationId, note: sentNote });
|
|
135
141
|
});
|
|
136
142
|
return textResult({
|
|
137
143
|
action,
|
package/dist/tools/plans.d.ts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { PickUpPatrolClient } from '../client.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { Transportation } from '../types.js';
|
|
4
|
+
/** The fields whose change proves a plan write actually landed. */
|
|
5
|
+
export interface PlanProof {
|
|
6
|
+
transportationId: number | null;
|
|
7
|
+
note: string | null;
|
|
8
|
+
}
|
|
9
|
+
/** Compare two proofs, ignoring whitespace the service may normalise off a note. */
|
|
10
|
+
export declare function proofsMatch(a: PlanProof, b: PlanProof): boolean;
|
|
4
11
|
/**
|
|
5
|
-
*
|
|
12
|
+
* What `GetPlanEdit` should report once a write has landed.
|
|
6
13
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
14
|
+
* Deliberately more than the transportation id: every dismissal option at the
|
|
15
|
+
* schools seen so far requires a note, so changing only the note is an ordinary
|
|
16
|
+
* edit — and an id-only comparison would report success without ever observing
|
|
17
|
+
* it. Same false-green as diffing a field that cannot move.
|
|
18
|
+
*
|
|
19
|
+
* A revert expects an EMPTY slot, not the weekday default. `GetPlanEdit`
|
|
20
|
+
* reports the date's override, not the effective plan: verified live on a
|
|
21
|
+
* Friday where the student had a Friday default and the date still read back
|
|
22
|
+
* `TransportationId: null`. Expecting the default here would report every
|
|
23
|
+
* successful revert as a failure.
|
|
11
24
|
*/
|
|
12
|
-
export declare function
|
|
25
|
+
export declare function expectedPlanState(requested: PlanProof): PlanProof;
|
|
13
26
|
/** Resolve a transportation id against the school's list, or fail with the options. */
|
|
14
27
|
export declare function resolveTransportation(client: PickUpPatrolClient, schoolId: number, transportationId: number): Promise<Transportation>;
|
|
15
28
|
export declare function registerPlanTools(server: McpServer, client: PickUpPatrolClient): void;
|
package/dist/tools/plans.js
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { McpToolError, textResult } from '@chrischall/mcp-utils';
|
|
3
3
|
import { buildPlanUpdates } from '../plans.js';
|
|
4
|
-
import {
|
|
4
|
+
import { weekdayOf } from '../dates.js';
|
|
5
5
|
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
6
6
|
import { withHints } from './_errors.js';
|
|
7
|
+
/** Compare two proofs, ignoring whitespace the service may normalise off a note. */
|
|
8
|
+
export function proofsMatch(a, b) {
|
|
9
|
+
return a.transportationId === b.transportationId && (a.note ?? '').trim() === (b.note ?? '').trim();
|
|
10
|
+
}
|
|
7
11
|
/**
|
|
8
|
-
*
|
|
12
|
+
* What `GetPlanEdit` should report once a write has landed.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately more than the transportation id: every dismissal option at the
|
|
15
|
+
* schools seen so far requires a note, so changing only the note is an ordinary
|
|
16
|
+
* edit — and an id-only comparison would report success without ever observing
|
|
17
|
+
* it. Same false-green as diffing a field that cannot move.
|
|
9
18
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
19
|
+
* A revert expects an EMPTY slot, not the weekday default. `GetPlanEdit`
|
|
20
|
+
* reports the date's override, not the effective plan: verified live on a
|
|
21
|
+
* Friday where the student had a Friday default and the date still read back
|
|
22
|
+
* `TransportationId: null`. Expecting the default here would report every
|
|
23
|
+
* successful revert as a failure.
|
|
14
24
|
*/
|
|
15
|
-
export function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (dayId === null)
|
|
20
|
-
return undefined;
|
|
21
|
-
const fallback = (student.DefaultPlans ?? []).find((plan) => plan.DayId === dayId);
|
|
22
|
-
return fallback === undefined ? undefined : (fallback.TransportationId ?? null);
|
|
25
|
+
export function expectedPlanState(requested) {
|
|
26
|
+
return requested.transportationId === null
|
|
27
|
+
? { transportationId: null, note: null }
|
|
28
|
+
: requested;
|
|
23
29
|
}
|
|
24
30
|
/** Resolve a transportation id against the school's list, or fail with the options. */
|
|
25
31
|
export async function resolveTransportation(client, schoolId, transportationId) {
|
|
@@ -102,22 +108,30 @@ export function registerPlanTools(server, client) {
|
|
|
102
108
|
// compare the one field that proves it. ModifiedDate is deliberately not
|
|
103
109
|
// compared: it advances on its own, which would make every write look
|
|
104
110
|
// successful.
|
|
111
|
+
// One expectation for the whole call: every date in a single UpdatePlans
|
|
112
|
+
// gets the same option and note, so this does not vary per date.
|
|
113
|
+
const expected = expectedPlanState({
|
|
114
|
+
transportationId: transportation_id,
|
|
115
|
+
note: plans[0]?.Note ?? null,
|
|
116
|
+
});
|
|
105
117
|
const verification = await Promise.all(dates.map(async (date) => {
|
|
106
118
|
const after = await client.getPlanEdit(date, student_id);
|
|
107
|
-
const
|
|
108
|
-
|
|
119
|
+
const actual = {
|
|
120
|
+
transportationId: after.TransportationId ?? null,
|
|
121
|
+
note: after.Note ?? null,
|
|
122
|
+
};
|
|
109
123
|
return {
|
|
110
124
|
date,
|
|
111
125
|
weekday: weekdayOf(date),
|
|
112
|
-
transportationId: actual,
|
|
126
|
+
transportationId: actual.transportationId,
|
|
113
127
|
transportation: after.TransportationName ?? null,
|
|
114
|
-
note:
|
|
128
|
+
note: actual.note,
|
|
115
129
|
earlyDismissalTime: after.EarlyDismissalTime ?? null,
|
|
116
130
|
locked: after.IsLocked ?? false,
|
|
117
|
-
verified:
|
|
131
|
+
verified: proofsMatch(actual, expected),
|
|
118
132
|
};
|
|
119
133
|
}));
|
|
120
|
-
const failed = verification.filter((v) => v.verified
|
|
134
|
+
const failed = verification.filter((v) => !v.verified);
|
|
121
135
|
return textResult({
|
|
122
136
|
action,
|
|
123
137
|
applied: verification,
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/pickuppatrol-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.1.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/pickuppatrol-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.1.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|
|
@@ -28,6 +28,8 @@ pup_login() {
|
|
|
28
28
|
-o /tmp/pup_auth.json -w '%{http_code}' >/tmp/pup_code
|
|
29
29
|
[ "$(cat /tmp/pup_code)" = 200 ] || { jq -r '.ResponseStatus.Message' /tmp/pup_auth.json >&2; return 1; }
|
|
30
30
|
export PUP_JAR="$jar"
|
|
31
|
+
# This deployment returns cookies only — BearerToken comes back null. The
|
|
32
|
+
# JWT path exists server-side, so keep a token if one ever appears.
|
|
31
33
|
PUP_TOKEN=$(jq -r '.BearerToken // empty' /tmp/pup_auth.json); export PUP_TOKEN
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -60,6 +62,8 @@ pup GET GetChildren | jq '.[] | select(.StudentId==1050046) | .DefaultPlans
|
|
|
60
62
|
pup GET "GetTransportations?SchoolId=1703" | jq -r '.[] | select(.IsActive)
|
|
61
63
|
| "\(.TransportationId)\t\(.Name)\tnote=\(.IsNoteRequired)\tcar=\(.UseCarNumbers)\tearly=\(.IsEarlyDismissal)"'
|
|
62
64
|
|
|
65
|
+
# GetPlanEdit returns the date's OVERRIDE, not the effective plan: a date with
|
|
66
|
+
# no specific plan reads back null even when a weekday default exists.
|
|
63
67
|
pup GET "GetPlanEdit?PlanDate=2026-08-17&StudentId=1050046" | jq '{TransportationName,Note,IsLocked}'
|
|
64
68
|
pup GET "GetInvalidPlanDates?SchoolId=1703" | jq 'length' # non-school days
|
|
65
69
|
```
|
|
@@ -103,5 +107,8 @@ A 2xx is not proof. The school silently ignores a change past its cutoff time.
|
|
|
103
107
|
pup GET "GetPlanEdit?PlanDate=2026-08-17&StudentId=1050046" | jq '.TransportationId'
|
|
104
108
|
```
|
|
105
109
|
|
|
106
|
-
Compare `TransportationId` —
|
|
107
|
-
and would make every write look successful.
|
|
110
|
+
Compare `TransportationId` **and `Note`** — never `ModifiedDate`, which advances
|
|
111
|
+
on its own and would make every write look successful. The note matters: every
|
|
112
|
+
option seen so far is `IsNoteRequired`, so a note-only edit is ordinary, and an
|
|
113
|
+
id-only comparison would pass without observing it. After *clearing* a date,
|
|
114
|
+
expect `null` — not the weekday default.
|
|
@@ -6,8 +6,9 @@ token value is recorded.
|
|
|
6
6
|
|
|
7
7
|
- Base: `https://app.pickuppatrol.net/api/json/reply/<DtoName>`
|
|
8
8
|
- GET args on the query string; POST/PUT/PATCH args in a JSON body
|
|
9
|
-
- Auth: session cookies from `Authenticate`,
|
|
10
|
-
|
|
9
|
+
- Auth: session cookies from `Authenticate` (`ss-id`, `ss-pid`, `ss-opt`). This
|
|
10
|
+
deployment returns `BearerToken: null`; send `Authorization: Bearer …` too if
|
|
11
|
+
one ever appears
|
|
11
12
|
- Errors: ServiceStack `{"ResponseStatus":{"ErrorCode","Message","Errors":[…]}}`;
|
|
12
13
|
an unauthenticated call to any authorised DTO is a bare `401`
|
|
13
14
|
|
|
@@ -21,7 +22,8 @@ curl -sS -c jar -X POST "$PUP/Authenticate" -H 'Content-Type: application/json'
|
|
|
21
22
|
```
|
|
22
23
|
|
|
23
24
|
Response: `{UserId, SessionId, UserName, DisplayName, BearerToken, RefreshToken,
|
|
24
|
-
Roles, Permissions, ResponseStatus}
|
|
25
|
+
Roles, Permissions, ResponseStatus}` — `BearerToken` and `RefreshToken` come
|
|
26
|
+
back null here; the session is the cookies.
|
|
25
27
|
|
|
26
28
|
Two-factor accounts (`User.OtpTypeId` set) need `POST /CreateOtp {OtpTypeId}`
|
|
27
29
|
then `PUT /VerifyOtp {Otp, RememberMe}`. Token refresh is
|
|
@@ -85,6 +87,11 @@ TransportationName SchoolName BusRouteUrl ValidationErrors EarlyDismissalTime
|
|
|
85
87
|
CarNumber LimitedIds IsNotePrivate
|
|
86
88
|
```
|
|
87
89
|
|
|
90
|
+
Returns the date's **override**, not the effective plan: a date with no specific
|
|
91
|
+
plan reads back `TransportationId: null` even when the student has a weekly
|
|
92
|
+
default for that weekday. Merge with `GetStudent(...).DefaultPlans` yourself to
|
|
93
|
+
know what actually happens on a date.
|
|
94
|
+
|
|
88
95
|
## Write DTOs
|
|
89
96
|
|
|
90
97
|
| DTO | Verb | Body |
|
|
@@ -159,10 +166,16 @@ dropped field is a field cleared.
|
|
|
159
166
|
|
|
160
167
|
## Verifying a write
|
|
161
168
|
|
|
162
|
-
Re-read and compare the
|
|
169
|
+
Re-read and compare the fields that prove the change — the transportation id
|
|
170
|
+
**and the note**. Every option seen so far is `IsNoteRequired`, so a note-only
|
|
171
|
+
edit is an ordinary change and an id-only comparison would pass without ever
|
|
172
|
+
observing it.
|
|
173
|
+
|
|
174
|
+
- one-off plan → `GetPlanEdit(PlanDate, StudentId)` → `.TransportationId`, `.Note`
|
|
175
|
+
- weekly defaults → `GetStudent(StudentId).DefaultPlans[]` → `.TransportationId`, `.Note`
|
|
163
176
|
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
After *clearing* a date, expect `TransportationId: null` — not the weekday
|
|
178
|
+
default, which `GetPlanEdit` does not merge in.
|
|
166
179
|
|
|
167
180
|
Exclude `ModifiedDate` and `DefaultsModifiedDate`: they advance on their own, so
|
|
168
181
|
including them makes every write look successful.
|