Skip to content

fix: OpenAPI guardian sweep 2026-09-14 (wallets, coupons, applied coupons, alerts) - #581

Open
lago-claude-ai-agent[bot] wants to merge 3 commits into
mainfrom
openapi-guardian/2026-09-14
Open

lago-claude-ai-agent[bot] wants to merge 3 commits into
mainfrom
openapi-guardian/2026-09-14

Conversation

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

OpenAPI Guardian sweep — 2026-09-14 (slice 6: wallets, coupons, applied_coupons, alerts)

Automated spec sweep vs lago-api and the SDK clients.
A human must review and merge — this agent never merges.
npm run build and npm run test pass on this branch (0 errors, 23 pre-existing array-params-plural warnings, unchanged from main). The regenerated openapi.yaml is committed alongside each src/ change.

Fixed in this PR

Type Count
Typos / definitions 0
Required vs optional 2
Inaccuracies (types, nullability, enums, shapes) 6
Filters / query params / paths 5

Field-level evidence

Required vs optional

  • src/schemas/WalletObject.yaml required: listed balance → entry removed — balance is not a declared property of the schema and V1::WalletSerializer#serialize never emits it; the balance is exposed as balance_cents and credits_balance, both already declared and required. Evidence: app/serializers/v1/wallet_serializer.rb.
  • src/schemas/WalletCreateInput.yaml top level: no requiredrequired: [wallet]WalletsController#customer_params and WalletActions#input_params both call params.require(:wallet), and Api::BaseController maps ActionController::ParameterMissing to a 400 (app/controllers/api/base_controller.rb:16). The sibling WalletUpdateInput already declared it. See [BREAKING-DOC] below.

Missing response fields (API returns them, spec omitted them)

  • src/schemas/AppliedCouponObject.yaml coupon_description: absent → ["string","null"] — emitted by V1::AppliedCouponSerializer as model.coupon.description; nullable because coupons.description is a nullable text column. Evidence: app/serializers/v1/applied_coupon_serializer.rb:11, db/structure.sql.
  • src/schemas/WalletTransactionObject.yaml billing_entity_code: absent → string — emitted by V1::WalletTransactionSerializer#billing_entity_code, falling back to the wallet's billing entity. Non-nullable: customers.billing_entity_id is NOT NULL, so the fallback always resolves. Evidence: app/serializers/v1/wallet_transaction_serializer.rb, db/structure.sql.

Missing request fields (controllers permit them, spec omitted them)

  • src/schemas/WalletCreateInput.yaml transaction_priority: absent → integer (1–50, default 50) — permitted by WalletActions#input_params and applied as the priority of the initial top-up in Wallets::CreateService#schedule_top_up. Range from WalletTransaction's validates :priority, inclusion: {in: 1..50}. Evidence: app/controllers/concerns/wallet_actions.rb:109, app/services/wallets/create_service.rb:127.
  • src/schemas/WalletUpdateInput.yaml paid_top_up_min_amount_cents / paid_top_up_max_amount_cents: absent → ["integer","null"] — permitted by WalletActions#update_params and applied by Wallets::UpdateService (lines 45–46). Both already existed on WalletCreateInput and WalletObject.
  • src/schemas/WalletTransactionCreateInput.yaml priority: absent → integer (1–50, default 50) — permitted by WalletTransactionsController#input_params, defaulted in WalletTransactions::CreateFromParamsService (@priority = params[:priority] || 50).
  • src/schemas/WalletTransactionCreateInput.yaml voided_transaction_id: absent → ["string","null"], format: uuid — permitted by the same input_params and validated by WalletTransactions::ValidateService, which requires a traceable wallet and an inbound transaction that still has a remaining amount.

Filters / query params

  • src/resources/customer_wallets.yaml GET: added currencyApi::V1::Customers::WalletsController#index permits :currency and passes it to WalletsQuery, which filters on balance_currency. The top-level GET /wallets already documented it; the Go client's WalletListInput also carries it.
  • src/resources/wallet_transactions.yaml GET: added metadata[key] — honoured by WalletTransactionsQuery#with_metadata via a jsonb_array_elements lookup. Documented with the same name: metadata[key] shape already used on GET /invoices.
  • src/resources/subscription_alerts.yaml GET: added page, per_pageSubscriptions::AlertsController#index passes pagination: {page:, limit:} and the response already declares a meta object via Alerts.yaml.
  • src/resources/customer_wallet_alerts.yaml GET: added page, per_page — same, via Customers::Wallets::AlertsController#index and WalletAlerts.yaml.

