Skip to content

fix(orders): tighten shipping address, line, and item schema constraints#121

Open
leofischer wants to merge 7 commits into
release/v2.3.0from
fix/shipping-address-validation-schema
Open

fix(orders): tighten shipping address, line, and item schema constraints#121
leofischer wants to merge 7 commits into
release/v2.3.0from
fix/shipping-address-validation-schema

Conversation

@leofischer

@leofischer leofischer commented Jun 29, 2026

Copy link
Copy Markdown
Member

Summary

Aligns the OpenAPI schemas for order shipping address, shipping line, line item, checkout, card/BNPL payment methods, and metadata with the validations actually enforced by payments-api's sanitizers at v2.3.0. Every constraint was verified against payments-api source, then adversarially re-reviewed by four independent passes that re-derived each bound from code; their confirmed findings are incorporated.

Targets release/v2.3.0.

Changes

Shipping (original scope)

  • Shared schemas/customers/shipping_contact_address.yml; create requires street1/postal_code/country (update does not); MX 5-digit postal-code rule; min/max lengths per the strict StringValidator bounds (max = less_than − 1 = 249; min = greater_than + 1). postal_code keeps 250 (its only cap is the inclusive model-level zip rule). City documents the enforced two-consecutive-letters pattern.

Checkout / order (validated with team rulings)

  • exclude_card_networks enum → [visa_master_card, amex] (exact-membership validator; visa/mastercard are rejected). Responses return snake_case — an earlier CamelCase-echo claim was refuted in adversarial review (Checkout.to_snake_case underscores values on every read) and removed.
  • three_ds_mode → adds accepted value not_strict; null defers to company-level 3DS configuration (it does not disable 3DS); not_strict is behaviorally identical to strict in payments-api (attempt acceptance is company-config, not mode). Create-only wording kept for checkouts (update cleaner permits nothing) and dropped for orders (permitted+persisted on update, validated on create).
  • Order/line-item/tax/shipping metadata: values documented as scalar string(≤249)/integer/number/boolean via anyOf (adversarial review caught that oneOf rejects integers, which match both integer and number). Worded as "not supported" rather than "rejected": per team direction payments-api keeps its current partial per-key enforcement, so the docs state the contract without over-promising server-side rejection.
  • maxProperties: 100 retained/extended as the documented contract. Known gap: payments-api does not enforce it (characterization specs pinning this landed in conekta/payments-api#6466); closing it is a future product decision.

Payment methods

  • BNPL: cancel_url is optional by design (spec-pinned in payments-api); can_not_expire removed (silently stripped by the cleaner); expires_at added (unix seconds; defaults to one month). Note: its datatype is currently unvalidated server-side (non-Integer 500s) — documented contract is integer.
  • Card: two-word cardholder name rule (letters only) + maxLength: 249; contract_id exposed (exactly 10 chars, create-only, stored as bank_contract_id, echoed in charge responses since v2.1.0; carries a do-not-put-sensitive-data caveat).
  • Fiscal entity address: residential removed — stripped by the cleaner and not storable on FiscalEntityAddress (the field exists only on ShippingAddress).

Verification

  • make merge rebuilds cleanly; bundler deduplicates the metadata value schema into a single shared component.
  • Adversarial review: 4 independent passes over the full diff, every constraint re-derived from payments-api source with file:line proof. Findings fixed: oneOfanyOf, CamelCase echo claim removed, order three_ds_mode create-only wording dropped, null semantics corrected, not_strict description made honest, city pattern documented.

Related

  • conekta/payments-api#6466 — characterization specs pinning the documented-but-unenforced gaps (metadata cap, per-key nested checks, BNPL types). Specs only; no behavior change.

🤖 Generated with Claude Code

Align the OpenAPI schemas with the validations enforced by payments-api's
v200 param sanitizers, which were previously under-documented.

- Extract the shipping-contact address into a shared
  schemas/customers/shipping_contact_address.yml and reference it from the
  create and update shipping-contact schemas, removing the duplicated inline
  address blocks.
- Mark street1, postal_code, and country as required on the create path
  (matching ShippingContacts::Validator#validate_on_create); the update path
  keeps them optional.
- Document the MX 5-digit postal code rule and add field descriptions.
- Add maxLength: 250 to address fields, shipping line carrier/tracking_number/
  method, and line-item name/brand/sku and tag items.
- Regenerate _build/api.yaml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@atlantis-conekta

Copy link
Copy Markdown
Error: This repo is not allowlisted for Atlantis.

leofischer and others added 5 commits June 29, 2026 13:18
Align the OpenAPI schemas with the validations enforced by payments-api's
v200 param sanitizers, which were previously under-documented.

- Extract the shipping-contact address into a shared
  schemas/customers/shipping_contact_address.yml and reference it from the
  create and update shipping-contact schemas, removing the duplicated inline
  address blocks.
- Mark street1, postal_code, and country as required on the create path
  (matching ShippingContacts::Validator#validate_on_create); the update path
  keeps them optional.
- Document the MX 5-digit postal code rule and add field descriptions.
- Add min/max length constraints matching StringValidator (strict bounds:
  less_than/greater_than). String fields cap at maxLength 249 (the sanitizer
  rejects length >= 250); minLength derives from greater_than + 1. postal_code
  keeps maxLength 250, its only cap being the model-level zip validation.
  Applies to address fields, shipping line carrier/tracking_number/method, and
  line-item name/description/brand/sku/tags (create and update).
- Regenerate _build/api.yaml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ations

Applies verified corrections from the payments-api validator audit:

- exclude_card_networks: real accepted vocabulary is [visa_master_card,
  amex] (exact-membership options_array validator); visa/mastercard as
  separate values are rejected. Responses echo stored values in CamelCase.
- three_ds_mode: add the accepted value not_strict (shared constant
  ALLOWED_TYPES_FOR_3DS_BY_API allows strict/not_strict/smart); note the
  field is create-only and nullable.
- metadata (orders, products, shipping/tax lines): values must be scalar
  string(<=249)/integer/number/boolean per nested_json_subtypes; replaces
  the too-loose additionalProperties:true and too-narrow {type: string}.
- bnpl: cancel_url is optional by design (spec-pinned in payments-api);
  remove can_not_expire (not in the cleaner allowlist, silently stripped);
  add expires_at (the real expiry control).
- card: document the enforced two-word cardholder name rule and maxLength;
  expose contract_id (10-char bank contract reference, create-only).
- fiscal entity address: remove residential (stripped by cleaner and not
  storable on FiscalEntityAddress).
- Rebuild _build/api.yaml.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…data schemas

Four skeptic reviewers re-derived every changed constraint from
payments-api source. Corrections from confirmed findings:

- metadata additionalProperties: oneOf -> anyOf. type integer and type
  number both match an integer value, so strict oneOf validators reject
  integers the API accepts.
- exclude_card_networks: remove the CamelCase echo claim and the
  CamelCase enum values. The mapper camelizes only the internal checkout
  service payload; Checkout.to_snake_case underscores values on every
  read (checkout.rb:418-420), so API responses return snake_case.
- order three_ds_mode: drop "accepted on creation only" (the orders
  cleaner permits it on update via permitted_params_shared; it is only
  validated on create). Checkout keeps create-only wording (its update
  cleaner permits nothing).
- three_ds_mode null semantics: null defers to the company-level 3DS
  configuration; it does not disable 3DS.
- not_strict description: behaviorally identical to strict in
  payments-api; attempt acceptance is governed by company configuration.
- shipping address city: document the enforced two-consecutive-letters
  pattern.
- card name / bnpl expires_at descriptions enriched per review.
- Rebuild _build/api.yaml.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cement

payments-api will not change validation behavior for existing customers,
so the partial per-key enforcement in NestedTypeValidator stays. State
that nested objects/arrays in metadata are "not supported" rather than
"rejected" — the contract is accurate without promising server-side
rejection in every position.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five reviewers (two removal deep-dives + three full-quadrant passes)
re-derived every changed line from payments-api validators, views and
specs. Both questioned removals were confirmed correct:

- can_not_expire (bnpl): never existed in the bnpl sanitizer (git -S
  empty since creation); all 26 code references are checkout/cash flows.
  Also cleaned the two stale bnpl request examples still sending it.
- residential (fiscal address): unstorable (no Mongoid field, not in
  attr_accessible), stripped or blocked on every route including nested
  customer create, never rendered.

Corrections from confirmed findings:

- order three_ds_mode: explicit null is rejected (type: String check on
  create, empirically reproduced) -> type string, enum without null;
  omitting the field means no 3DS via the API (company-level 3DS applies
  only via Checkout or antifraud), replacing the wrong deferral claim.
- checkout three_ds_mode: stays nullable (response schema) but explains
  null-in-responses vs omit-on-creation.
- not_strict summaries: 3DS2 "flow" not "challenge" (challenge vs
  frictionless is issuer-side).
- card name description: the regex allows , . ' - and accented letters;
  no longer implies punctuated names are rejected.
- tax description minLength 2 -> 3 (greater_than: 2 is strict).
- between_streets: document enforced maxLength 249 and two-letter
  pattern (city_validations).
- klarna_bnpl intentionally NOT added to product_type despite being
  parser-accepted: not publicly available.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@leofischer

Copy link
Copy Markdown
Member Author

Second adversarial review — consolidated verdict

Five independent reviewers (two removal deep-dives + three full-quadrant passes) re-derived every changed line of this PR from payments-api validators, views, and specs. One reviewer empirically executed the TypeValidator to settle null handling.

Both questioned removals confirmed correct:

  • can_not_expire (BNPL): never existed in the BNPL sanitizer — git log -S returns empty since the sanitizer's creation; all 26 codebase references belong to checkout/cash/OXXO/SPEI flows where the field remains documented. The two stale BNPL request examples still sending it are now cleaned.
  • residential (fiscal address): no Mongoid field on FiscalEntityAddress/Address, not in attr_accessible, stripped or blocked on every route (including nested fiscal_entities on customer create, where the cleaner passes the subtree but mass-assignment strips it), never rendered by any view, never touched by a spec. The residential: true values in order examples sit under shipping_contact.address, where the field is real and stays documented.

Errors found in the previous revision, now fixed:

  • order three_ds_mode: explicit null is rejected on create (type: String check — empirically reproduced), so the nullable type/enum was wrong; and nil means no 3DS via the API, not company-config deferral (that fallback only applies via Checkout or antifraud). Checkout keeps nullable (it is also the response schema) with omit-vs-null semantics explained.
  • not_strict summaries now say 3DS2 flow (challenge vs frictionless is issuer-side).
  • card name description no longer implies , . ' - and accented letters are rejected (the regex allows them).
  • tax description.minLength corrected 2 → 3 (strict greater_than: 2).
  • between_streets now documents its enforced maxLength 249 + two-consecutive-letters pattern.
  • klarna_bnpl deliberately NOT added to product_type despite being parser-accepted — not publicly available.

Pre-existing spec-modeling issues flagged for follow-up (not touched):

  • charge_request.yml payment_method oneOf includes a catch-all payment_method_general_request branch — a valid BNPL request matches two branches, which strict oneOf validators reject. Should be anyOf or discriminated.
  • checkout_request.yml never documented can_not_expire even though checkout sanitizers accept it (docs gap in the opposite direction).
  • payments-api accepts order three_ds_mode on update unvalidated (arbitrary strings persist and silently degrade) — documented in conekta/payments-api#6466's gap table.

🤖 Generated with Claude Code

Comment thread schemas/checkouts/checkout.yml
…h CI

CI bundles with redocly/cli 2.34.0 and diffs against the committed
_build/api.yaml, but the Makefile still used
openapi-generator-cli v7.23.0, producing an incompatible serialization —
so any bundle committed via `make merge` failed the up-to-date check.
Regenerate the bundle with the CI command and point `make merge` at the
same redocly invocation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants