Skip to content

feat(fees): add platform fee definitions, assessment, and reversal#550

Open
pengying wants to merge 1 commit into
mainfrom
06-04-feat_platform_fees
Open

feat(fees): add platform fee definitions, assessment, and reversal#550
pengying wants to merge 1 commit into
mainfrom
06-04-feat_platform_fees

Conversation

@pengying
Copy link
Copy Markdown
Contributor

@pengying pengying commented Jun 4, 2026

Summary

Adds a Fees API that lets platforms define the fees they charge their customers and assess or reverse those fees against an account. Modeled on the two-step pattern used by comparable banking platforms (Marqeta Fee + fee transfer, Stripe Application Fees): a reusable fee definition plus separate assess / reverse actions that produce transactions.

Fee definitions — CRUD

Method Path Operation
POST /fees createFee
GET /fees listFees (filter by type, paginated)
GET /fees/{feeId} getFeeById
PATCH /fees/{feeId} updateFee
DELETE /fees/{feeId} deleteFee

A Fee has a type (TRANSFER_OUT / TRANSFER_IN), a description, and a fixedFeeRange ({ currency, min, max } in minor units) and/or a variableFeeRange ({ min, max } in basis points). At least one range is required.

Assess — POST /fee-assessments

Assesses a fee against an account, choosing the fixedAmount (minor units) and/or variableFeeRate (basis points) — validated against the fee's ranges (400 if out of range). Returns 201 + the resulting transaction (TransactionOneOf).

{ "feeId": "Fee:...", "accountId": "InternalAccount:...", "fixedAmount": 250, "variableFeeRate": 100 }

Reverse — POST /fee-reversals

Reverses all or part of an assessed fee, identified by its transaction. Optional amount for partial reversal (omit to reverse the full fee), optional reason. Returns 201 + the reversing transaction.

{ "transactionId": "Transaction:...", "amount": 100, "reason": "Goodwill credit" }

Notes / out of scope

  • Fee assessments and reversals surface as existing transactions (no new FEE transaction type in this PR — fees reuse OUTGOING / INCOMING).
  • Assessments are not their own queryable resource; the resulting transaction is the record.
  • API contract only (no backend implementation).

🤖 Generated with Claude Code

Add a Fees API for platforms to define and assess their own fees against
customer accounts:

- CRUD for Fee definitions (POST/GET /fees, GET/PATCH/DELETE /fees/{feeId}).
  A fee has a type (TRANSFER_OUT / TRANSFER_IN), a description, and a fixed fee
  range (minor units) and/or a variable fee range (basis points).
- POST /fee-assessments assesses a fee against an account, choosing the fixed
  amount and variable rate within the fee's ranges; returns a transaction.
- POST /fee-reversals reverses all or part of an assessed fee by its
  transaction id; returns a reversing transaction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
grid-flow-builder Ignored Ignored Preview Jun 4, 2026 8:15pm

Request Review

Copy link
Copy Markdown
Contributor Author

pengying commented Jun 4, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions github-actions Bot added the breaking-change Introduces a breaking change to the OpenAPI spec label Jun 4, 2026
@pengying pengying marked this pull request as ready for review June 4, 2026 20:15
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

⚠️ Breaking OpenAPI changes detected

This PR introduces breaking changes to openapi.yaml:

API Changelog 2025-10-13 vs. 2025-10-13

API Changes

