-
Notifications
You must be signed in to change notification settings - Fork 7
feat(fees): add platform fee definitions, assessment, and reversal #550
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| type: object | ||
| description: >- | ||
| A platform-defined fee that can be assessed against a customer's account. A fee | ||
| defines an allowed fixed amount range and/or variable rate range; the specific | ||
| amounts are chosen when the fee is assessed. | ||
| required: | ||
| - id | ||
| - type | ||
| - description | ||
| - createdAt | ||
| - updatedAt | ||
| properties: | ||
| id: | ||
| type: string | ||
| description: Unique identifier for the fee. | ||
| example: Fee:019542f5-b3e7-1d02-0000-000000000010 | ||
| 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. Present if the fee has | ||
| a fixed component. | ||
| variableFeeRange: | ||
| $ref: ./VariableFeeRange.yaml | ||
| description: >- | ||
| The allowed range for the variable portion of the fee. Present if the fee | ||
| has a variable component. | ||
| createdAt: | ||
| type: string | ||
| format: date-time | ||
| description: When the fee was created. | ||
| example: '2026-06-04T14:25:18Z' | ||
| updatedAt: | ||
| type: string | ||
| format: date-time | ||
| description: When the fee was last updated. | ||
| example: '2026-06-04T14:30:00Z' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| type: object | ||
| description: >- | ||
| Request to assess a defined fee against an account. The chosen fixed amount | ||
| and variable fee rate must fall within the ranges defined on the fee. | ||
| required: | ||
| - feeId | ||
| - accountId | ||
| properties: | ||
| feeId: | ||
| type: string | ||
| description: The identifier of the fee to assess. | ||
| example: Fee:019542f5-b3e7-1d02-0000-000000000010 | ||
| accountId: | ||
| type: string | ||
| description: The identifier of the account to charge the fee against. | ||
| example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 | ||
| 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 | ||
|
Comment on lines
+17
to
+34
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Both Prompt To Fix With AIThis 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. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,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. | ||
|
Comment on lines
+1
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The description states "At least one of Prompt To Fix With AIThis 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. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| type: object | ||
| required: | ||
| - data | ||
| - hasMore | ||
| properties: | ||
| data: | ||
| type: array | ||
| description: List of fees matching the filter criteria. | ||
| items: | ||
| $ref: ./Fee.yaml | ||
| hasMore: | ||
| type: boolean | ||
| description: Indicates if more results are available beyond this page. | ||
| nextCursor: | ||
| type: string | ||
| description: >- | ||
| Cursor to retrieve the next page of results (only present if hasMore is | ||
| true). | ||
| totalCount: | ||
| type: integer | ||
| description: >- | ||
| Total number of fees matching the criteria (excluding pagination). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| type: object | ||
| description: >- | ||
| Request to reverse all or part of a previously assessed fee. | ||
| required: | ||
| - transactionId | ||
| properties: | ||
| transactionId: | ||
| type: string | ||
| description: The identifier of the fee assessment transaction to reverse. | ||
| example: Transaction:019542f5-b3e7-1d02-0000-000000000004 | ||
| 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 | ||
|
Comment on lines
+11
to
+19
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The prose says "provide an amount less than the original fee" for a partial reversal, but Prompt To Fix With AIThis 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. |
||
| reason: | ||
| type: string | ||
| description: Human-readable reason for the reversal. | ||
| example: Goodwill credit issued to customer | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| type: string | ||
| enum: | ||
| - TRANSFER_OUT | ||
| - TRANSFER_IN | ||
| description: >- | ||
| The type of operation a fee applies to. TRANSFER_OUT applies to transfer-out | ||
| operations; TRANSFER_IN applies to transfer-in operations. | ||
| example: TRANSFER_OUT |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,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. | ||
|
Comment on lines
+1
to
+15
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis 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. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| type: object | ||
| description: >- | ||
| The allowed range for the fixed portion of a fee, in the smallest unit of the | ||
| currency (e.g. cents). When assessing the fee, the chosen fixed amount must | ||
| fall within this range (inclusive). | ||
| required: | ||
| - currency | ||
| - min | ||
| - max | ||
| properties: | ||
| currency: | ||
| $ref: ../common/Currency.yaml | ||
| description: The currency of the fixed fee range. | ||
| min: | ||
| type: integer | ||
| format: int64 | ||
| minimum: 0 | ||
| description: >- | ||
| Minimum fixed fee, in the smallest unit of the currency (e.g. cents). | ||
| example: 100 | ||
| max: | ||
| type: integer | ||
| format: int64 | ||
| minimum: 0 | ||
| description: >- | ||
| Maximum fixed fee, in the smallest unit of the currency (e.g. cents). | ||
| example: 500 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| type: object | ||
| description: >- | ||
| The allowed range for the variable (percentage) portion of a fee, expressed in | ||
| basis points (1 basis point = 0.01%). When assessing the fee, the chosen | ||
| variable fee rate must fall within this range (inclusive). | ||
| required: | ||
| - min | ||
| - max | ||
| properties: | ||
| min: | ||
| type: integer | ||
| minimum: 0 | ||
| maximum: 10000 | ||
| description: Minimum variable fee rate, in basis points (e.g. 50 = 0.50%). | ||
| example: 50 | ||
| max: | ||
| type: integer | ||
| minimum: 0 | ||
| maximum: 10000 | ||
| description: Maximum variable fee rate, in basis points (e.g. 200 = 2.00%). | ||
| example: 200 |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| post: | ||
| summary: Assess a fee | ||
| description: > | ||
| Assess a defined fee against a customer's account, charging the chosen fixed | ||
| amount and/or variable fee rate (which must fall within the ranges defined on | ||
| the fee). Returns the resulting transaction. | ||
| operationId: assessFee | ||
| tags: | ||
| - Fees | ||
| security: | ||
| - BasicAuth: [] | ||
| parameters: | ||
| - name: Idempotency-Key | ||
| in: header | ||
| required: false | ||
| description: > | ||
| A unique identifier for the request. If the same key is sent multiple | ||
| times, the server will return the same response as the first request. | ||
| schema: | ||
| type: string | ||
| example: 550e8400-e29b-41d4-a716-446655440000 | ||
| requestBody: | ||
| required: true | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: ../../components/schemas/fees/FeeAssessmentRequest.yaml | ||
| responses: | ||
| '201': | ||
|
Comment on lines
+22
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis 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! |
||
| description: Fee assessed successfully | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: ../../components/schemas/transactions/TransactionOneOf.yaml | ||
| '400': | ||
| description: Bad request - Invalid parameters or amounts outside the fee's ranges | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: ../../components/schemas/errors/Error400.yaml | ||
| '401': | ||
| description: Unauthorized | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: ../../components/schemas/errors/Error401.yaml | ||
| '404': | ||
| description: Fee or account not found | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: ../../components/schemas/errors/Error404.yaml | ||
| '500': | ||
| description: Internal service error | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: ../../components/schemas/errors/Error500.yaml | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| post: | ||
| summary: Reverse a fee | ||
| description: > | ||
| Reverse all or part of a previously assessed fee, identified by its | ||
| transaction. Provide an amount to reverse part of the fee, or omit it to | ||
| reverse the full assessed fee. Returns the resulting reversing transaction. | ||
| operationId: reverseFee | ||
| tags: | ||
| - Fees | ||
| security: | ||
| - BasicAuth: [] | ||
| parameters: | ||
| - name: Idempotency-Key | ||
| in: header | ||
| required: false | ||
| description: > | ||
| A unique identifier for the request. If the same key is sent multiple | ||
| times, the server will return the same response as the first request. | ||
| schema: | ||
| type: string | ||
| example: 550e8400-e29b-41d4-a716-446655440000 | ||
| requestBody: | ||
| required: true | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: ../../components/schemas/fees/FeeReversalRequest.yaml | ||
| responses: | ||
| '201': | ||
| description: Fee reversed successfully | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: ../../components/schemas/transactions/TransactionOneOf.yaml | ||
| '400': | ||
| description: Bad request - Invalid parameters or reversal amount exceeds the original fee | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: ../../components/schemas/errors/Error400.yaml | ||
| '401': | ||
| description: Unauthorized | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: ../../components/schemas/errors/Error401.yaml | ||
| '404': | ||
| description: Transaction not found | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: ../../components/schemas/errors/Error404.yaml | ||
| '500': | ||
| description: Internal service error | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: ../../components/schemas/errors/Error500.yaml |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
variableFeeRateis expressed in basis points (a percentage), but the request contains nobaseAmountfield — 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 abaseAmountfield 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