Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,84 @@ For development with external networking:
% make docker-console
```

## Period-key conventions

Every input and output on `/calculate` is keyed by a period string. Two shapes
are supported:

- **Year key** — `"2026"`. Treated as the value for the entire year.
- **Month key** — `"2026-01"`. Treated as the value for that single month.

Each PolicyEngine variable has a fixed `definition_period` (year or month).
Annual variables like `employment_income` and `state_name` are defined for
the year; monthly variables like `snap_earned_income`, `snap_gross_income`,
and `rent` are defined for the month.

### Recommended pattern: stay consistent within a request

Pick one cadence per request and use it everywhere:

| You want | Send inputs as | Request outputs as |
| ---------------- | ------------------ | ---------------------- |
| Annual totals | `{"2026": V}` | `{"2026": null}` |
| A specific month | `{"2026-01": V}` | `{"2026-01": null}` |

If you only think in yearly amounts, use year keys for everything — including
monthly variables. For numeric inputs, the API treats the year value as the
annual total and distributes it as `V/12` across the 12 months before the
engine runs; the engine returns the annual sum on the way back. Booleans,
strings, and enums are broadcast unchanged across months.

If you need per-month variation, key both the input and the output to the
same month.

### Sending both annual and monthly inputs for the same variable

You can pin specific months while letting the year value cover the rest.
For numeric MONTH-defined variables, the year value is treated as the
**annual total**: explicit monthly values consume part of the budget, and
the remainder splits evenly across the unset months. This matches the
hosted v1 API and OpenFisca's `set_input_divide_by_period`.

```jsonc
// Annual $1200 with June pinned to $600. Remaining $600 splits across
// the other 11 months as raw float ≈ $54.55/mo.
"snap_earned_income": {"2026": 1200, "2026-06": 600}
```

For boolean / string / enum MONTH-defined variables, explicit monthly values
override the year-broadcast for that month while the year value applies to
the rest:

```jsonc
// "SUA all year except LUA in June".
"snap_utility_allowance_type": {"2026": "SUA", "2026-06": "LUA"}
```

The API only rejects with a 400 when **every month** of the year is
explicit AND those monthlies don't sum to the annual total — that's an
"Inconsistent input" the engine can't reconcile. Partial monthly
overrides (any number from 0 to 11 explicit months) are accepted; the
remainder is distributed across the unset months even when it's negative
(matching v1 / OpenFisca exactly). Output-request `null` slots don't
count as inputs, so `{"2026": 1200, "2026-06": null}` keeps both: the
year expands as usual and the engine returns June's value.

### What goes wrong when you mix shapes

Sending a single-month input (`{"2026-01": V}`) on a monthly variable but
requesting an annual output (`{"2026": null}`) is the most common pitfall.
The other 11 months default to 0 in the engine, so the annual sum looks like
a year of benefits even though only January was actually specified. The API
returns a `warnings` array in the response when it detects this combination
so you can correct the request before relying on the number.

### What the API echoes back

Output keys are echoed back exactly as you sent them. Input keys are
preserved unchanged; the year-to-month split happens internally and never
shows up in the response.

## Development rules

1. Every endpoint should return a JSON object with at least a "status" and "message" field.
Expand Down
1 change: 1 addition & 0 deletions changelog.d/normalize-annual-keys-on-month-vars.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Match the hosted v1 API when partners send a YEAR-period key on a MONTH-defined variable. Numeric values are treated as annual totals: any explicit monthly inputs are subtracted, and the remainder splits evenly across the unset months as a raw float (matching policyengine-core's `set_input_divide_by_period`, which has had this behavior since 2015). Boolean / string / enum values broadcast unchanged across the unset months while explicit monthly values override. The API rejects with a 400 ("Inconsistent input") only when every month of the year is explicit AND those monthlies don't sum to the annual total — partial monthly overrides are silently accepted with the remainder distributed across the unset months, matching v1's exact rule. Negative annual values are accepted (some MONTH-defined numeric variables can legitimately be negative). Output requests echo the partner's original keys. Adds a `warnings` array to the response when partial monthly input is paired with an annual output for the same year and there is no year-key fallback for that variable, so partners see a heads-up that the unset months will read the engine's default (purely additive — v1 has no such warning, the numeric output is unchanged).
1 change: 1 addition & 0 deletions changelog.d/period-shape-warnings.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a `warnings` field to `/calculate` responses when the request mixes single-month input on a MONTH-defined variable with an annual output request. The other 11 months default to 0 in the engine, silently inflating annual sums; the warning explains the issue and points partners to a fix (send a yearly key, or set all 12 monthly keys).
1 change: 1 addition & 0 deletions changelog.d/reject-invalid-period-keys.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reject `/calculate` requests with malformed period keys (e.g. `"not-a-period"`, `"2026/13"`) with a 400 ("Invalid period key") that names the offending key, variable, and entity. Previously these were passed through to the engine and silently ignored, leaving partners with a confusing zero result. Pydantic doesn't validate period-key strings, so this is the first checkpoint that catches them.
Loading
Loading