[BREAKING-DOC] flags

  • WalletCreateInput top-level required: [wallet]. The code proves the wrapper is mandatory (params.require(:wallet) → 400), so the spec was wrong. But on paper this is an optional→required tightening on the request body of POST /wallets and POST /customers/{external_customer_id}/wallets, and it is what the compatibility checker reports as BLOCK. No caller that works today breaks — a request without the wallet wrapper already fails with 400 — but generated clients may start rejecting such payloads locally instead of sending them. Flagged so reviewers weigh it consciously.

Contract compatibility impact

Decision: BLOCK — 2 blocking · 0 warning · 23 informational.
BLOCK is advisory and requires explicit human review; this agent never merges or approves.

Both blocking findings are the single WalletCreateInput requiredness fix above, reported once per affected operation:

Severity Operation Location Finding Required action
BLOCK POST /customers/{external_customer_id}/wallets /requestBody/application/json/wallet request property 'wallet' became required Keep the request property optional or require explicit human approval.
BLOCK POST /wallets /requestBody/application/json/wallet request property 'wallet' became required Keep the request property optional or require explicit human approval.

There were no WARN findings. The remaining 23 findings are all INFO (additive request/response properties and added optional query parameters, requiring no caller migration): coupon_description on the four applied-coupon responses; billing_entity_code on the six wallet-transaction responses; transaction_priority, priority, voided_transaction_id and the two paid_top_up_* request properties; and the currency, metadata[key], page and per_page query parameters.

Note that the WalletObject.required fix does not appear in the report at all: balance was never a declared property, so the checker's $ref/allOf resolver had nothing to compare.

Customer exposure: unknown. No authorized read-only usage source was provided for this run, and the checker has no production telemetry, so no claim is made about how many consumers send a bare wallet payload today.

SDK drift (spec is right — needs an sdk-clients-update run)

Resource Client(s) Divergence
wallets (create) Ruby, Python, Go transaction_priority missing from the create payload builders (Customers::Wallets::WhitelistParams#wallet, models/wallet.py::Wallet, WalletInput)
wallets (update) paid_top_up_min/max_amount_cents already present in all three hand-written clients
wallet_transactions (create) Ruby, Python, Go priority and voided_transaction_id missing from WalletTransaction#whitelist_params, models/wallet_transaction.py::WalletTransaction, WalletTransactionInput
wallet_transactions (response) Python, Go billing_entity_code missing from WalletTransactionResponse / WalletTransaction (present on their wallet models)
wallet_transactions (list) Go metadata[key] filter missing from WalletTransactionListInput
applied_coupons (response) Python, Go, Rust coupon_description missing everywhere. Go and Rust also lack coupon_status and coupon_deleted_at; Python also lacks coupon_name — all three already in the spec
coupons (response) Rust description missing from models/coupon.rs::Coupon
alerts (response) Go external_subscription_id, direction, lago_wallet_id and wallet_code missing from Alert; only the deprecated subscription_external_id is modelled
wallets, wallet_transactions, alerts Rust not modelled at all — known partial coverage by design, reported as a gap rather than drift
all JavaScript none. openapi/ is generated at build time from the published spec and is not committed, so the client inherits these fixes on its next CI build

Needs human confirmation (not changed)

  • billing_entity_id on wallet create. WalletActions#input_params permits :billing_entity_id alongside :billing_entity_code, and Wallets::CreateService#billing_entity resolves either one. The spec documents only billing_entity_code, and every other public billing-entity reference in the spec is code-based. Adding a raw internal UUID lookup to the public contract is a product decision, so nothing was changed — please confirm whether billing_entity_id is intended as public API or is an internal/GraphQL-shared param.
  • subscription_external_id on alert responses. V1::UsageMonitoring::AlertSerializer emits both external_subscription_id and subscription_external_id, the latter explicitly commented # DEPRECATED in the code. Neither AlertObject nor WalletAlertObject declares the deprecated alias. Documenting it with deprecated: true (the pattern used for the invoice.add_on_added webhook in fix(webhook): deprecate the stale invoice.add_on_added webhook #577) would be truthful, but advertising a deprecated alias is a maintainer call — left alone.
  • Under-declared required on wallet and alert response schemas. V1::WalletSerializer unconditionally emits billing_entity_code, priority, name, code, purchase_order_number and payment_method, and AlertSerializer unconditionally emits direction and thresholds, yet none of these appear in the corresponding required lists. This is the same class of finding raised for CustomerBaseObject in fix: OpenAPI guardian sweep 2026-08-17 (slice 2 remainder + docs-guardian leads) #566 and still unresolved; not touched here to avoid a second requiredness change in a PR that already carries one.

Also verified clean (no change needed)

  • All 24 slice-6 paths and their HTTP verbs match config/routes/shared_api.rb exactly, including the two destroy_all collection deletes and the wallet_code vs code nesting parameters.
  • Coupon enums (coupon_type, frequency, expiration) and applied-coupon enums (status, frequency) match the model constants; GET /coupons correctly documents no filters, because CouponsController#index passes only pagination to CouponsQuery even though the query class supports status and a search term.
  • alert_type enums are correct as split: 5 subscription types on the subscription alert schemas, 4 wallet types on the wallet alert schemas, all 9 on TriggeredAlertObject — matching Alert::SUBSCRIPTION_TYPES / WALLET_TYPES and the per-alertable rejection in CreateAlertService. direction is derived server-side and correctly absent from all alert inputs.
  • subscription_status on the subscription alert endpoints is real (Subscriptions::BaseController#find_subscription), not a phantom filter.
  • Wallet metadata endpoints: MetadataObject is ["object","null"], so the {"metadata": null} body returned after a delete validates.

Triaged from the docs guardian

Today's docs sweep (getlago/lago-doc#674) reported no suspected spec issues. The five leads from lago-doc#663 and #669 were re-verified as already landed on main: the five wallet/payment-receipt webhooks and the TriggeredAlertObject enum from #576, and the invoice.add_on_added deprecation from #577. The remaining credit_note.refund_failure naming inconsistency is internal to lago-api (config/webhook_event_types.yml vs the wire webhook_type) and is not a spec issue.

Feedback from previous guardian PRs

The four review items on #578 (approved 2026-09-09) were all resolved as "keep as is" and are recorded as standing conventions in the private skills repo; no spec change was required, and per that guidance the resend_email endpoint family is left to its dedicated cross-surface task rather than folded into this sweep.

Process feedback for the retro

  • The Slack bot token in this runner only grants chat:write. conversations.history and conversations.replies both fail with missing_scope, so this run could not read thread replies on previous #documentation posts and may have missed Slack-delivered feedback. Announcing still works. Adding channels:history (or groups:history) would close the loop.

Deferred to next run

  • Nothing deferred for capacity reasons: 10 src/ files changed, well under the ~40-file cap.

lago-claude-ai-agent Bot and others added 3 commits September 14, 2026 08:08
WalletObject listed `balance` as a required property, but no such
property is declared in the schema and V1::WalletSerializer never emits
it. The wallet balance is exposed as `balance_cents` (and
`credits_balance`), both of which are already declared and required.

Evidence: app/serializers/v1/wallet_serializer.rb

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

Four list endpoints honoured filters that the spec did not document:

- GET /customers/{external_customer_id}/wallets accepts `currency`
  (Api::V1::Customers::WalletsController#index permits it and passes it
  to WalletsQuery, which filters on balance_currency). The top-level
  /wallets already documented it.
- GET /wallets/{lago_id}/wallet_transactions accepts `metadata[key]`
  (WalletTransactionsQuery#with_metadata). Documented with the same
  shape already used on GET /invoices.
- GET /subscriptions/{external_id}/alerts and
  GET /customers/{external_customer_id}/wallets/{wallet_code}/alerts are
  both paginated and already return a `meta` object, but `page` and
  `per_page` were undocumented.

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

Response fields the API returns but the spec omitted:

- AppliedCouponObject.coupon_description — emitted by
  V1::AppliedCouponSerializer from the parent coupon; nullable because
  coupons.description is a nullable text column.
- WalletTransactionObject.billing_entity_code — emitted by
  V1::WalletTransactionSerializer, falling back to the wallet's billing
  entity. Non-nullable: customers.billing_entity_id is NOT NULL, so the
  fallback always resolves.

Request fields the controllers permit but the spec omitted:

- WalletCreateInput.transaction_priority — permitted by WalletActions
  #input_params and applied as the priority of the initial top-up
  transaction (Wallets::CreateService#schedule_top_up). Range 1-50,
  default 50, per WalletTransaction's inclusion validation.
- WalletUpdateInput.paid_top_up_min_amount_cents /
  paid_top_up_max_amount_cents — permitted by WalletActions
  #update_params and applied by Wallets::UpdateService.
- WalletTransactionCreateInput.priority — permitted by
  WalletTransactionsController#input_params, defaulting to 50.
- WalletTransactionCreateInput.voided_transaction_id — permitted and
  validated by WalletTransactions::ValidateService, which requires a
  traceable wallet and an inbound transaction with a remaining amount.

Also adds the missing top-level `required: [wallet]` to
WalletCreateInput: the controller calls params.require(:wallet), and the
sibling WalletUpdateInput already declared it.

Co-Authored-By: Claude Opus 5 (1M context) <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.

0 participants