mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
Add categories endpoint in API (#460)
* Add categories endpoint in API * FIX eager load parent and subcategories associations * FIX update specs to match * Add rswag spec * FIX openapi spec * FIX final warns
This commit is contained in:
139
docs/api/categories.md
Normal file
139
docs/api/categories.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Categories API Documentation
|
||||
|
||||
The Categories API allows external applications to retrieve financial categories within Sure. Categories are used to classify transactions and can be organized in a hierarchical structure with parent categories and subcategories. The OpenAPI description is generated directly from executable request specs, ensuring it always reflects the behaviour of the running Rails application.
|
||||
|
||||
## Generated OpenAPI specification
|
||||
|
||||
- The source of truth for the documentation lives in [`spec/requests/api/v1/categories_spec.rb`](../../spec/requests/api/v1/categories_spec.rb). These specs authenticate against the Rails stack, exercise every category endpoint, and capture real response shapes.
|
||||
- Regenerate the OpenAPI document with:
|
||||
|
||||
```sh
|
||||
SWAGGER_DRY_RUN=0 bundle exec rspec spec/requests --format Rswag::Specs::SwaggerFormatter
|
||||
```
|
||||
|
||||
The task compiles the request specs and writes the result to [`docs/api/openapi.yaml`](openapi.yaml).
|
||||
|
||||
- Run just the documentation specs with:
|
||||
|
||||
```sh
|
||||
bundle exec rspec spec/requests/api/v1/categories_spec.rb
|
||||
```
|
||||
|
||||
## Authentication requirements
|
||||
|
||||
All category endpoints require an OAuth2 access token or API key that grants the `read` scope.
|
||||
|
||||
## Available endpoints
|
||||
|
||||
| Endpoint | Scope | Description |
|
||||
| --- | --- | --- |
|
||||
| `GET /api/v1/categories` | `read` | List categories with filtering and pagination. |
|
||||
| `GET /api/v1/categories/{id}` | `read` | Retrieve a single category with full details. |
|
||||
|
||||
Refer to the generated [`openapi.yaml`](openapi.yaml) for request/response schemas, reusable components (pagination, errors), and security definitions.
|
||||
|
||||
## Filtering options
|
||||
|
||||
The `GET /api/v1/categories` endpoint supports the following query parameters for filtering:
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `page` | integer | Page number (default: 1) |
|
||||
| `per_page` | integer | Items per page (default: 25, max: 100) |
|
||||
| `classification` | string | Filter by classification: `income` or `expense` |
|
||||
| `roots_only` | boolean | Return only root categories (categories without a parent) |
|
||||
| `parent_id` | uuid | Filter subcategories by parent category ID |
|
||||
|
||||
## Category object
|
||||
|
||||
A category response includes:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Food & Drink",
|
||||
"classification": "expense",
|
||||
"color": "#f97316",
|
||||
"icon": "utensils",
|
||||
"parent": null,
|
||||
"subcategories_count": 2,
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"updated_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Category hierarchy
|
||||
|
||||
Categories support a two-level hierarchy:
|
||||
|
||||
- **Root categories** (parent categories) have `parent: null` and may have subcategories
|
||||
- **Subcategories** have a `parent` object containing the parent's `id` and `name`
|
||||
|
||||
Example subcategory response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Restaurants",
|
||||
"classification": "expense",
|
||||
"color": "#f97316",
|
||||
"icon": "utensils",
|
||||
"parent": {
|
||||
"id": "uuid",
|
||||
"name": "Food & Drink"
|
||||
},
|
||||
"subcategories_count": 0,
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"updated_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Classification types
|
||||
|
||||
Categories are classified into two types:
|
||||
|
||||
| Classification | Description |
|
||||
| --- | --- |
|
||||
| `income` | Categories for income transactions (salary, investments, etc.) |
|
||||
| `expense` | Categories for expense transactions (food, utilities, etc.) |
|
||||
|
||||
Subcategories inherit the classification of their parent category.
|
||||
|
||||
## Filtering examples
|
||||
|
||||
### Get all expense categories
|
||||
|
||||
```
|
||||
GET /api/v1/categories?classification=expense
|
||||
```
|
||||
|
||||
### Get only root categories (no subcategories)
|
||||
|
||||
```
|
||||
GET /api/v1/categories?roots_only=true
|
||||
```
|
||||
|
||||
### Get subcategories of a specific parent
|
||||
|
||||
```
|
||||
GET /api/v1/categories?parent_id=<parent-category-uuid>
|
||||
```
|
||||
|
||||
### Combine filters with pagination
|
||||
|
||||
```
|
||||
GET /api/v1/categories?classification=expense&roots_only=true&page=1&per_page=10
|
||||
```
|
||||
|
||||
## Error responses
|
||||
|
||||
Errors conform to the shared `ErrorResponse` schema in the OpenAPI document:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "error_code",
|
||||
"message": "Human readable error message"
|
||||
}
|
||||
```
|
||||
|
||||
Common error codes include `unauthorized`, `not_found`, and `internal_server_error`.
|
||||
@@ -242,6 +242,67 @@ components:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
CategoryParent:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
name:
|
||||
type: string
|
||||
CategoryDetail:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- classification
|
||||
- color
|
||||
- icon
|
||||
- subcategories_count
|
||||
- created_at
|
||||
- updated_at
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
name:
|
||||
type: string
|
||||
classification:
|
||||
type: string
|
||||
enum:
|
||||
- income
|
||||
- expense
|
||||
color:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
parent:
|
||||
"$ref": "#/components/schemas/CategoryParent"
|
||||
nullable: true
|
||||
subcategories_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
CategoryCollection:
|
||||
type: object
|
||||
required:
|
||||
- categories
|
||||
- pagination
|
||||
properties:
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/components/schemas/CategoryDetail"
|
||||
pagination:
|
||||
"$ref": "#/components/schemas/Pagination"
|
||||
Merchant:
|
||||
type: object
|
||||
required:
|
||||
@@ -356,6 +417,94 @@ components:
|
||||
message:
|
||||
type: string
|
||||
paths:
|
||||
"/api/v1/categories":
|
||||
get:
|
||||
summary: List categories
|
||||
tags:
|
||||
- Categories
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: Authorization
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Bearer token with read scope
|
||||
- name: page
|
||||
in: query
|
||||
required: false
|
||||
description: 'Page number (default: 1)'
|
||||
schema:
|
||||
type: integer
|
||||
- name: per_page
|
||||
in: query
|
||||
required: false
|
||||
description: 'Items per page (default: 25, max: 100)'
|
||||
schema:
|
||||
type: integer
|
||||
- name: classification
|
||||
in: query
|
||||
required: false
|
||||
description: Filter by classification (income or expense)
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- income
|
||||
- expense
|
||||
- name: roots_only
|
||||
in: query
|
||||
required: false
|
||||
description: Return only root categories (no parent)
|
||||
schema:
|
||||
type: boolean
|
||||
- name: parent_id
|
||||
in: query
|
||||
required: false
|
||||
description: Filter by parent category ID
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: categories filtered by parent
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/CategoryCollection"
|
||||
"/api/v1/categories/{id}":
|
||||
parameters:
|
||||
- name: Authorization
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Bearer token with read scope
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: Category ID
|
||||
schema:
|
||||
type: string
|
||||
get:
|
||||
summary: Retrieve a category
|
||||
tags:
|
||||
- Categories
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: subcategory retrieved with parent
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/CategoryDetail"
|
||||
'404':
|
||||
description: category not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
"/api/v1/chats":
|
||||
get:
|
||||
summary: List chats
|
||||
@@ -420,13 +569,12 @@ paths:
|
||||
example: Monthly budget review
|
||||
message:
|
||||
type: string
|
||||
description: Initial message in the chat
|
||||
description: Optional initial message in the chat
|
||||
model:
|
||||
type: string
|
||||
description: Optional OpenAI model identifier
|
||||
required:
|
||||
- title
|
||||
- message
|
||||
required: true
|
||||
"/api/v1/chats/{id}":
|
||||
parameters:
|
||||
@@ -646,18 +794,18 @@ paths:
|
||||
type: string
|
||||
- name: start_date
|
||||
in: query
|
||||
format: date
|
||||
required: false
|
||||
description: Filter transactions from this date
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- name: end_date
|
||||
in: query
|
||||
format: date
|
||||
required: false
|
||||
description: Filter transactions until this date
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- name: min_amount
|
||||
in: query
|
||||
required: false
|
||||
@@ -672,19 +820,51 @@ paths:
|
||||
type: number
|
||||
- name: type
|
||||
in: query
|
||||
enum:
|
||||
- income
|
||||
- expense
|
||||
required: false
|
||||
description: "Filter by transaction type:\n * `income` \n * `expense` \n "
|
||||
description: Filter by transaction type
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- income
|
||||
- expense
|
||||
- name: search
|
||||
in: query
|
||||
required: false
|
||||
description: Search by name, notes, or merchant name
|
||||
schema:
|
||||
type: string
|
||||
- name: account_ids
|
||||
in: query
|
||||
required: false
|
||||
description: Filter by multiple account IDs
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
- name: category_ids
|
||||
in: query
|
||||
required: false
|
||||
description: Filter by multiple category IDs
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
- name: merchant_ids
|
||||
in: query
|
||||
required: false
|
||||
description: Filter by multiple merchant IDs
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
- name: tag_ids
|
||||
in: query
|
||||
required: false
|
||||
description: Filter by tag IDs
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: transactions filtered by date range
|
||||
@@ -741,6 +921,9 @@ paths:
|
||||
name:
|
||||
type: string
|
||||
description: Transaction name/description
|
||||
description:
|
||||
type: string
|
||||
description: Alternative to name field
|
||||
notes:
|
||||
type: string
|
||||
description: Additional notes
|
||||
@@ -846,8 +1029,14 @@ paths:
|
||||
type: number
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
description: Alternative to name field
|
||||
notes:
|
||||
type: string
|
||||
currency:
|
||||
type: string
|
||||
description: Currency code
|
||||
category_id:
|
||||
type: string
|
||||
format: uuid
|
||||
|
||||
Reference in New Issue
Block a user