Skip to content

fix(sdk): compare payment.execution_date bounds as ISO 8601 values - #378

Open
baaakgun4543 wants to merge 1 commit into
google-agentic-commerce:mainfrom
baaakgun4543:fix/execution-date-iso8601
Open

baaakgun4543 wants to merge 1 commit into
google-agentic-commerce:mainfrom
baaakgun4543:fix/execution-date-iso8601

Conversation

@baaakgun4543

Copy link
Copy Markdown

Description

ExecutionDateEvaluator compares execution_date with not_before and not_after as raw strings. The spec example window uses date-times with an offset ("not_before": "2026-03-31T00:00:00Z", "not_after": "2026-04-30T23:59:59Z"), and payment_mandate.execution_date is an ISO 8601 date. Lexical order is not chronological order, so the check fails in both directions.

Executions outside the window are accepted (fail-open). All three cases below use the spec example window:

execution_date Instant (UTC) Why it slips through
2026-04-30T23:59:59-09:00 2026-05-01T08:59:59Z, after not_after - sorts before Z
2026-04-30T23:59:59.5Z half a second after not_after . sorts before Z
2026-03-31T08:00:00+09:00 2026-03-30T23:00Z, before not_before the local time sorts after T00:00:00Z

Valid executions are rejected (fail-closed).

  • A date-only execution on the first day of the spec's own example window: 2026-03-31 sorts before 2026-03-31T00:00:00Z.
  • 2026-05-01T08:00:00+09:00, which is 2026-04-30T23:00Z and inside the window.

Change

  • _parse_iso8601 parses YYYY-MM-DD as a date and anything else with datetime.fromisoformat, which supports Z and fractional seconds on Python ≥ 3.11, the project minimum. Naive date-times are treated as UTC.
  • _compare_iso8601 compares instants. If either side is a calendar date, both sides are compared as calendar dates, each taken in its own offset.
  • Invalid (non-ISO-8601) values produce a violation, so the check fails closed instead of being compared lexically.

The existing date-only tests (within_window, before_window, after_window, missing_passes) are unchanged and pass.

Tests

New tests in constraints_tests.py:

  • test_payment_execution_date_outside_window_is_rejected, parametrized over the three fail-open cases above.
  • test_payment_execution_date_on_datetime_window_start, the spec example window.
  • test_payment_execution_date_offset_compared_as_instant.
  • test_payment_execution_date_offset_after_window, a regression guard that passes before and after.
  • test_payment_execution_date_invalid_fails_closed.

6 of the 7 new cases fail on main.

Full suite in a clean environment with the pinned dependencies (Python 3.12):

Passed Failed
main 186 2
This change 193 2

The 2 failures are the kb_sd_jwt_intermediate_tests.py aud/nonce mismatch tests. They fail identically on main and are unrelated. The change adds no ruff check findings beyond those already on main.

If the payment.budget fix lands first, the only conflict is the adjacent datetime and decimal import lines.

Open question (not changed here)

When execution_date is absent, the schema says this "indicates immediate execution". The evaluator currently skips the window check, and test_payment_execution_date_missing_passes pins that behaviour, even for a window entirely in the past. This PR leaves that alone; see #376.

Related to #376

  • Follow the CONTRIBUTING Guide.
  • Make your Pull Request title in the https://www.conventionalcommits.org/ specification.
  • Ensure the tests and linter pass (see the note above on the 2 failures already present on main).
  • Appropriate docs were updated (if necessary): no doc change.

ExecutionDateEvaluator compared raw strings. Executions outside the window were accepted when they sorted inside it (a negative UTC offset or fractional seconds after not_after, a positive offset before not_before), and valid ones were rejected (a date-only execution on the first day of the spec's example window). Parse ISO 8601 dates and date-times, compare instants (day granularity when either side is a date), and report invalid values instead of comparing them lexically.
@baaakgun4543

Copy link
Copy Markdown
Author

Note on the failing spellcheck check: every flagged word is already present in the touched files on main. They are PISP, Pisp, pisp, Pisps, otherpisp (the PISP identifiers) and SECP (from SECP256R1). This PR adds none of them. The workflow checks every word in a changed file, and these terms are missing from .cspell/custom-words.txt, so any change to constraints.py or its tests trips it.

#379 adds those terms to the dictionary. Once it is merged, re-running spellcheck here should pass.

@baaakgun4543

Copy link
Copy Markdown
Author

Related: #318 changes the same function, ExecutionDateEvaluator.evaluate, to apply the window to immediate payments. It still compares the bounds as strings, though. With #318 alone, the three fail-open cases in this PR remain: a negative offset or fractional seconds after not_after, and a positive offset before not_before.

The two changes are complementary and will conflict textually. Whichever lands first, I'll rebase this one onto it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant