feat(fees): add platform fee definitions, assessment, and reversal#550
feat(fees): add platform fee definitions, assessment, and reversal#550pengying wants to merge 1 commit into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
✱ Stainless preview builds for gridThis PR will update the cli csharp go kotlin openapi php python ruby typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ grid-openapi studio · code · diff
✅ grid-ruby studio · code · diff
✅ grid-kotlin studio · code · diff
✅ grid-python studio · code · diff
✅ grid-csharp studio · code · diff
✅ grid-typescript studio · code · diff
✅ grid-php studio · code · diff
✅ grid-go studio · code · diff
✅ grid-cli studio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
Greptile SummaryThis PR introduces a Fees API for platforms to define reusable fee templates (CRUD on
Confidence Score: 3/5The 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
|
| 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
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
| 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 |
There was a problem hiding this 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).
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.| 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. |
There was a problem hiding this 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.
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.| 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 |
There was a problem hiding this 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.
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.| 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. |
There was a problem hiding this 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.
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.| 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 |
There was a problem hiding this 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.
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.| requestBody: | ||
| required: true | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: ../../components/schemas/fees/FeeAssessmentRequest.yaml | ||
| responses: | ||
| '201': |
There was a problem hiding this 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.
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!

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
POST/feescreateFeeGET/feeslistFees(filter bytype, paginated)GET/fees/{feeId}getFeeByIdPATCH/fees/{feeId}updateFeeDELETE/fees/{feeId}deleteFeeA
Feehas atype(TRANSFER_OUT/TRANSFER_IN), adescription, and afixedFeeRange({ currency, min, max }in minor units) and/or avariableFeeRange({ min, max }in basis points). At least one range is required.Assess —
POST /fee-assessmentsAssesses a fee against an account, choosing the
fixedAmount(minor units) and/orvariableFeeRate(basis points) — validated against the fee's ranges (400if out of range). Returns201+ the resulting transaction (TransactionOneOf).{ "feeId": "Fee:...", "accountId": "InternalAccount:...", "fixedAmount": 250, "variableFeeRate": 100 }Reverse —
POST /fee-reversalsReverses all or part of an assessed fee, identified by its transaction. Optional
amountfor partial reversal (omit to reverse the full fee), optionalreason. Returns201+ the reversing transaction.{ "transactionId": "Transaction:...", "amount": 100, "reason": "Goodwill credit" }Notes / out of scope
FEEtransaction type in this PR — fees reuseOUTGOING/INCOMING).🤖 Generated with Claude Code