POST /auth/credentials/{id}/verify

  • ⚠️ added the new required request property oneOf[subschema #1: Email OTP Credential Verify Request]/allOf[#/components/schemas/EmailOtpCredentialVerifyRequestFields]/clientPublicKey
  • ⚠️ added the new required request property oneOf[subschema #1: Email OTP Credential Verify Request]/allOf[#/components/schemas/EmailOtpCredentialVerifyRequestFields]/otp
  • ⚠️ removed the success response with the status 202
  • ⚠️ deleted the header request parameter Grid-Wallet-Signature
  • ⚠️ removed the request property oneOf[subschema #1: Email OTP Credential Verify Request]/allOf[#/components/schemas/EmailOtpCredentialVerifyRequestFields]/encryptedOtpBundle

Detected by oasdiff. This PR will need approval from an API reviewer before merge.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

✱ Stainless preview builds for grid

This PR will update the grid SDKs with the following commit messages.

cli

chore(internal): regenerate SDK with no functional changes

csharp

chore(internal): regenerate SDK with no functional changes

go

chore(internal): regenerate SDK with no functional changes

kotlin

chore(internal): regenerate SDK with no functional changes

openapi

feat(api): add fees resource with CRUD, assessment, and reversal endpoints

php

chore(internal): regenerate SDK with no functional changes

python

chore(internal): regenerate SDK with no functional changes

ruby

chore(internal): regenerate SDK with no functional changes

typescript

chore(internal): regenerate SDK with no functional changes

Edit this comment to update them. They will appear in their respective SDK's changelogs.

grid-openapi studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅

New diagnostics (7 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-assessments`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-reversals`
grid-ruby studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ⏭️ (prev: build ✅) → lint ✅test ✅

New diagnostics (7 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-assessments`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-reversals`
grid-kotlin studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ⏭️ (prev: build ✅) → lint ⏭️ (prev: lint ✅) → test ✅

New diagnostics (7 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-assessments`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-reversals`
grid-python studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ⏭️ (prev: build ✅) → lint ⏭️ (prev: lint ❗) → test ❗

New diagnostics (7 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-assessments`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-reversals`
grid-csharp studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️build ⏭️ (prev: build ❗) → lint ⏭️ (prev: lint ✅) → test ❗

New diagnostics (7 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-assessments`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-reversals`
grid-typescript studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ⏭️ (prev: build ✅) → lint ⏭️ (prev: lint ✅) → test ✅

New diagnostics (7 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-assessments`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-reversals`
grid-php studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅lint ✅test ✅

New diagnostics (7 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-assessments`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-reversals`
grid-go studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ⏭️ (prev: build ✅) → lint ❗test ❗

go get github.com/stainless-sdks/grid-go@395e64c8cb3c06f936d8cfbf172cc1477eec22d8
New diagnostics (7 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-assessments`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-reversals`
grid-cli studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️build ❗lint ❗test ❗

New diagnostics (7 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fees`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /fees/{feeId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-assessments`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /fee-reversals`

This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-06-04 20:20:30 UTC

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 4, 2026

Greptile Summary

This PR introduces a Fees API for platforms to define reusable fee templates (CRUD on /fees) and then assess or reverse those fees against customer accounts via /fee-assessments and /fee-reversals. The design follows the two-step definition + action pattern (similar to Marqeta/Stripe) and is API-contract only with no backend implementation.

  • Fee definitions (POST/GET/PATCH/DELETE /fees): A Fee object carries a type (TRANSFER_OUT/TRANSFER_IN), a human-readable description, and optional fixedFeeRange (currency + min/max in minor units) and/or variableFeeRange (min/max in basis points). The "at least one range required" invariant is documented in prose but not captured by any OpenAPI schema keyword.
  • Assessment (POST /fee-assessments): The platform picks a fixedAmount and/or variableFeeRate within the fee's configured ranges and charges an account; returns the resulting transaction. The variableFeeRate is a basis-point rate but no baseAmount field exists in the request — it is unclear what reference amount the server applies the rate against to derive the actual dollar charge.
  • Reversal (POST /fee-reversals): References an assessed fee's transaction ID for full or partial reversal; partial reversal amount has a minimum: 1 but no schema-level maximum.

Confidence Score: 3/5

The contract has a meaningful ambiguity in how variable fees are computed that needs resolution before client teams implement against it.

The variable fee assessment path lacks a baseAmount field, making it impossible for callers (or the server contract) to deterministically compute the dollar value of a variable-rate charge. Combined with "at least one range" and conditional-required constraints that live only in prose rather than the schema, the contract as written would allow malformed requests through any OpenAPI-aware validation layer. These gaps affect the two most important action endpoints.

openapi/components/schemas/fees/FeeAssessmentRequest.yaml is the highest-priority file — the missing baseAmount and unenforced conditional-required fields need to be resolved before this contract is shared with integrators. FeeCreateRequest.yaml and FeeUpdateRequest.yaml also need schema-level enforcement of the "at least one range" invariant.

Important Files Changed

Filename Overview
openapi/components/schemas/fees/FeeAssessmentRequest.yaml Two issues: missing baseAmount for variable fee computation (rate in bps has no reference amount), and conditionally-required fields (fixedAmount, variableFeeRate) not reflected in the schema's required list.
openapi/components/schemas/fees/FeeCreateRequest.yaml "At least one of fixedFeeRange or variableFeeRange" constraint described in prose but not enforced by any OpenAPI schema keyword (anyOf, required, etc.).
openapi/components/schemas/fees/FeeUpdateRequest.yaml PATCH schema allows both ranges to be removed simultaneously, which would violate the invariant that a fee must have at least one range; no documentation or schema guard prevents this.
openapi/components/schemas/fees/FeeReversalRequest.yaml Clean structure; minor gap: amount lacks a maximum constraint to cap partial reversals at the original fee amount.
openapi/paths/fees/fee_assessments.yaml Endpoint structure is correct with idempotency key and proper 201/400/401/404/500 responses; missing request body examples and the underlying schema gaps (baseAmount, conditional required fields) affect this path.
openapi/paths/fees/fee_reversals.yaml Well-structured reversal endpoint with idempotency key; missing request body examples; inherits amount max-constraint gap from FeeReversalRequest.
openapi/paths/fees/fees.yaml Correct list + create structure with pagination parameters; missing request body examples on POST; inherits the "at least one range" schema enforcement gap from FeeCreateRequest.
openapi/paths/fees/fees_{feeId}.yaml GET/PATCH/DELETE are all consistent with platform patterns; PATCH inherits the "can zero out both ranges" gap from FeeUpdateRequest.
openapi/openapi.yaml Correct additions: new Fees tag and four path refs registered cleanly alongside existing paths.

Sequence Diagram

sequenceDiagram
    participant P as Platform
    participant API as Grid API
    participant Acct as Customer Account

    P->>API: POST /fees (type, description, fixedFeeRange, variableFeeRange)
    API-->>P: "201 Fee {id, type, fixedFeeRange, variableFeeRange, ...}"

    Note over P,API: Optional: PATCH /fees/{feeId} to update ranges

    P->>API: POST /fee-assessments (feeId, accountId, fixedAmount?, variableFeeRate?)
    API->>API: Validate amounts within fee ranges
    API->>Acct: Debit fee amount
    API-->>P: 201 TransactionOneOf (fee assessment transaction)

    Note over P,API: Later — full or partial reversal

    P->>API: POST /fee-reversals (transactionId, amount?, reason?)
    API->>Acct: Credit reversal amount
    API-->>P: 201 TransactionOneOf (reversing transaction)

    P->>API: "DELETE /fees/{feeId}"
    API-->>P: 204 No Content
Loading
Prompt To Fix All With AI
Fix the following 6 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 6
openapi/components/schemas/fees/FeeAssessmentRequest.yaml:26-34
**Missing base amount for variable fee computation**

`variableFeeRate` is expressed in basis points (a percentage), but the request contains no `baseAmount` field — so there is no way for the server to determine the actual dollar value of the variable component. Without knowing the reference amount (e.g., the associated transfer amount or some other notional), `variableFeeRate: 100` (1%) computes to an indeterminate dollar charge. Either a `baseAmount` field should be added to the request, or the API description needs to explicitly document what amount the server applies the rate against (and how callers can predict the resulting charge).

### Issue 2 of 6
openapi/components/schemas/fees/FeeCreateRequest.yaml:1-26
**"At least one" constraint not enforced by the schema**

The description states "At least one of `fixedFeeRange` or `variableFeeRange` must be provided", but neither field appears in the `required` array and there is no `anyOf` / `minProperties` keyword to capture this invariant. An OpenAPI validator (or any code-generated client) will happily accept a `POST /fees` body with only `type` and `description`, bypassing the intended constraint. Adding a schema-level enforcement (e.g., `anyOf: [{required: [fixedFeeRange]}, {required: [variableFeeRange]}]`) aligns the contract with the intended behavior and enables automated validation.

### Issue 3 of 6
openapi/components/schemas/fees/FeeAssessmentRequest.yaml:17-34
**Conditional required fields not captured in schema**

Both `fixedAmount` and `variableFeeRate` are described as "Required if the fee defines a fixedFeeRange/variableFeeRange", but neither appears in the `required` list and there is no `if/then` or `anyOf` construct to encode this conditionality. A request body with only `{feeId, accountId}` is structurally valid per the schema even though the server would reject it. Consider at minimum adding an `anyOf` enforcing that at least one of the two fields must be present, and document the conditional requirement more precisely.

### Issue 4 of 6
openapi/components/schemas/fees/FeeUpdateRequest.yaml:1-15
**PATCH can remove all fee ranges, violating the fee invariant**

`FeeUpdateRequest` allows `fixedFeeRange` and `variableFeeRange` to be independently updated. If a client sends `{"fixedFeeRange": null}` against a fee that only has `fixedFeeRange` (no `variableFeeRange`), the result would be a fee with no ranges — violating the invariant established in `FeeCreateRequest`. Consider documenting the constraint in the `PATCH` description (e.g., "at least one range must remain after the update"), and returning a `400` when the update would leave the fee rangeless.

### Issue 5 of 6
openapi/components/schemas/fees/FeeReversalRequest.yaml:11-19
**`amount` has no maximum constraint in schema**

The prose says "provide an amount less than the original fee" for a partial reversal, but `amount` only specifies `minimum: 1` with no `maximum`. OpenAPI validators will accept arbitrarily large reversal amounts. While the server should enforce this at runtime (and return `400`), adding the constraint to the schema makes the contract self-documenting and enables client-side validation tooling.

### Issue 6 of 6
openapi/paths/fees/fee_assessments.yaml:22-29
**Missing request body examples on action endpoints**

`assessFee` and `reverseFee` have no `examples` block in their `requestBody`, unlike comparable action endpoints in the repo (e.g., `transfer_out.yaml`). Without examples, generated documentation is less helpful and it is harder for integrators to understand valid payloads. Adding at least one example per endpoint (e.g., fixed-only, variable-only, combined) would align with existing patterns. The same applies to `fee_reversals.yaml` and the `POST /fees` create endpoint.

Reviews (1): Last reviewed commit: "feat(fees): add platform fee definitions..." | Re-trigger Greptile

Comment on lines +26 to +34
variableFeeRate:
type: integer
minimum: 0
maximum: 10000
description: >-
The variable fee rate to assess, in basis points (e.g. 100 = 1.00%).
Required if the fee defines a variableFeeRange, and must fall within that
range.
example: 100
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing base amount for variable fee computation

variableFeeRate is expressed in basis points (a percentage), but the request contains no baseAmount field — so there is no way for the server to determine the actual dollar value of the variable component. Without knowing the reference amount (e.g., the associated transfer amount or some other notional), variableFeeRate: 100 (1%) computes to an indeterminate dollar charge. Either a baseAmount field should be added to the request, or the API description needs to explicitly document what amount the server applies the rate against (and how callers can predict the resulting charge).

Prompt To Fix With AI
This is a comment left during a code review.
Path: openapi/components/schemas/fees/FeeAssessmentRequest.yaml
Line: 26-34

Comment:
**Missing base amount for variable fee computation**

`variableFeeRate` is expressed in basis points (a percentage), but the request contains no `baseAmount` field — so there is no way for the server to determine the actual dollar value of the variable component. Without knowing the reference amount (e.g., the associated transfer amount or some other notional), `variableFeeRate: 100` (1%) computes to an indeterminate dollar charge. Either a `baseAmount` field should be added to the request, or the API description needs to explicitly document what amount the server applies the rate against (and how callers can predict the resulting charge).

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1 to +26
type: object
description: >-
Request to create a platform-defined fee. At least one of fixedFeeRange or
variableFeeRange must be provided.
required:
- type
- description
properties:
type:
$ref: ./FeeType.yaml
description:
type: string
description: Human-readable description of the fee.
example: Transfer-out processing fee
fixedFeeRange:
$ref: ./FixedFeeRange.yaml
description: >-
The allowed range for the fixed portion of the fee. Provide if the fee has
a fixed component. At least one of fixedFeeRange or variableFeeRange is
required.
variableFeeRange:
$ref: ./VariableFeeRange.yaml
description: >-
The allowed range for the variable portion of the fee. Provide if the fee
has a variable component. At least one of fixedFeeRange or variableFeeRange
is required.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 "At least one" constraint not enforced by the schema

The description states "At least one of fixedFeeRange or variableFeeRange must be provided", but neither field appears in the required array and there is no anyOf / minProperties keyword to capture this invariant. An OpenAPI validator (or any code-generated client) will happily accept a POST /fees body with only type and description, bypassing the intended constraint. Adding a schema-level enforcement (e.g., anyOf: [{required: [fixedFeeRange]}, {required: [variableFeeRange]}]) aligns the contract with the intended behavior and enables automated validation.

Prompt To Fix With AI
This is a comment left during a code review.
Path: openapi/components/schemas/fees/FeeCreateRequest.yaml
Line: 1-26

Comment:
**"At least one" constraint not enforced by the schema**

The description states "At least one of `fixedFeeRange` or `variableFeeRange` must be provided", but neither field appears in the `required` array and there is no `anyOf` / `minProperties` keyword to capture this invariant. An OpenAPI validator (or any code-generated client) will happily accept a `POST /fees` body with only `type` and `description`, bypassing the intended constraint. Adding a schema-level enforcement (e.g., `anyOf: [{required: [fixedFeeRange]}, {required: [variableFeeRange]}]`) aligns the contract with the intended behavior and enables automated validation.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +17 to +34
fixedAmount:
type: integer
format: int64
minimum: 0
description: >-
The fixed fee amount to assess, in the smallest unit of the currency
(e.g. cents). Required if the fee defines a fixedFeeRange, and must fall
within that range.
example: 250
variableFeeRate:
type: integer
minimum: 0
maximum: 10000
description: >-
The variable fee rate to assess, in basis points (e.g. 100 = 1.00%).
Required if the fee defines a variableFeeRange, and must fall within that
range.
example: 100
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Conditional required fields not captured in schema

Both fixedAmount and variableFeeRate are described as "Required if the fee defines a fixedFeeRange/variableFeeRange", but neither appears in the required list and there is no if/then or anyOf construct to encode this conditionality. A request body with only {feeId, accountId} is structurally valid per the schema even though the server would reject it. Consider at minimum adding an anyOf enforcing that at least one of the two fields must be present, and document the conditional requirement more precisely.

Prompt To Fix With AI
This is a comment left during a code review.
Path: openapi/components/schemas/fees/FeeAssessmentRequest.yaml
Line: 17-34

Comment:
**Conditional required fields not captured in schema**

Both `fixedAmount` and `variableFeeRate` are described as "Required if the fee defines a fixedFeeRange/variableFeeRange", but neither appears in the `required` list and there is no `if/then` or `anyOf` construct to encode this conditionality. A request body with only `{feeId, accountId}` is structurally valid per the schema even though the server would reject it. Consider at minimum adding an `anyOf` enforcing that at least one of the two fields must be present, and document the conditional requirement more precisely.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1 to +15
type: object
description: >-
Request to update a platform-defined fee. Only the provided fields are
updated.
properties:
description:
type: string
description: Human-readable description of the fee.
example: Transfer-out processing fee
fixedFeeRange:
$ref: ./FixedFeeRange.yaml
description: The allowed range for the fixed portion of the fee.
variableFeeRange:
$ref: ./VariableFeeRange.yaml
description: The allowed range for the variable portion of the fee.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 PATCH can remove all fee ranges, violating the fee invariant

FeeUpdateRequest allows fixedFeeRange and variableFeeRange to be independently updated. If a client sends {"fixedFeeRange": null} against a fee that only has fixedFeeRange (no variableFeeRange), the result would be a fee with no ranges — violating the invariant established in FeeCreateRequest. Consider documenting the constraint in the PATCH description (e.g., "at least one range must remain after the update"), and returning a 400 when the update would leave the fee rangeless.

Prompt To Fix With AI
This is a comment left during a code review.
Path: openapi/components/schemas/fees/FeeUpdateRequest.yaml
Line: 1-15

Comment:
**PATCH can remove all fee ranges, violating the fee invariant**

`FeeUpdateRequest` allows `fixedFeeRange` and `variableFeeRange` to be independently updated. If a client sends `{"fixedFeeRange": null}` against a fee that only has `fixedFeeRange` (no `variableFeeRange`), the result would be a fee with no ranges — violating the invariant established in `FeeCreateRequest`. Consider documenting the constraint in the `PATCH` description (e.g., "at least one range must remain after the update"), and returning a `400` when the update would leave the fee rangeless.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +11 to +19
amount:
type: integer
format: int64
minimum: 1
description: >-
The amount to reverse, in the smallest unit of the currency (e.g. cents).
For a partial reversal, provide an amount less than the original fee. If
omitted, the full assessed fee is reversed.
example: 100
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 amount has no maximum constraint in schema

The prose says "provide an amount less than the original fee" for a partial reversal, but amount only specifies minimum: 1 with no maximum. OpenAPI validators will accept arbitrarily large reversal amounts. While the server should enforce this at runtime (and return 400), adding the constraint to the schema makes the contract self-documenting and enables client-side validation tooling.

Prompt To Fix With AI
This is a comment left during a code review.
Path: openapi/components/schemas/fees/FeeReversalRequest.yaml
Line: 11-19

Comment:
**`amount` has no maximum constraint in schema**

The prose says "provide an amount less than the original fee" for a partial reversal, but `amount` only specifies `minimum: 1` with no `maximum`. OpenAPI validators will accept arbitrarily large reversal amounts. While the server should enforce this at runtime (and return `400`), adding the constraint to the schema makes the contract self-documenting and enables client-side validation tooling.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +22 to +29
requestBody:
required: true
content:
application/json:
schema:
$ref: ../../components/schemas/fees/FeeAssessmentRequest.yaml
responses:
'201':
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing request body examples on action endpoints

assessFee and reverseFee have no examples block in their requestBody, unlike comparable action endpoints in the repo (e.g., transfer_out.yaml). Without examples, generated documentation is less helpful and it is harder for integrators to understand valid payloads. Adding at least one example per endpoint (e.g., fixed-only, variable-only, combined) would align with existing patterns. The same applies to fee_reversals.yaml and the POST /fees create endpoint.

Prompt To Fix With AI
This is a comment left during a code review.
Path: openapi/paths/fees/fee_assessments.yaml
Line: 22-29

Comment:
**Missing request body examples on action endpoints**

`assessFee` and `reverseFee` have no `examples` block in their `requestBody`, unlike comparable action endpoints in the repo (e.g., `transfer_out.yaml`). Without examples, generated documentation is less helpful and it is harder for integrators to understand valid payloads. Adding at least one example per endpoint (e.g., fixed-only, variable-only, combined) would align with existing patterns. The same applies to `fee_reversals.yaml` and the `POST /fees` create endpoint.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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

Labels

breaking-change Introduces a breaking change to the OpenAPI spec

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant