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:
soky srm
2025-12-17 15:00:01 +01:00
committed by GitHub
parent 9d54719007
commit 7be799fac7
12 changed files with 924 additions and 16 deletions

139
docs/api/categories.md Normal file
View 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`.

View File

@@ -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