added api logic, vitest, minimal testing ui
This commit is contained in:
548
api/openapi.yaml
Normal file
548
api/openapi.yaml
Normal file
@@ -0,0 +1,548 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: SkyMoney API
|
||||
version: 0.1.0
|
||||
description: |
|
||||
Fastify backend for budgeting/allocations.
|
||||
Most endpoints accept an optional `x-user-id` header; when omitted, the server
|
||||
defaults to `demo-user-1`.
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8080
|
||||
|
||||
tags:
|
||||
- name: Health
|
||||
- name: Dashboard
|
||||
- name: Income
|
||||
- name: Transactions
|
||||
- name: VariableCategories
|
||||
- name: FixedPlans
|
||||
|
||||
paths:
|
||||
/health:
|
||||
get:
|
||||
tags: [Health]
|
||||
summary: Liveness check
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/HealthOk' }
|
||||
|
||||
/health/db:
|
||||
get:
|
||||
tags: [Health]
|
||||
summary: DB health + latency
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
responses:
|
||||
'200':
|
||||
description: DB OK
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/DbHealth' }
|
||||
|
||||
/dashboard:
|
||||
get:
|
||||
tags: [Dashboard]
|
||||
summary: Aggregated dashboard data
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
responses:
|
||||
'200':
|
||||
description: Dashboard payload
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/DashboardResponse' }
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
|
||||
/income:
|
||||
post:
|
||||
tags: [Income]
|
||||
summary: Create income event and allocate funds
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/IncomeRequest' }
|
||||
responses:
|
||||
'200':
|
||||
description: Allocation result
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/IncomeAllocationResponse' }
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
|
||||
/income/preview:
|
||||
post:
|
||||
tags: [Income]
|
||||
summary: Preview allocation of a hypothetical income amount
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/IncomeRequest' }
|
||||
responses:
|
||||
'200':
|
||||
description: Preview
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/IncomePreviewResponse' }
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
|
||||
/transactions:
|
||||
get:
|
||||
tags: [Transactions]
|
||||
summary: List transactions with filters and pagination
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
- in: query
|
||||
name: from
|
||||
schema: { type: string, pattern: '^\d{4}-\d{2}-\d{2}$' }
|
||||
description: Inclusive start date (YYYY-MM-DD)
|
||||
- in: query
|
||||
name: to
|
||||
schema: { type: string, pattern: '^\d{4}-\d{2}-\d{2}$' }
|
||||
description: Inclusive end date (YYYY-MM-DD)
|
||||
- in: query
|
||||
name: kind
|
||||
schema: { $ref: '#/components/schemas/TransactionKind' }
|
||||
- in: query
|
||||
name: q
|
||||
schema: { type: string }
|
||||
description: Simple search (currently numeric amount match)
|
||||
- in: query
|
||||
name: page
|
||||
schema: { type: integer, minimum: 1, default: 1 }
|
||||
- in: query
|
||||
name: limit
|
||||
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
|
||||
responses:
|
||||
'200':
|
||||
description: List
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/TransactionList' }
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
post:
|
||||
tags: [Transactions]
|
||||
summary: Create a transaction
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/TransactionCreate' }
|
||||
responses:
|
||||
'200':
|
||||
description: Created
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Transaction' }
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
|
||||
/variable-categories:
|
||||
post:
|
||||
tags: [VariableCategories]
|
||||
summary: Create a variable category (sum of percents must be 100)
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/VariableCategoryCreate' }
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/OkResponse' }
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
|
||||
/variable-categories/{id}:
|
||||
patch:
|
||||
tags: [VariableCategories]
|
||||
summary: Update a variable category (sum of percents must be 100)
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: string }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/VariableCategoryPatch' }
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/OkResponse' }
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
delete:
|
||||
tags: [VariableCategories]
|
||||
summary: Delete a variable category (sum of percents must remain 100)
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/OkResponse' }
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
|
||||
/fixed-plans:
|
||||
post:
|
||||
tags: [FixedPlans]
|
||||
summary: Create a fixed plan
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/FixedPlanCreate' }
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/OkResponse' }
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
|
||||
/fixed-plans/{id}:
|
||||
patch:
|
||||
tags: [FixedPlans]
|
||||
summary: Update a fixed plan (fundedCents cannot exceed totalCents)
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: string }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/FixedPlanPatch' }
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/OkResponse' }
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
delete:
|
||||
tags: [FixedPlans]
|
||||
summary: Delete a fixed plan
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserId'
|
||||
- $ref: '#/components/parameters/RequestId'
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/OkResponse' }
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
|
||||
components:
|
||||
parameters:
|
||||
UserId:
|
||||
in: header
|
||||
name: x-user-id
|
||||
required: false
|
||||
schema: { type: string }
|
||||
description: Override the stubbed user id for the request.
|
||||
RequestId:
|
||||
in: header
|
||||
name: x-request-id
|
||||
required: false
|
||||
schema: { type: string, maxLength: 64 }
|
||||
description: Custom request id (echoed back by server).
|
||||
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Validation or guard failed
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ErrorResponse' }
|
||||
NotFound:
|
||||
description: Resource not found
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ErrorResponse' }
|
||||
InternalError:
|
||||
description: Unexpected server error
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ErrorResponse' }
|
||||
|
||||
schemas:
|
||||
HealthOk:
|
||||
type: object
|
||||
properties:
|
||||
ok: { type: boolean, const: true }
|
||||
required: [ok]
|
||||
|
||||
DbHealth:
|
||||
type: object
|
||||
properties:
|
||||
ok: { type: boolean, const: true }
|
||||
nowISO: { type: string, format: date-time }
|
||||
latencyMs: { type: integer, minimum: 0 }
|
||||
required: [ok, nowISO, latencyMs]
|
||||
|
||||
OkResponse:
|
||||
type: object
|
||||
properties:
|
||||
ok: { type: boolean, const: true }
|
||||
required: [ok]
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
ok: { type: boolean, const: false }
|
||||
code: { type: string }
|
||||
message: { type: string }
|
||||
requestId: { type: string }
|
||||
required: [ok, code, message, requestId]
|
||||
|
||||
VariableCategory:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
userId: { type: string }
|
||||
name: { type: string }
|
||||
percent: { type: integer, minimum: 0, maximum: 100 }
|
||||
isSavings: { type: boolean }
|
||||
priority: { type: integer, minimum: 0 }
|
||||
balanceCents:
|
||||
type: integer
|
||||
description: Current balance; may be omitted or 0 when not loaded.
|
||||
required: [id, userId, name, percent, isSavings, priority]
|
||||
|
||||
FixedPlan:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
userId: { type: string }
|
||||
name: { type: string }
|
||||
totalCents: { type: integer, minimum: 0 }
|
||||
fundedCents: { type: integer, minimum: 0 }
|
||||
priority: { type: integer, minimum: 0 }
|
||||
dueOn: { type: string, format: date-time }
|
||||
cycleStart: { type: string, format: date-time }
|
||||
required: [id, userId, name, priority, dueOn]
|
||||
|
||||
TransactionKind:
|
||||
type: string
|
||||
enum: [variable_spend, fixed_payment]
|
||||
|
||||
Transaction:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
userId: { type: string }
|
||||
kind: { $ref: '#/components/schemas/TransactionKind' }
|
||||
amountCents: { type: integer, minimum: 0 }
|
||||
occurredAt: { type: string, format: date-time }
|
||||
categoryId: { type: string, nullable: true }
|
||||
planId: { type: string, nullable: true }
|
||||
required: [id, userId, kind, amountCents, occurredAt]
|
||||
|
||||
TransactionList:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/Transaction' }
|
||||
page: { type: integer, minimum: 1 }
|
||||
limit: { type: integer, minimum: 1, maximum: 100 }
|
||||
total: { type: integer, minimum: 0 }
|
||||
required: [items, page, limit, total]
|
||||
|
||||
TransactionCreate:
|
||||
type: object
|
||||
properties:
|
||||
kind: { $ref: '#/components/schemas/TransactionKind' }
|
||||
amountCents: { type: integer, minimum: 1 }
|
||||
occurredAtISO: { type: string, format: date-time }
|
||||
categoryId: { type: string, nullable: true }
|
||||
planId: { type: string, nullable: true }
|
||||
required: [kind, amountCents, occurredAtISO]
|
||||
|
||||
DashboardResponse:
|
||||
type: object
|
||||
properties:
|
||||
totals:
|
||||
type: object
|
||||
properties:
|
||||
incomeCents: { type: integer, minimum: 0 }
|
||||
variableBalanceCents: { type: integer, minimum: 0 }
|
||||
fixedRemainingCents: { type: integer, minimum: 0 }
|
||||
required: [incomeCents, variableBalanceCents, fixedRemainingCents]
|
||||
percentTotal: { type: integer, minimum: 0, maximum: 100 }
|
||||
variableCategories:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/VariableCategory' }
|
||||
fixedPlans:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/FixedPlan' }
|
||||
recentTransactions:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/Transaction' }
|
||||
required: [totals, percentTotal, variableCategories, fixedPlans, recentTransactions]
|
||||
|
||||
IncomeRequest:
|
||||
type: object
|
||||
properties:
|
||||
amountCents: { type: integer, minimum: 0 }
|
||||
required: [amountCents]
|
||||
|
||||
AllocationItem:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
name: { type: string }
|
||||
amountCents: { type: integer, minimum: 0 }
|
||||
required: [id, name, amountCents]
|
||||
|
||||
IncomePreviewResponse:
|
||||
type: object
|
||||
properties:
|
||||
fixed:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/AllocationItem' }
|
||||
variable:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/AllocationItem' }
|
||||
unallocatedCents: { type: integer, minimum: 0 }
|
||||
required: [fixed, variable, unallocatedCents]
|
||||
|
||||
IncomeAllocationResponse:
|
||||
type: object
|
||||
description: >
|
||||
Shape returned by allocateIncome. Tests expect:
|
||||
fixedAllocations, variableAllocations, remainingUnallocatedCents.
|
||||
Additional fields may be present.
|
||||
properties:
|
||||
fixedAllocations:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/AllocationItem' }
|
||||
variableAllocations:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/AllocationItem' }
|
||||
remainingUnallocatedCents: { type: integer, minimum: 0 }
|
||||
additionalProperties: true
|
||||
|
||||
VariableCategoryCreate:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
percent: { type: integer, minimum: 0, maximum: 100 }
|
||||
isSavings: { type: boolean }
|
||||
priority: { type: integer, minimum: 0 }
|
||||
required: [name, percent, isSavings, priority]
|
||||
|
||||
VariableCategoryPatch:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
percent: { type: integer, minimum: 0, maximum: 100 }
|
||||
isSavings: { type: boolean }
|
||||
priority: { type: integer, minimum: 0 }
|
||||
additionalProperties: false
|
||||
|
||||
FixedPlanCreate:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
totalCents: { type: integer, minimum: 0 }
|
||||
fundedCents: { type: integer, minimum: 0 }
|
||||
priority: { type: integer, minimum: 0 }
|
||||
dueOn: { type: string, format: date-time }
|
||||
cycleStart: { type: string, format: date-time }
|
||||
required: [name, totalCents, priority, dueOn]
|
||||
|
||||
FixedPlanPatch:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
totalCents: { type: integer, minimum: 0 }
|
||||
fundedCents: { type: integer, minimum: 0 }
|
||||
priority: { type: integer, minimum: 0 }
|
||||
dueOn: { type: string, format: date-time }
|
||||
cycleStart: { type: string, format: date-time }
|
||||
additionalProperties: false
|
||||
Reference in New Issue
Block a user