mirror of
https://github.com/we-promise/sure.git
synced 2026-09-09 00:24:15 +00:00
* feat(bills): schema and domain core for the bills subsystem First of three chunks carved out of #3083. This one carries the schema and the domain layer: no bills pages, no calendar feed, no assistant tools. Nothing here is reachable from the UI yet, so it changes no user-visible behavior on its own. Schema, in a single migration with a full down: - recurrence_rules, recurring_occurrences, recurring_allocations, recurring_price_changes and recurring_match_rejections - bill columns on recurring_transactions (bill_type, payment_url, autopay, notes, anchor and end conditions, weekend adjustment, dedup scope) - the four data backfills, in their original order Domain layer: - Schedule, the pure date PORO every cadence resolves through, and FrequencyPreset for the labels - OccurrenceGenerator, Matcher, Allocator, PriceChangeDetector, Classifier, DeclaredBill, HistoryBackfiller and PaycheckPlanner - Pipeline, tying detection to generation, plus the nightly job and rake task Existing detection code changed in three places, each a bug this schema exposes: - Cleaner used a flat two-month staleness threshold, which silently retired every quarterly and annual series - SubscriptionAuditGenerator used a flat 45-day overdue threshold, meaningless at both ends of the frequency range - CashFlowWarningGenerator read one projected entry per series, which only equalled the monthly amount because every series was monthly; weekly bills were under-counted fourfold in its 30-day projection The JSON API travels with the model rather than the UI, because the status enum widens here. The API accepts only active and inactive on write; suggested, paused and ended are lifecycle states owned by detection, so the documented enum stays truthful. Uniqueness keys gain dedup_scope alongside amount, never instead of it: a series that is not price-forked carries a blank scope, so amount is what keeps two different prices apart. Suite 7,550 runs, 0 failures. Rubocop and brakeman clean. Eager loading verified, and the migration reverses and re-applies. Includes the first review round: orphan repair matches income and refuses coincidental twins, session imports persist occurrence mappings across chunks, semimonthly anchors canonicalize, classifier keywords match whole words, and the down refuses rather than failing when price-forked rows exist. * Address second review round Bound the cross-currency default allocation by the entry leftover and the occurrence remainder, matching the same-currency path. Let keyword stems carry a suffix again after the word-boundary fix silenced them. Skip an incoherent recurrence rule row instead of rolling back the whole import. Check rollback collisions per restored index so a refusal cannot land after the bills tables are dropped. Replay the closed_at test through a real second import. Preload the orphan repair associations and move the allocator errors to locale keys. * Match index NULL semantics in the rollback collision checks GROUP BY treats NULLs as equal but the restored unique indexes do not: account_id is nullable and indexed, so two accountless rows can never collide under any of them. Excluding NULL accounts keeps the guard from refusing a rollback PostgreSQL can perform. Verified live both ways: accountless duplicates roll back, a real collision still refuses. * Address maintainer review Scope the payable debt-destination subquery to the row and its family instead of scanning every account in the installation. Batch the cash flow generator remaining-amount sums into one grouped query, matching the two sibling sites. Enforce both window bounds in the after_count branch so a future-anchored plan cannot leak past the requested end date. Skip the explicit regeneration when the day column change will fire the model callback anyway. Add the missing locale entry for the allocation currency validation.
1783 lines
76 KiB
Ruby
1783 lines
76 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
RSpec.configure do |config|
|
|
config.openapi_root = Rails.root.join('docs', 'api').to_s
|
|
reset_count_keys = Family::FinancialDataReset::STATUS_COUNT_KEYS.map(&:to_s)
|
|
|
|
config.openapi_specs = {
|
|
'openapi.yaml' => {
|
|
openapi: '3.0.3',
|
|
info: {
|
|
title: 'Sure API',
|
|
version: 'v1',
|
|
description: 'OpenAPI documentation generated from executable request specs.'
|
|
},
|
|
servers: [
|
|
{
|
|
url: 'https://app.sure.am',
|
|
description: 'Production'
|
|
},
|
|
{
|
|
url: 'http://localhost:3000',
|
|
description: 'Local development'
|
|
}
|
|
],
|
|
components: {
|
|
securitySchemes: {
|
|
apiKeyAuth: {
|
|
type: :apiKey,
|
|
name: 'X-Api-Key',
|
|
in: :header,
|
|
description: 'API key for authentication. Generate one from your account settings.'
|
|
}
|
|
},
|
|
schemas: {
|
|
Pagination: {
|
|
type: :object,
|
|
required: %w[page per_page total_count total_pages],
|
|
properties: {
|
|
page: { type: :integer, minimum: 1 },
|
|
per_page: { type: :integer, minimum: 1 },
|
|
total_count: { type: :integer, minimum: 0 },
|
|
total_pages: { type: :integer, minimum: 0 }
|
|
}
|
|
},
|
|
FamilyExportFile: {
|
|
type: :object,
|
|
required: %w[attached],
|
|
properties: {
|
|
attached: { type: :boolean },
|
|
byte_size: { type: :integer, nullable: true, minimum: 0 },
|
|
content_type: { type: :string, nullable: true }
|
|
}
|
|
},
|
|
FamilyExport: {
|
|
type: :object,
|
|
required: %w[id status filename downloadable file created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
status: { type: :string, enum: %w[pending processing completed failed] },
|
|
filename: { type: :string },
|
|
downloadable: { type: :boolean },
|
|
download_path: { type: :string, nullable: true },
|
|
file: { '$ref' => '#/components/schemas/FamilyExportFile' },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
FamilyExportResponse: {
|
|
type: :object,
|
|
required: %w[data],
|
|
properties: {
|
|
data: { '$ref' => '#/components/schemas/FamilyExport' }
|
|
}
|
|
},
|
|
FamilyExportCollection: {
|
|
type: :object,
|
|
required: %w[data meta],
|
|
properties: {
|
|
data: {
|
|
type: :array,
|
|
maxItems: 100,
|
|
items: { '$ref' => '#/components/schemas/FamilyExport' }
|
|
},
|
|
meta: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
ErrorResponse: {
|
|
type: :object,
|
|
required: %w[error],
|
|
properties: {
|
|
error: { type: :string },
|
|
message: { type: :string, nullable: true },
|
|
details: {
|
|
oneOf: [
|
|
{ type: :array, items: { type: :string } },
|
|
{ type: :object }
|
|
],
|
|
nullable: true
|
|
},
|
|
errors: {
|
|
type: :array,
|
|
items: { type: :string },
|
|
nullable: true,
|
|
description: 'Validation error messages (alternative to details used by trades, valuations, etc.)'
|
|
}
|
|
}
|
|
},
|
|
ErrorResponseWithImportId: {
|
|
type: :object,
|
|
required: %w[error import_id],
|
|
properties: {
|
|
error: { type: :string },
|
|
message: { type: :string, nullable: true },
|
|
import_id: {
|
|
type: :string,
|
|
format: :uuid,
|
|
description: 'Import ID preserved for retry or inspection after upload succeeds but publish fails'
|
|
}
|
|
}
|
|
},
|
|
MfaRequiredResponse: {
|
|
type: :object,
|
|
required: %w[error mfa_required],
|
|
properties: {
|
|
error: { type: :string },
|
|
mfa_required: { type: :boolean }
|
|
}
|
|
},
|
|
ToolCall: {
|
|
type: :object,
|
|
required: %w[id function_name function_arguments created_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
function_name: { type: :string },
|
|
function_arguments: { type: :object, additionalProperties: true },
|
|
function_result: { type: :object, additionalProperties: true, nullable: true },
|
|
created_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
Message: {
|
|
type: :object,
|
|
required: %w[id type role content created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
type: { type: :string, enum: %w[user_message assistant_message] },
|
|
role: { type: :string, enum: %w[user assistant] },
|
|
content: { type: :string },
|
|
model: { type: :string, nullable: true },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' },
|
|
tool_calls: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/ToolCall' },
|
|
nullable: true
|
|
}
|
|
}
|
|
},
|
|
MessageResponse: {
|
|
allOf: [
|
|
{ '$ref' => '#/components/schemas/Message' },
|
|
{
|
|
type: :object,
|
|
required: %w[chat_id],
|
|
properties: {
|
|
chat_id: { type: :string, format: :uuid },
|
|
ai_response_status: { type: :string, enum: %w[pending complete failed], nullable: true },
|
|
ai_response_message: { type: :string, nullable: true }
|
|
}
|
|
}
|
|
]
|
|
},
|
|
ChatResource: {
|
|
type: :object,
|
|
required: %w[id title created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
title: { type: :string },
|
|
error: { type: :string, nullable: true },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
ChatSummary: {
|
|
allOf: [
|
|
{ '$ref' => '#/components/schemas/ChatResource' },
|
|
{
|
|
type: :object,
|
|
required: %w[message_count],
|
|
properties: {
|
|
message_count: { type: :integer, minimum: 0 },
|
|
last_message_at: { type: :string, format: :'date-time', nullable: true }
|
|
}
|
|
}
|
|
]
|
|
},
|
|
ChatDetail: {
|
|
allOf: [
|
|
{ '$ref' => '#/components/schemas/ChatResource' },
|
|
{
|
|
type: :object,
|
|
required: %w[messages],
|
|
properties: {
|
|
messages: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/Message' }
|
|
},
|
|
pagination: {
|
|
'$ref' => '#/components/schemas/Pagination',
|
|
nullable: true
|
|
}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
ChatCollection: {
|
|
type: :object,
|
|
required: %w[chats pagination],
|
|
properties: {
|
|
chats: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/ChatSummary' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
Insight: {
|
|
type: :object,
|
|
required: %w[id type title body priority status],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
type: { type: :string },
|
|
title: { type: :string },
|
|
body: { type: :string },
|
|
priority: { type: :string, enum: %w[high medium low] },
|
|
status: { type: :string, enum: %w[active read] },
|
|
generated_at: { type: :string, format: :'date-time', nullable: true }
|
|
}
|
|
},
|
|
InsightCollection: {
|
|
type: :object,
|
|
required: %w[insights],
|
|
properties: {
|
|
insights: { type: :array, items: { '$ref' => '#/components/schemas/Insight' } }
|
|
}
|
|
},
|
|
PushSubscription: {
|
|
type: :object,
|
|
required: %w[id environment platform last_registered_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
environment: { type: :string, enum: %w[sandbox production] },
|
|
platform: { type: :string, enum: %w[ios] },
|
|
last_registered_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
RetryResponse: {
|
|
type: :object,
|
|
required: %w[message message_id],
|
|
properties: {
|
|
message: { type: :string },
|
|
message_id: { type: :string, format: :uuid }
|
|
}
|
|
},
|
|
Account: {
|
|
type: :object,
|
|
required: %w[id name account_type],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string },
|
|
account_type: { type: :string, nullable: true },
|
|
status: { type: :string }
|
|
}
|
|
},
|
|
AccountDetail: {
|
|
type: :object,
|
|
required: %w[id name balance balance_cents cash_balance cash_balance_cents currency classification account_type status created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string },
|
|
balance: { type: :string },
|
|
balance_cents: { type: :integer, description: 'Signed balance in minor currency units' },
|
|
cash_balance: { type: :string },
|
|
cash_balance_cents: { type: :integer, description: 'Signed cash balance in minor currency units' },
|
|
currency: { type: :string },
|
|
classification: { type: :string },
|
|
account_type: { type: :string, nullable: true },
|
|
subtype: { type: :string, nullable: true },
|
|
status: { type: :string, enum: %w[active draft disabled pending_deletion] },
|
|
institution_name: { type: :string, nullable: true },
|
|
institution_domain: { type: :string, nullable: true },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
AccountCollection: {
|
|
type: :object,
|
|
required: %w[accounts pagination],
|
|
properties: {
|
|
accounts: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/AccountDetail' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
FamilySettings: {
|
|
type: :object,
|
|
required: %w[id currency locale date_format month_start_day moniker default_account_sharing custom_enabled_currencies enabled_currencies created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string, nullable: true },
|
|
currency: { type: :string },
|
|
locale: { type: :string },
|
|
date_format: { type: :string },
|
|
country: { type: :string, nullable: true },
|
|
timezone: { type: :string, nullable: true },
|
|
month_start_day: { type: :integer, minimum: 1, maximum: 28 },
|
|
moniker: { type: :string, enum: Family::MONIKERS },
|
|
default_account_sharing: { type: :string, enum: %w[shared private] },
|
|
custom_enabled_currencies: { type: :boolean },
|
|
enabled_currencies: {
|
|
type: :array,
|
|
items: { type: :string }
|
|
},
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
BudgetSummary: {
|
|
type: :object,
|
|
required: %w[id start_date end_date name currency initialized current created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
start_date: { type: :string, format: :date },
|
|
end_date: { type: :string, format: :date },
|
|
name: { type: :string },
|
|
currency: { type: :string },
|
|
initialized: { type: :boolean },
|
|
current: { type: :boolean },
|
|
budgeted_spending: { type: :string, nullable: true },
|
|
budgeted_spending_cents: { type: :integer, nullable: true },
|
|
expected_income: { type: :string, nullable: true },
|
|
expected_income_cents: { type: :integer, nullable: true },
|
|
allocated_spending: { type: :string },
|
|
allocated_spending_cents: { type: :integer },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
Budget: {
|
|
type: :object,
|
|
required: %w[id start_date end_date name currency initialized current created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
start_date: { type: :string, format: :date },
|
|
end_date: { type: :string, format: :date },
|
|
name: { type: :string },
|
|
currency: { type: :string },
|
|
initialized: { type: :boolean },
|
|
current: { type: :boolean },
|
|
budgeted_spending: { type: :string, nullable: true },
|
|
budgeted_spending_cents: { type: :integer, nullable: true },
|
|
expected_income: { type: :string, nullable: true },
|
|
expected_income_cents: { type: :integer, nullable: true },
|
|
allocated_spending: { type: :string },
|
|
allocated_spending_cents: { type: :integer },
|
|
actual_spending: { type: :string },
|
|
actual_spending_cents: { type: :integer },
|
|
actual_income: { type: :string },
|
|
actual_income_cents: { type: :integer },
|
|
available_to_spend: { type: :string },
|
|
available_to_spend_cents: { type: :integer },
|
|
available_to_allocate: { type: :string },
|
|
available_to_allocate_cents: { type: :integer },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
BudgetCollection: {
|
|
type: :object,
|
|
required: %w[budgets pagination],
|
|
properties: {
|
|
budgets: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/BudgetSummary' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
BudgetCategorySummary: {
|
|
type: :object,
|
|
required: %w[id budget_id currency subcategory inherits_parent_budget rollover_enabled category created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
budget_id: { type: :string, format: :uuid },
|
|
currency: { type: :string },
|
|
subcategory: { type: :boolean },
|
|
inherits_parent_budget: { type: :boolean },
|
|
rollover_enabled: { type: :boolean },
|
|
budgeted_spending: { type: :string },
|
|
budgeted_spending_cents: { type: :integer },
|
|
display_budgeted_spending: { type: :string },
|
|
display_budgeted_spending_cents: { type: :integer },
|
|
category: {
|
|
type: :object,
|
|
required: %w[id name color lucide_icon],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string },
|
|
color: { type: :string },
|
|
lucide_icon: { type: :string },
|
|
parent_id: { type: :string, format: :uuid, nullable: true }
|
|
}
|
|
},
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
BudgetCategory: {
|
|
type: :object,
|
|
required: %w[id budget_id currency subcategory inherits_parent_budget rollover_enabled category created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
budget_id: { type: :string, format: :uuid },
|
|
currency: { type: :string },
|
|
subcategory: { type: :boolean },
|
|
inherits_parent_budget: { type: :boolean },
|
|
rollover_enabled: { type: :boolean },
|
|
budgeted_spending: { type: :string },
|
|
budgeted_spending_cents: { type: :integer },
|
|
display_budgeted_spending: { type: :string },
|
|
display_budgeted_spending_cents: { type: :integer },
|
|
actual_spending: { type: :string },
|
|
actual_spending_cents: { type: :integer },
|
|
rolled_over_amount: { type: :string },
|
|
rolled_over_amount_cents: { type: :integer },
|
|
available_to_spend: { type: :string },
|
|
available_to_spend_cents: { type: :integer },
|
|
category: {
|
|
type: :object,
|
|
required: %w[id name color lucide_icon],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string },
|
|
color: { type: :string },
|
|
lucide_icon: { type: :string },
|
|
parent_id: { type: :string, format: :uuid, nullable: true }
|
|
}
|
|
},
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
BudgetCategoryCollection: {
|
|
type: :object,
|
|
required: %w[budget_categories pagination],
|
|
properties: {
|
|
budget_categories: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/BudgetCategorySummary' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
Balance: {
|
|
type: :object,
|
|
required: %w[id date currency flows_factor balance balance_cents start_balance start_balance_cents end_balance end_balance_cents account created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
date: { type: :string, format: :date },
|
|
currency: { type: :string },
|
|
flows_factor: { type: :number, format: :float },
|
|
balance: { type: :string },
|
|
balance_cents: { type: :integer, description: 'Balance in currency minor units' },
|
|
cash_balance: { type: :string, nullable: true },
|
|
cash_balance_cents: { type: :integer, nullable: true, description: 'Cash balance in currency minor units' },
|
|
start_cash_balance: { type: :string },
|
|
start_cash_balance_cents: { type: :integer, description: 'Starting cash balance in currency minor units' },
|
|
start_non_cash_balance: { type: :string },
|
|
start_non_cash_balance_cents: { type: :integer, description: 'Starting non-cash balance in currency minor units' },
|
|
start_balance: { type: :string },
|
|
start_balance_cents: { type: :integer, description: 'Starting total balance in currency minor units' },
|
|
cash_inflows: { type: :string },
|
|
cash_inflows_cents: { type: :integer, description: 'Cash inflows in currency minor units' },
|
|
cash_outflows: { type: :string },
|
|
cash_outflows_cents: { type: :integer, description: 'Cash outflows in currency minor units' },
|
|
non_cash_inflows: { type: :string },
|
|
non_cash_inflows_cents: { type: :integer, description: 'Non-cash inflows in currency minor units' },
|
|
non_cash_outflows: { type: :string },
|
|
non_cash_outflows_cents: { type: :integer, description: 'Non-cash outflows in currency minor units' },
|
|
net_market_flows: { type: :string },
|
|
net_market_flows_cents: { type: :integer, description: 'Net market flows in currency minor units' },
|
|
cash_adjustments: { type: :string },
|
|
cash_adjustments_cents: { type: :integer, description: 'Cash adjustments in currency minor units' },
|
|
non_cash_adjustments: { type: :string },
|
|
non_cash_adjustments_cents: { type: :integer, description: 'Non-cash adjustments in currency minor units' },
|
|
end_cash_balance: { type: :string },
|
|
end_cash_balance_cents: { type: :integer, description: 'Ending cash balance in currency minor units' },
|
|
end_non_cash_balance: { type: :string },
|
|
end_non_cash_balance_cents: { type: :integer, description: 'Ending non-cash balance in currency minor units' },
|
|
end_balance: { type: :string },
|
|
end_balance_cents: { type: :integer, description: 'Ending total balance in currency minor units' },
|
|
account: { '$ref' => '#/components/schemas/BalanceAccount' },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
BalanceAccount: {
|
|
type: :object,
|
|
required: %w[id name account_type],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string },
|
|
account_type: { type: :string, nullable: true }
|
|
}
|
|
},
|
|
BalanceCollection: {
|
|
type: :object,
|
|
required: %w[balances pagination],
|
|
properties: {
|
|
balances: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/Balance' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
Category: {
|
|
type: :object,
|
|
required: %w[id name color icon],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string },
|
|
color: { type: :string },
|
|
icon: { type: :string }
|
|
}
|
|
},
|
|
CategoryParent: {
|
|
type: :object,
|
|
required: %w[id name],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string }
|
|
}
|
|
},
|
|
CategoryDetail: {
|
|
type: :object,
|
|
required: %w[id name color icon subcategories_count created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string },
|
|
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: %w[categories pagination],
|
|
properties: {
|
|
categories: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/CategoryDetail' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
CategoryCreateRequest: {
|
|
type: :object,
|
|
required: %w[category],
|
|
properties: {
|
|
category: {
|
|
type: :object,
|
|
required: %w[name],
|
|
properties: {
|
|
name: { type: :string, description: 'Category name (required, unique within family)' },
|
|
color: { type: :string, description: 'Hex color code (e.g. #22c55e). Defaults to #6172F3 if omitted; subcategories inherit parent color.' },
|
|
icon: { type: :string, description: 'Lucide icon name (e.g. "coffee"). Auto-suggested from the name when omitted.' },
|
|
parent_id: { type: :string, format: :uuid, nullable: true, description: 'Parent category ID. Must belong to the same family. Categories support up to 2 levels of nesting.' }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
Merchant: {
|
|
type: :object,
|
|
required: %w[id name],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string }
|
|
}
|
|
},
|
|
MerchantDetail: {
|
|
type: :object,
|
|
required: %w[id name type created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string },
|
|
type: { type: :string, enum: %w[FamilyMerchant ProviderMerchant] },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
MerchantImportResult: {
|
|
type: :object,
|
|
required: %w[imported skipped merchants],
|
|
properties: {
|
|
imported: { type: :integer, description: 'Number of merchants successfully created' },
|
|
skipped: { type: :integer, description: 'Number of rows skipped (duplicates or invalid)' },
|
|
merchants: { type: :array, items: { '$ref' => '#/components/schemas/MerchantDetail' } }
|
|
}
|
|
},
|
|
Tag: {
|
|
type: :object,
|
|
required: %w[id name color],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string },
|
|
color: { type: :string }
|
|
}
|
|
},
|
|
TagDetail: {
|
|
type: :object,
|
|
required: %w[id name color created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string },
|
|
color: { type: :string },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
TagCollection: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/TagDetail' }
|
|
},
|
|
RuleAction: {
|
|
type: :object,
|
|
required: %w[id action_type created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
action_type: { type: :string },
|
|
value: { type: :string, nullable: true },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
RuleCondition: {
|
|
type: :object,
|
|
required: %w[id condition_type operator sub_conditions created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
condition_type: { type: :string },
|
|
operator: { type: :string },
|
|
value: { type: :string, nullable: true },
|
|
sub_conditions: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/RuleCondition' }
|
|
},
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
Rule: {
|
|
type: :object,
|
|
required: %w[id resource_type active conditions actions created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string, nullable: true },
|
|
resource_type: { type: :string, enum: %w[transaction] },
|
|
active: { type: :boolean },
|
|
effective_date: { type: :string, format: :date, nullable: true },
|
|
conditions: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/RuleCondition' }
|
|
},
|
|
actions: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/RuleAction' }
|
|
},
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
RuleResponse: {
|
|
type: :object,
|
|
required: %w[data],
|
|
properties: {
|
|
data: { '$ref' => '#/components/schemas/Rule' }
|
|
}
|
|
},
|
|
RuleCollection: {
|
|
type: :object,
|
|
required: %w[data meta],
|
|
properties: {
|
|
data: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/Rule' }
|
|
},
|
|
meta: {
|
|
type: :object,
|
|
required: %w[current_page total_pages total_count per_page],
|
|
properties: {
|
|
current_page: { type: :integer },
|
|
next_page: { type: :integer, nullable: true },
|
|
prev_page: { type: :integer, nullable: true },
|
|
total_pages: { type: :integer },
|
|
total_count: { type: :integer },
|
|
per_page: { type: :integer }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
RuleRun: {
|
|
type: :object,
|
|
required: %w[id rule_id rule_name execution_type status transactions_queued transactions_processed transactions_modified pending_jobs_count executed_at rule created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
rule_id: { type: :string, format: :uuid },
|
|
rule_name: { type: :string, nullable: true },
|
|
execution_type: { type: :string, enum: %w[manual scheduled] },
|
|
status: { type: :string, enum: %w[pending success failed] },
|
|
transactions_queued: { type: :integer, minimum: 0 },
|
|
transactions_processed: { type: :integer, minimum: 0 },
|
|
transactions_modified: { type: :integer, minimum: 0 },
|
|
pending_jobs_count: { type: :integer, minimum: 0 },
|
|
executed_at: { type: :string, format: :'date-time' },
|
|
error_message: { type: :string, nullable: true },
|
|
rule: {
|
|
type: :object,
|
|
nullable: true,
|
|
required: %w[id resource_type active],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string, nullable: true },
|
|
resource_type: { type: :string },
|
|
active: { type: :boolean }
|
|
}
|
|
},
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
RuleRunResponse: {
|
|
type: :object,
|
|
required: %w[data],
|
|
properties: {
|
|
data: { '$ref' => '#/components/schemas/RuleRun' }
|
|
}
|
|
},
|
|
RuleRunCollection: {
|
|
type: :object,
|
|
required: %w[data meta],
|
|
properties: {
|
|
data: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/RuleRun' }
|
|
},
|
|
meta: {
|
|
type: :object,
|
|
required: %w[current_page total_pages total_count per_page],
|
|
properties: {
|
|
current_page: { type: :integer },
|
|
next_page: { type: :integer, nullable: true },
|
|
prev_page: { type: :integer, nullable: true },
|
|
total_pages: { type: :integer },
|
|
total_count: { type: :integer },
|
|
per_page: { type: :integer }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
Transfer: {
|
|
type: :object,
|
|
required: %w[id amount currency],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
amount: { type: :string },
|
|
currency: { type: :string },
|
|
other_account: { '$ref' => '#/components/schemas/Account', nullable: true }
|
|
}
|
|
},
|
|
RecurringTransaction: {
|
|
type: :object,
|
|
required: %w[id amount amount_cents currency expected_day_of_month last_occurrence_date next_expected_date status occurrence_count manual created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
amount: { type: :string },
|
|
amount_cents: { type: :integer, description: 'Amount in currency minor units' },
|
|
currency: { type: :string },
|
|
expected_day_of_month: { type: :integer, minimum: 1, maximum: 31 },
|
|
last_occurrence_date: { type: :string, format: :date },
|
|
next_expected_date: { type: :string, format: :date },
|
|
status: { type: :string, enum: %w[suggested active paused inactive ended] },
|
|
occurrence_count: { type: :integer, minimum: 0 },
|
|
name: { type: :string, nullable: true },
|
|
manual: { type: :boolean },
|
|
payment_url: { type: :string, nullable: true, description: 'Link to the biller portal where this bill is paid. Only http and https are accepted; a bare host is stored as https.' },
|
|
autopay: { type: :boolean, description: 'Whether this bill pays itself automatically.' },
|
|
notes: { type: :string, nullable: true, description: 'Free-text notes shown alongside the bill.' },
|
|
bill_type: { type: :string, enum: %w[bill subscription installment income transfer other], description: 'What kind of obligation this is.' },
|
|
category_id: { type: :string, format: :uuid, nullable: true },
|
|
anchor_date: { type: :string, format: :date, nullable: true, description: 'Reference occurrence that phases every-N cadences.' },
|
|
weekend_adjust: { type: :string, enum: %w[none skip before after] },
|
|
end_mode: { type: :string, enum: %w[never on_date after_count] },
|
|
end_on: { type: :string, format: :date, nullable: true },
|
|
end_after_count: { type: :integer, nullable: true },
|
|
renews_on: { type: :string, format: :date, nullable: true },
|
|
trial_ends_on: { type: :string, format: :date, nullable: true },
|
|
cancelled_on: { type: :string, format: :date, nullable: true },
|
|
recurrence_rules: {
|
|
type: :array,
|
|
description: 'Repetition patterns; multiple rows express semimonthly and similar multi-pattern cadences. Empty means legacy monthly on expected_day_of_month.',
|
|
items: {
|
|
type: :object,
|
|
properties: {
|
|
frequency: { type: :string, enum: %w[weekly monthly yearly] },
|
|
interval: { type: :integer },
|
|
day_of_month: { type: :integer, nullable: true, description: '-1 means the last day of the month.' },
|
|
weekday: { type: :integer, nullable: true },
|
|
weekday_ordinal: { type: :integer, nullable: true, description: '-1 means the last such weekday.' },
|
|
month_of_year: { type: :integer, nullable: true }
|
|
}
|
|
}
|
|
},
|
|
expected_amount_min: { type: :string, nullable: true },
|
|
expected_amount_min_cents: { type: :integer, nullable: true, description: 'Minimum expected amount in currency minor units' },
|
|
expected_amount_max: { type: :string, nullable: true },
|
|
expected_amount_max_cents: { type: :integer, nullable: true, description: 'Maximum expected amount in currency minor units' },
|
|
expected_amount_avg: { type: :string, nullable: true },
|
|
expected_amount_avg_cents: { type: :integer, nullable: true, description: 'Average expected amount in currency minor units' },
|
|
account: { '$ref' => '#/components/schemas/Account', nullable: true },
|
|
merchant: { '$ref' => '#/components/schemas/Merchant', nullable: true },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
RecurringTransactionCollection: {
|
|
type: :object,
|
|
required: %w[recurring_transactions pagination],
|
|
properties: {
|
|
recurring_transactions: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/RecurringTransaction' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
Transaction: {
|
|
type: :object,
|
|
required: %w[id date amount currency name classification account tags created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
date: { type: :string, format: :date },
|
|
amount: { type: :string },
|
|
currency: { type: :string },
|
|
name: { type: :string },
|
|
notes: { type: :string, nullable: true },
|
|
external_id: { type: :string, nullable: true },
|
|
source: { type: :string, nullable: true },
|
|
user_modified: { type: :boolean },
|
|
classification: { type: :string },
|
|
account: { '$ref' => '#/components/schemas/Account' },
|
|
category: { '$ref' => '#/components/schemas/Category', nullable: true },
|
|
merchant: { '$ref' => '#/components/schemas/Merchant', nullable: true },
|
|
tags: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/Tag' }
|
|
},
|
|
transfer: { '$ref' => '#/components/schemas/Transfer', nullable: true },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
TransactionCollection: {
|
|
type: :object,
|
|
required: %w[transactions pagination],
|
|
properties: {
|
|
transactions: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/Transaction' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
TransferTransactionSide: {
|
|
type: :object,
|
|
required: %w[id entry_id date amount amount_cents currency name kind account],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
entry_id: { type: :string, format: :uuid },
|
|
date: { type: :string, format: :date },
|
|
amount: { type: :string },
|
|
amount_cents: { type: :integer, description: 'Signed amount in currency minor units' },
|
|
currency: { type: :string },
|
|
name: { type: :string },
|
|
kind: { type: :string },
|
|
account: {
|
|
type: :object,
|
|
required: %w[id name account_type],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string },
|
|
account_type: { type: :string, nullable: true }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
TransferDecision: {
|
|
type: :object,
|
|
required: %w[id status date amount amount_cents currency transfer_type inflow_transaction outflow_transaction created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
status: { type: :string, enum: %w[pending confirmed] },
|
|
date: { type: :string, format: :date },
|
|
amount: { type: :string },
|
|
amount_cents: { type: :integer, description: 'Absolute transfer amount in currency minor units' },
|
|
currency: { type: :string },
|
|
transfer_type: { type: :string, enum: %w[transfer liability_payment loan_payment] },
|
|
notes: { type: :string, nullable: true },
|
|
source_fee_amount: { type: :string, nullable: true, description: 'Fee charged to the source account' },
|
|
source_fee_currency: { type: :string, nullable: true },
|
|
destination_fee_amount: { type: :string, nullable: true, description: 'Fee deducted from the destination account' },
|
|
destination_fee_currency: { type: :string, nullable: true },
|
|
inflow_transaction: { '$ref' => '#/components/schemas/TransferTransactionSide' },
|
|
outflow_transaction: { '$ref' => '#/components/schemas/TransferTransactionSide' },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
TransferDecisionCollection: {
|
|
type: :object,
|
|
required: %w[transfers pagination],
|
|
properties: {
|
|
transfers: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/TransferDecision' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
RejectedTransfer: {
|
|
type: :object,
|
|
required: %w[id inflow_transaction outflow_transaction created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
inflow_transaction: { '$ref' => '#/components/schemas/TransferTransactionSide' },
|
|
outflow_transaction: { '$ref' => '#/components/schemas/TransferTransactionSide' },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
RejectedTransferCollection: {
|
|
type: :object,
|
|
required: %w[rejected_transfers pagination],
|
|
properties: {
|
|
rejected_transfers: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/RejectedTransfer' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
Valuation: {
|
|
type: :object,
|
|
required: %w[id date amount currency kind account created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
date: { type: :string, format: :date },
|
|
amount: { type: :string },
|
|
currency: { type: :string },
|
|
notes: { type: :string, nullable: true },
|
|
kind: { type: :string },
|
|
account: { '$ref' => '#/components/schemas/Account' },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
ValuationCollection: {
|
|
type: :object,
|
|
required: %w[valuations pagination],
|
|
properties: {
|
|
valuations: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/Valuation' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
DeleteResponse: {
|
|
type: :object,
|
|
required: %w[message],
|
|
properties: {
|
|
message: { type: :string }
|
|
}
|
|
},
|
|
TransactionResponse: {
|
|
type: :object,
|
|
required: %w[id date amount currency name entryable_type account],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
date: { type: :string, format: :date },
|
|
amount: { type: :string },
|
|
currency: { type: :string },
|
|
name: { type: :string },
|
|
entryable_type: { type: :string },
|
|
account: {
|
|
type: :object,
|
|
required: %w[id name account_type],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string },
|
|
account_type: { type: :string, nullable: true }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
ImportConfiguration: {
|
|
type: :object,
|
|
properties: {
|
|
date_col_label: { type: :string, nullable: true },
|
|
amount_col_label: { type: :string, nullable: true },
|
|
name_col_label: { type: :string, nullable: true },
|
|
category_col_label: { type: :string, nullable: true },
|
|
tags_col_label: { type: :string, nullable: true },
|
|
notes_col_label: { type: :string, nullable: true },
|
|
account_col_label: { type: :string, nullable: true },
|
|
date_format: { type: :string, nullable: true },
|
|
number_format: { type: :string, nullable: true },
|
|
signage_convention: { type: :string, nullable: true }
|
|
}
|
|
},
|
|
ImportStats: {
|
|
type: :object,
|
|
required: %w[rows_count valid_rows_count invalid_rows_count mappings_count unassigned_mappings_count],
|
|
properties: {
|
|
rows_count: { type: :integer, minimum: 0 },
|
|
valid_rows_count: { type: :integer, minimum: 0 },
|
|
invalid_rows_count: { type: :integer, minimum: 0 },
|
|
mappings_count: { type: :integer, minimum: 0 },
|
|
unassigned_mappings_count: { type: :integer, minimum: 0 }
|
|
}
|
|
},
|
|
ImportVerificationReadback: {
|
|
type: :object,
|
|
description: 'SureImport only. Expected NDJSON counts compared to family-scoped database readback after publish.',
|
|
properties: {
|
|
status: { type: :string, enum: %w[not_verified matched mismatch failed reverted] },
|
|
checked_at: { type: :string, format: :'date-time', nullable: true },
|
|
expected_record_counts: {
|
|
type: :object,
|
|
additionalProperties: { type: :integer }
|
|
},
|
|
before_counts: {
|
|
type: :object,
|
|
additionalProperties: { type: :integer }
|
|
},
|
|
after_counts: {
|
|
type: :object,
|
|
additionalProperties: { type: :integer }
|
|
},
|
|
actual_delta_counts: {
|
|
type: :object,
|
|
additionalProperties: { type: :integer }
|
|
},
|
|
checked_counts: {
|
|
type: :object,
|
|
additionalProperties: { type: :integer }
|
|
},
|
|
mismatches: {
|
|
type: :object,
|
|
additionalProperties: {
|
|
type: :object,
|
|
required: %w[expected actual],
|
|
properties: {
|
|
expected: { type: :integer },
|
|
actual: { type: :integer }
|
|
}
|
|
}
|
|
},
|
|
error: { type: :string, nullable: true }
|
|
}
|
|
},
|
|
ImportVerification: {
|
|
type: :object,
|
|
description: 'SureImport only. Captured at upload and completed after import publish.',
|
|
required: %w[expected_record_counts readback],
|
|
properties: {
|
|
expected_record_counts: {
|
|
type: :object,
|
|
additionalProperties: { type: :integer }
|
|
},
|
|
readback: { '$ref' => '#/components/schemas/ImportVerificationReadback' }
|
|
}
|
|
},
|
|
ImportPreflightContent: {
|
|
type: :object,
|
|
required: %w[filename content_type byte_size],
|
|
properties: {
|
|
filename: { type: :string },
|
|
content_type: { type: :string },
|
|
byte_size: { type: :integer, minimum: 0 }
|
|
}
|
|
},
|
|
ImportPreflightError: {
|
|
type: :object,
|
|
required: %w[code message],
|
|
properties: {
|
|
code: { type: :string },
|
|
message: { type: :string }
|
|
}
|
|
},
|
|
ImportPreflightStats: {
|
|
type: :object,
|
|
required: %w[rows_count],
|
|
properties: {
|
|
rows_count: {
|
|
type: :integer,
|
|
minimum: 0,
|
|
description: 'CSV parsed non-header rows, or nonblank Sure NDJSON lines.'
|
|
},
|
|
valid_rows_count: {
|
|
type: :integer,
|
|
minimum: 0,
|
|
description: 'SureImport only. Valid NDJSON records.'
|
|
},
|
|
invalid_rows_count: {
|
|
type: :integer,
|
|
minimum: 0,
|
|
description: 'SureImport only. Invalid NDJSON records. CSV malformed content returns a 422 instead.'
|
|
},
|
|
entity_counts: {
|
|
type: :object,
|
|
additionalProperties: { type: :integer },
|
|
nullable: true
|
|
},
|
|
record_type_counts: {
|
|
type: :object,
|
|
additionalProperties: { type: :integer },
|
|
nullable: true
|
|
}
|
|
}
|
|
},
|
|
ImportPreflight: {
|
|
type: :object,
|
|
required: %w[type valid content stats errors warnings],
|
|
properties: {
|
|
type: { type: :string, enum: Import::TYPES },
|
|
valid: { type: :boolean },
|
|
content: { '$ref' => '#/components/schemas/ImportPreflightContent' },
|
|
stats: { '$ref' => '#/components/schemas/ImportPreflightStats' },
|
|
headers: {
|
|
type: :array,
|
|
items: { type: :string },
|
|
nullable: true
|
|
},
|
|
required_headers: {
|
|
type: :array,
|
|
items: { type: :string },
|
|
nullable: true
|
|
},
|
|
missing_required_headers: {
|
|
type: :array,
|
|
items: { type: :string },
|
|
nullable: true
|
|
},
|
|
errors: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/ImportPreflightError' }
|
|
},
|
|
warnings: {
|
|
type: :array,
|
|
items: { type: :string }
|
|
}
|
|
}
|
|
},
|
|
ImportPreflightResponse: {
|
|
type: :object,
|
|
required: %w[data],
|
|
properties: {
|
|
data: { '$ref' => '#/components/schemas/ImportPreflight' }
|
|
}
|
|
},
|
|
ImportStatusSummary: {
|
|
type: :object,
|
|
required: %w[uploaded configured terminal],
|
|
properties: {
|
|
uploaded: { type: :boolean },
|
|
configured: { type: :boolean },
|
|
terminal: { type: :boolean }
|
|
}
|
|
},
|
|
ImportStatusDetail: {
|
|
allOf: [
|
|
{ '$ref' => '#/components/schemas/ImportStatusSummary' },
|
|
{
|
|
type: :object,
|
|
required: %w[cleaned publishable revertable],
|
|
properties: {
|
|
cleaned: { type: :boolean },
|
|
publishable: { type: :boolean },
|
|
revertable: { type: :boolean }
|
|
}
|
|
}
|
|
]
|
|
},
|
|
ImportSummary: {
|
|
type: :object,
|
|
required: %w[id type status created_at updated_at status_detail],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
type: { type: :string, enum: Import::TYPES },
|
|
status: { type: :string, enum: %w[pending complete importing reverting revert_failed failed] },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' },
|
|
account_id: { type: :string, format: :uuid, nullable: true },
|
|
rows_count: { type: :integer, minimum: 0 },
|
|
error: { type: :string, nullable: true },
|
|
status_detail: { '$ref' => '#/components/schemas/ImportStatusSummary' }
|
|
}
|
|
},
|
|
ImportDetail: {
|
|
type: :object,
|
|
required: %w[id type status created_at updated_at status_detail configuration stats],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
type: { type: :string, enum: Import::TYPES },
|
|
status: { type: :string, enum: %w[pending complete importing reverting revert_failed failed] },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' },
|
|
account_id: { type: :string, format: :uuid, nullable: true },
|
|
error: { type: :string, nullable: true },
|
|
status_detail: { '$ref' => '#/components/schemas/ImportStatusDetail' },
|
|
configuration: { '$ref' => '#/components/schemas/ImportConfiguration' },
|
|
stats: { '$ref' => '#/components/schemas/ImportStats' },
|
|
verification: { '$ref' => '#/components/schemas/ImportVerification' }
|
|
}
|
|
},
|
|
ImportCollection: {
|
|
type: :object,
|
|
required: %w[data meta],
|
|
properties: {
|
|
data: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/ImportSummary' }
|
|
},
|
|
meta: {
|
|
type: :object,
|
|
required: %w[current_page total_pages total_count per_page],
|
|
properties: {
|
|
current_page: { type: :integer, minimum: 1 },
|
|
next_page: { type: :integer, nullable: true },
|
|
prev_page: { type: :integer, nullable: true },
|
|
total_pages: { type: :integer, minimum: 0 },
|
|
total_count: { type: :integer, minimum: 0 },
|
|
per_page: { type: :integer, minimum: 1 }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
ImportResponse: {
|
|
type: :object,
|
|
required: %w[data],
|
|
properties: {
|
|
data: { '$ref' => '#/components/schemas/ImportDetail' }
|
|
}
|
|
},
|
|
ImportSessionChunk: {
|
|
type: :object,
|
|
required: %w[id sequence status rows_count summary created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
sequence: { type: :integer, minimum: 1 },
|
|
client_chunk_id: { type: :string, nullable: true },
|
|
status: { type: :string, enum: %w[pending importing complete failed] },
|
|
rows_count: { type: :integer, minimum: 0 },
|
|
summary: {
|
|
type: :object,
|
|
additionalProperties: {
|
|
type: :object,
|
|
additionalProperties: { type: :integer }
|
|
}
|
|
},
|
|
error: {
|
|
type: :object,
|
|
nullable: true,
|
|
additionalProperties: true
|
|
},
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
ImportSession: {
|
|
type: :object,
|
|
required: %w[id type status chunks_count summary chunks created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
type: { type: :string, enum: %w[SureImport] },
|
|
status: { type: :string, enum: %w[pending importing complete failed] },
|
|
client_session_id: { type: :string, nullable: true },
|
|
expected_chunks: { type: :integer, nullable: true, minimum: 1 },
|
|
chunks_count: { type: :integer, minimum: 0 },
|
|
summary: {
|
|
type: :object,
|
|
additionalProperties: {
|
|
type: :object,
|
|
additionalProperties: { type: :integer }
|
|
}
|
|
},
|
|
error: {
|
|
type: :object,
|
|
nullable: true,
|
|
additionalProperties: true
|
|
},
|
|
chunks: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/ImportSessionChunk' }
|
|
},
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
ImportSessionResponse: {
|
|
type: :object,
|
|
required: %w[data],
|
|
properties: {
|
|
data: { '$ref' => '#/components/schemas/ImportSession' }
|
|
}
|
|
},
|
|
ProviderConnectionInstitution: {
|
|
type: :object,
|
|
required: %w[name],
|
|
properties: {
|
|
name: { type: :string, nullable: true },
|
|
domain: { type: :string, nullable: true },
|
|
url: { type: :string, nullable: true }
|
|
}
|
|
},
|
|
ProviderConnectionAccounts: {
|
|
type: :object,
|
|
required: %w[total_count linked_count unlinked_count],
|
|
properties: {
|
|
total_count: { type: :integer, minimum: 0 },
|
|
linked_count: { type: :integer, minimum: 0 },
|
|
unlinked_count: { type: :integer, minimum: 0 }
|
|
}
|
|
},
|
|
ProviderConnectionSyncLatest: {
|
|
type: :object,
|
|
required: %w[id status created_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
status: { type: :string },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
syncing_at: { type: :string, format: :'date-time', nullable: true },
|
|
completed_at: { type: :string, format: :'date-time', nullable: true },
|
|
failed_at: { type: :string, format: :'date-time', nullable: true },
|
|
error: {
|
|
type: :object,
|
|
nullable: true,
|
|
description: "Sanitized latest sync error summary. Null when the latest sync is not failed or stale.",
|
|
required: %w[present],
|
|
properties: {
|
|
present: { type: :boolean, description: "Always true when this object is present." },
|
|
message: { type: :string, nullable: true, description: "Stable sanitized error category message; raw provider error text is never exposed." }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
ProviderConnectionSync: {
|
|
type: :object,
|
|
required: %w[syncing],
|
|
properties: {
|
|
syncing: { type: :boolean },
|
|
status_summary: { type: :string, nullable: true },
|
|
last_synced_at: { type: :string, format: :'date-time', nullable: true },
|
|
latest: {
|
|
allOf: [ { '$ref' => '#/components/schemas/ProviderConnectionSyncLatest' } ],
|
|
nullable: true
|
|
}
|
|
}
|
|
},
|
|
ProviderConnection: {
|
|
type: :object,
|
|
required: %w[id provider provider_type name status requires_update credentials_configured scheduled_for_deletion pending_account_setup institution accounts sync created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
provider: { type: :string },
|
|
provider_type: { type: :string },
|
|
name: { type: :string },
|
|
status: { type: :string, nullable: true },
|
|
requires_update: { type: :boolean, nullable: true, description: "False when the provider item does not expose this status." },
|
|
credentials_configured: { type: :boolean, nullable: true, description: "False when credential readiness is unknown." },
|
|
scheduled_for_deletion: { type: :boolean, nullable: true, description: "False when the provider item does not expose this status." },
|
|
pending_account_setup: { type: :boolean, nullable: true, description: "False when account setup state is unknown." },
|
|
institution: { '$ref' => '#/components/schemas/ProviderConnectionInstitution' },
|
|
accounts: { '$ref' => '#/components/schemas/ProviderConnectionAccounts' },
|
|
sync: { '$ref' => '#/components/schemas/ProviderConnectionSync' },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
ProviderConnectionCollection: {
|
|
type: :object,
|
|
required: %w[data],
|
|
properties: {
|
|
data: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/ProviderConnection' }
|
|
}
|
|
}
|
|
},
|
|
ImportRowMapping: {
|
|
type: :object,
|
|
required: %w[key type value create_when_empty creatable mappable],
|
|
properties: {
|
|
key: { type: :string, nullable: true },
|
|
type: { type: :string },
|
|
value: { type: :string, nullable: true },
|
|
create_when_empty: { type: :boolean },
|
|
creatable: { type: :boolean },
|
|
mappable: {
|
|
type: :object,
|
|
nullable: true,
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
type: { type: :string },
|
|
name: { type: :string, nullable: true }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
ImportRowDiagnostic: {
|
|
type: :object,
|
|
required: %w[id row_number valid errors fields mappings],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
row_number: { type: :integer, minimum: 1 },
|
|
valid: { type: :boolean },
|
|
errors: {
|
|
type: :array,
|
|
items: { type: :string }
|
|
},
|
|
fields: {
|
|
type: :object,
|
|
properties: {
|
|
account: { type: :string, nullable: true },
|
|
date: { type: :string, nullable: true },
|
|
qty: { type: :string, nullable: true },
|
|
ticker: { type: :string, nullable: true },
|
|
exchange_operating_mic: { type: :string, nullable: true },
|
|
price: { type: :string, nullable: true },
|
|
amount: { type: :string, nullable: true },
|
|
currency: { type: :string, nullable: true },
|
|
name: { type: :string, nullable: true },
|
|
category: { type: :string, nullable: true },
|
|
tags: { type: :string, nullable: true },
|
|
entity_type: { type: :string, nullable: true },
|
|
notes: { type: :string, nullable: true },
|
|
active: { type: :boolean, nullable: true },
|
|
effective_date: { type: :string, nullable: true },
|
|
conditions: { type: :string, nullable: true },
|
|
actions: { type: :string, nullable: true }
|
|
}
|
|
},
|
|
mappings: {
|
|
type: :object,
|
|
properties: {
|
|
account: { '$ref' => '#/components/schemas/ImportRowMapping' },
|
|
category: { '$ref' => '#/components/schemas/ImportRowMapping' },
|
|
account_type: { '$ref' => '#/components/schemas/ImportRowMapping' },
|
|
tags: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/ImportRowMapping' }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
ImportRowDiagnosticCollection: {
|
|
type: :object,
|
|
required: %w[data meta],
|
|
properties: {
|
|
data: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/ImportRowDiagnostic' }
|
|
},
|
|
meta: {
|
|
type: :object,
|
|
required: %w[current_page total_pages total_count per_page],
|
|
properties: {
|
|
current_page: { type: :integer, minimum: 1 },
|
|
next_page: { type: :integer, nullable: true },
|
|
prev_page: { type: :integer, nullable: true },
|
|
total_pages: { type: :integer, minimum: 0 },
|
|
total_count: { type: :integer, minimum: 0 },
|
|
per_page: { type: :integer, minimum: 1 }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
SyncableSummary: {
|
|
type: :object,
|
|
required: %w[type id],
|
|
properties: {
|
|
type: { type: :string },
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string, nullable: true }
|
|
}
|
|
},
|
|
SyncErrorSummary: {
|
|
type: :object,
|
|
required: %w[message],
|
|
properties: {
|
|
message: { type: :string }
|
|
}
|
|
},
|
|
SyncResource: {
|
|
type: :object,
|
|
required: %w[id status in_progress terminal syncable children_count created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
status: { type: :string, enum: %w[pending syncing completed failed stale] },
|
|
in_progress: { type: :boolean },
|
|
terminal: { type: :boolean },
|
|
syncable: { '$ref' => '#/components/schemas/SyncableSummary' },
|
|
parent_id: { type: :string, format: :uuid, nullable: true },
|
|
children_count: { type: :integer, minimum: 0 },
|
|
window_start_date: { type: :string, format: :date, nullable: true },
|
|
window_end_date: { type: :string, format: :date, nullable: true },
|
|
pending_at: { type: :string, format: :'date-time', nullable: true },
|
|
syncing_at: { type: :string, format: :'date-time', nullable: true },
|
|
completed_at: { type: :string, format: :'date-time', nullable: true },
|
|
failed_at: { type: :string, format: :'date-time', nullable: true },
|
|
error: { nullable: true, allOf: [ { '$ref' => '#/components/schemas/SyncErrorSummary' } ] },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
SyncResponse: {
|
|
type: :object,
|
|
required: %w[data],
|
|
properties: {
|
|
data: { nullable: true, allOf: [ { '$ref' => '#/components/schemas/SyncResource' } ] }
|
|
}
|
|
},
|
|
SyncCollection: {
|
|
type: :object,
|
|
required: %w[data meta],
|
|
properties: {
|
|
data: {
|
|
type: :array,
|
|
maxItems: 100,
|
|
items: { '$ref' => '#/components/schemas/SyncResource' }
|
|
},
|
|
meta: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
Trade: {
|
|
type: :object,
|
|
required: %w[id date amount currency name qty price account created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
date: { type: :string, format: :date },
|
|
amount: { type: :string },
|
|
currency: { type: :string },
|
|
name: { type: :string },
|
|
notes: { type: :string, nullable: true },
|
|
qty: { type: :string },
|
|
price: { type: :string },
|
|
investment_activity_label: { type: :string, nullable: true },
|
|
account: { '$ref' => '#/components/schemas/Account' },
|
|
security: {
|
|
type: :object,
|
|
nullable: true,
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
ticker: { type: :string },
|
|
name: { type: :string, nullable: true }
|
|
}
|
|
},
|
|
category: {
|
|
type: :object,
|
|
nullable: true,
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
name: { type: :string }
|
|
}
|
|
},
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
TradeCollection: {
|
|
type: :object,
|
|
required: %w[trades pagination],
|
|
properties: {
|
|
trades: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/Trade' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
Holding: {
|
|
type: :object,
|
|
required: %w[id date qty price amount currency account security created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
date: { type: :string, format: :date },
|
|
qty: { type: :string, description: 'Quantity of shares held' },
|
|
price: { type: :string, description: 'Formatted price per share' },
|
|
amount: { type: :string },
|
|
currency: { type: :string },
|
|
cost_basis_source: { type: :string, nullable: true },
|
|
account: { '$ref' => '#/components/schemas/Account' },
|
|
security: {
|
|
type: :object,
|
|
required: %w[id ticker name],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
ticker: { type: :string },
|
|
name: { type: :string, nullable: true }
|
|
}
|
|
},
|
|
avg_cost: { type: :string, nullable: true },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
HoldingCollection: {
|
|
type: :object,
|
|
required: %w[holdings pagination],
|
|
properties: {
|
|
holdings: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/Holding' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
Security: {
|
|
type: :object,
|
|
required: %w[id ticker kind offline created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
ticker: { type: :string },
|
|
name: { type: :string, nullable: true },
|
|
kind: { type: :string, enum: %w[standard cash] },
|
|
country_code: { type: :string, nullable: true },
|
|
exchange_mic: { type: :string, nullable: true },
|
|
exchange_acronym: { type: :string, nullable: true },
|
|
exchange_operating_mic: { type: :string, nullable: true },
|
|
exchange_name: { type: :string, nullable: true },
|
|
offline: { type: :boolean },
|
|
offline_reason: { type: :string, nullable: true },
|
|
website_url: { type: :string, nullable: true },
|
|
logo_url: { type: :string, nullable: true },
|
|
first_provider_price_on: { type: :string, format: :date, nullable: true },
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
SecurityCollection: {
|
|
type: :object,
|
|
required: %w[securities pagination],
|
|
properties: {
|
|
securities: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/Security' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
SecurityPrice: {
|
|
type: :object,
|
|
required: %w[id date price price_amount currency provisional security created_at updated_at],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
date: { type: :string, format: :date },
|
|
price: { type: :string, description: 'Formatted security price' },
|
|
price_amount: { type: :string, description: 'Exact decimal security price' },
|
|
currency: { type: :string },
|
|
provisional: { type: :boolean },
|
|
security: {
|
|
type: :object,
|
|
required: %w[id ticker],
|
|
properties: {
|
|
id: { type: :string, format: :uuid },
|
|
ticker: { type: :string },
|
|
name: { type: :string, nullable: true },
|
|
exchange_operating_mic: { type: :string, nullable: true }
|
|
}
|
|
},
|
|
created_at: { type: :string, format: :'date-time' },
|
|
updated_at: { type: :string, format: :'date-time' }
|
|
}
|
|
},
|
|
SecurityPriceCollection: {
|
|
type: :object,
|
|
required: %w[security_prices pagination],
|
|
properties: {
|
|
security_prices: {
|
|
type: :array,
|
|
items: { '$ref' => '#/components/schemas/SecurityPrice' }
|
|
},
|
|
pagination: { '$ref' => '#/components/schemas/Pagination' }
|
|
}
|
|
},
|
|
Money: {
|
|
type: :object,
|
|
required: %w[amount currency formatted],
|
|
properties: {
|
|
amount: { type: :string, description: 'Numeric amount as string' },
|
|
currency: { type: :string, description: 'ISO 4217 currency code' },
|
|
formatted: { type: :string, description: 'Locale-formatted money string' }
|
|
}
|
|
},
|
|
BalanceSheet: {
|
|
type: :object,
|
|
required: %w[currency net_worth assets liabilities],
|
|
properties: {
|
|
currency: { type: :string, description: 'Family primary currency' },
|
|
net_worth: { '$ref' => '#/components/schemas/Money' },
|
|
assets: { '$ref' => '#/components/schemas/Money' },
|
|
liabilities: { '$ref' => '#/components/schemas/Money' }
|
|
}
|
|
},
|
|
SuccessMessage: {
|
|
type: :object,
|
|
required: %w[message],
|
|
properties: {
|
|
message: { type: :string }
|
|
}
|
|
},
|
|
ResetInitiatedResponse: {
|
|
type: :object,
|
|
required: %w[message status job_id family_id status_url],
|
|
properties: {
|
|
message: { type: :string },
|
|
status: { type: :string, enum: %w[queued] },
|
|
job_id: {
|
|
type: :string,
|
|
description: 'Informational Active Job identifier returned by the queue adapter; reset status is family-scoped, not job-scoped.'
|
|
},
|
|
family_id: { type: :string, format: :uuid, description: 'UUID of the family being reset.' },
|
|
status_url: { type: :string }
|
|
}
|
|
},
|
|
ResetStatusResponse: {
|
|
type: :object,
|
|
required: %w[status family_id reset_complete counts],
|
|
properties: {
|
|
status: {
|
|
type: :string,
|
|
enum: %w[complete data_remaining],
|
|
description: 'Counts-based family reset status at response time.'
|
|
},
|
|
family_id: { type: :string, format: :uuid, description: 'UUID of the family whose reset target counts were checked.' },
|
|
reset_complete: {
|
|
type: :boolean,
|
|
description: 'True when all reset target counts are zero at response time. This is a family data snapshot, not a durable per-job completion record.'
|
|
},
|
|
counts: {
|
|
type: :object,
|
|
required: reset_count_keys,
|
|
additionalProperties: { type: :integer, minimum: 0 },
|
|
properties: reset_count_keys.index_with { { type: :integer, minimum: 0 } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
config.openapi_format = :yaml
|
|
end
|