mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 12:08:47 +02:00
OpenAPI type codegen with CI drift gate (plan 3.4, ARCH-16, FE-14)
Every dict-returning route the SPA consumes now declares a response model (auth, bulk, sync-status, notifications). api-types.gen.ts is generated from app.openapi() via openapi-typescript, and the 22 hand- written read interfaces in api.ts are now aliases onto the generated schemas — backend schema drift becomes a typecheck failure, and CI regenerates the file and fails the deploy if it's stale. The aliasing immediately caught two real drifts: RecurringItemType was missing SAVINGS, and raw_charges (untyped JSON column) is now explicitly shaped at the boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4058
frontend/src/lib/api-types.gen.ts
Normal file
4058
frontend/src/lib/api-types.gen.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,10 @@
|
||||
import type { components } from "./api-types.gen";
|
||||
|
||||
/** Schemas generated from the backend OpenAPI spec (pnpm gen:api). Aliasing
|
||||
* the app's read types to them makes backend schema drift a typecheck error
|
||||
* instead of a runtime surprise (review ARCH-16 / FE-14 / plan 3.4). */
|
||||
type Schema<K extends keyof components["schemas"]> = components["schemas"][K];
|
||||
|
||||
const BASE_URL = "/api/v1";
|
||||
|
||||
class ApiError extends Error {
|
||||
@@ -125,91 +132,22 @@ export async function logout() {
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface Account {
|
||||
id: number;
|
||||
bank: string;
|
||||
currency: string;
|
||||
label: string;
|
||||
balance: number;
|
||||
account_type: string;
|
||||
next_payment: number | null;
|
||||
updated_at: string;
|
||||
}
|
||||
export type Account = Schema<'AccountRead'>;
|
||||
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
auto_match_patterns: string | null;
|
||||
}
|
||||
export type Category = Schema<'CategoryRead'>;
|
||||
|
||||
export interface SkippedDuplicate {
|
||||
line: number;
|
||||
merchant: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
existing_id: number;
|
||||
existing_merchant: string;
|
||||
existing_date: string;
|
||||
existing_amount: number;
|
||||
existing_source: string;
|
||||
}
|
||||
export type SkippedDuplicate = Schema<'SkippedDuplicate'>;
|
||||
|
||||
export interface ImportResult {
|
||||
imported: number;
|
||||
duplicates: number;
|
||||
errors: string[];
|
||||
skipped: SkippedDuplicate[];
|
||||
}
|
||||
export type ImportResult = Schema<'PasteImportResult'>;
|
||||
|
||||
export interface Transaction {
|
||||
id: number;
|
||||
amount: number;
|
||||
currency: string;
|
||||
merchant: string;
|
||||
city: string | null;
|
||||
date: string;
|
||||
card_type: string | null;
|
||||
card_last4: string | null;
|
||||
authorization_code: string | null;
|
||||
reference: string | null;
|
||||
transaction_type: string;
|
||||
source: string;
|
||||
bank: string;
|
||||
notes: string | null;
|
||||
category_id: number | null;
|
||||
category: Category | null;
|
||||
deferred_to_next_cycle: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
export type Transaction = Schema<'TransactionRead'>;
|
||||
|
||||
// --- Budget / Recurring Items ---
|
||||
|
||||
export type RecurringItemType = "INCOME" | "EXPENSE";
|
||||
export type RecurringFrequency =
|
||||
| "WEEKLY"
|
||||
| "MONTHLY"
|
||||
| "QUARTERLY"
|
||||
| "BIANNUAL"
|
||||
| "YEARLY";
|
||||
export type RecurringItemType = Schema<"RecurringItemType">;
|
||||
export type RecurringFrequency = Schema<"RecurringFrequency">;
|
||||
|
||||
export interface RecurringItem {
|
||||
id: number;
|
||||
name: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
item_type: RecurringItemType;
|
||||
frequency: RecurringFrequency;
|
||||
day_of_month: number | null;
|
||||
month_of_year: number | null;
|
||||
override_amounts: Record<string, number> | null;
|
||||
category_id: number | null;
|
||||
is_active: boolean;
|
||||
notes: string | null;
|
||||
created_at: string;
|
||||
category: Category | null;
|
||||
}
|
||||
export type RecurringItem = Schema<'RecurringItemRead'>;
|
||||
|
||||
export interface RecurringItemCreate {
|
||||
name: string;
|
||||
@@ -239,76 +177,19 @@ export interface RecurringItemUpdate {
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
export interface RecurringItemDetail {
|
||||
id: number;
|
||||
name: string;
|
||||
amount: number;
|
||||
projected_amount: number | null;
|
||||
used_actual: boolean;
|
||||
item_type: string;
|
||||
frequency: string;
|
||||
category_name: string | null;
|
||||
category_id: number | null;
|
||||
}
|
||||
export type RecurringItemDetail = Schema<'RecurringItemDetail'>;
|
||||
|
||||
export interface ActualsBySource {
|
||||
source: string;
|
||||
total_compra: number;
|
||||
total_devolucion: number;
|
||||
net: number;
|
||||
count: number;
|
||||
}
|
||||
export type ActualsBySource = Schema<'ActualsBySource'>;
|
||||
|
||||
export interface MonthlyProjection {
|
||||
month: number;
|
||||
year: number;
|
||||
projected_income: number;
|
||||
projected_fixed_expenses: number;
|
||||
actual_credit_card: number;
|
||||
actual_cash: number;
|
||||
actual_transfers: number;
|
||||
uncovered_actual: number;
|
||||
gran_total_egresos: number;
|
||||
net_balance: number;
|
||||
carryover_balance: number;
|
||||
cumulative_balance: number;
|
||||
balance_overridden: boolean;
|
||||
}
|
||||
export type MonthlyProjection = Schema<'MonthlyProjectionResponse'>;
|
||||
|
||||
export interface YearlyProjection {
|
||||
year: number;
|
||||
months: MonthlyProjection[];
|
||||
annual_income: number;
|
||||
annual_expenses: number;
|
||||
annual_net: number;
|
||||
}
|
||||
export type YearlyProjection = Schema<'YearlyProjectionResponse'>;
|
||||
|
||||
export interface MonthlyDetail {
|
||||
year: number;
|
||||
month: number;
|
||||
income_items: RecurringItemDetail[];
|
||||
expense_items: RecurringItemDetail[];
|
||||
actuals_by_source: ActualsBySource[];
|
||||
total_projected_income: number;
|
||||
total_projected_expenses: number;
|
||||
uncovered_actual: number;
|
||||
gran_total_egresos: number;
|
||||
net_balance: number;
|
||||
cc_by_category: { category_name: string; amount: number }[];
|
||||
}
|
||||
export type MonthlyDetail = Schema<'MonthlyDetailResponse'>;
|
||||
|
||||
// --- Savings Accrual ---
|
||||
|
||||
export interface SavingsAccrual {
|
||||
id: number;
|
||||
year: number;
|
||||
month: number;
|
||||
memp_amount: number;
|
||||
mpat_amount: number;
|
||||
trigger_transaction_id: number | null;
|
||||
applied_at: string;
|
||||
notes: string | null;
|
||||
}
|
||||
export type SavingsAccrual = Schema<'SavingsAccrualRead'>;
|
||||
|
||||
export interface SavingsAccrualCreate {
|
||||
year: number;
|
||||
@@ -361,11 +242,7 @@ export const deleteBalanceOverride = (year: number, month: number) =>
|
||||
|
||||
// --- Salarios ---
|
||||
|
||||
export interface SalariosSummary {
|
||||
count: number;
|
||||
total_amount: number;
|
||||
latest_date: string | null;
|
||||
}
|
||||
export type SalariosSummary = Schema<'SalariosSummary'>;
|
||||
|
||||
export const getSalarios = (params?: { limit?: number; offset?: number }) =>
|
||||
api.get<Transaction[]>("/salarios/", { params });
|
||||
@@ -374,32 +251,9 @@ export const getSalariosSummary = () =>
|
||||
|
||||
// --- Pensions ---
|
||||
|
||||
export interface PensionSnapshot {
|
||||
id: number;
|
||||
fund: string;
|
||||
contract_number: string;
|
||||
period_start: string;
|
||||
period_end: string;
|
||||
saldo_anterior: number;
|
||||
aportes: number;
|
||||
rendimientos: number;
|
||||
retiros: number;
|
||||
traslados: number;
|
||||
comision: number;
|
||||
correccion: number;
|
||||
bonificacion: number;
|
||||
saldo_final: number;
|
||||
source_filename: string;
|
||||
created_at: string;
|
||||
}
|
||||
export type PensionSnapshot = Schema<'PensionSnapshotRead'>;
|
||||
|
||||
export interface PensionUploadResult {
|
||||
imported: number;
|
||||
updated: number;
|
||||
duplicates: number;
|
||||
errors: string[];
|
||||
snapshots: PensionSnapshot[];
|
||||
}
|
||||
export type PensionUploadResult = Schema<'PensionUploadResult'>;
|
||||
|
||||
export interface PensionManualEntry {
|
||||
fund: string;
|
||||
@@ -436,51 +290,19 @@ export interface MunicipalCharge {
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface WaterMeterReading {
|
||||
id: number;
|
||||
meter_id: string;
|
||||
period: string;
|
||||
reading_previous: number;
|
||||
reading_current: number;
|
||||
consumption_m3: number;
|
||||
agua_potable: number;
|
||||
serv_ambientales: number;
|
||||
alcant_sanitario: number;
|
||||
iva: number;
|
||||
is_historical: boolean;
|
||||
receipt_id: number | null;
|
||||
created_at: string;
|
||||
}
|
||||
export type WaterMeterReading = Schema<'WaterMeterReadingRead'>;
|
||||
|
||||
export interface MunicipalReceipt {
|
||||
id: number;
|
||||
receipt_date: string;
|
||||
due_date: string;
|
||||
period: string;
|
||||
account: string;
|
||||
finca: string;
|
||||
holder_name: string;
|
||||
holder_cedula: string;
|
||||
holder_address: string;
|
||||
subtotal: number;
|
||||
interests: number;
|
||||
iva: number;
|
||||
total: number;
|
||||
raw_charges: MunicipalCharge[];
|
||||
source_filename: string;
|
||||
created_at: string;
|
||||
}
|
||||
export type MunicipalReceipt = Omit<
|
||||
Schema<'MunicipalReceiptRead'>,
|
||||
'raw_charges'
|
||||
> & { raw_charges: MunicipalCharge[] };
|
||||
|
||||
export interface MunicipalReceiptDetail extends MunicipalReceipt {
|
||||
water_readings: WaterMeterReading[];
|
||||
}
|
||||
export type MunicipalReceiptDetail = Omit<
|
||||
Schema<'MunicipalReceiptDetailRead'>,
|
||||
'raw_charges'
|
||||
> & { raw_charges: MunicipalCharge[] };
|
||||
|
||||
export interface MunicipalReceiptUploadResult {
|
||||
imported: number;
|
||||
updated: number;
|
||||
errors: string[];
|
||||
receipt: MunicipalReceipt | null;
|
||||
}
|
||||
export type MunicipalReceiptUploadResult = Schema<'MunicipalReceiptUploadResult'>;
|
||||
|
||||
export const uploadMunicipalReceipt = (file: File) => {
|
||||
const form = new FormData();
|
||||
@@ -502,20 +324,9 @@ export const getWaterConsumption = (months?: number) =>
|
||||
|
||||
// --- Sync Status ---
|
||||
|
||||
export interface SyncSource {
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
last_received: string | null;
|
||||
age_days: number | null;
|
||||
warn_after_days: number;
|
||||
status: 'ok' | 'warning' | 'never';
|
||||
}
|
||||
export type SyncSource = Schema<'SyncSourceStatus'>;
|
||||
|
||||
export interface SyncStatusResponse {
|
||||
sources: SyncSource[];
|
||||
warnings: number;
|
||||
}
|
||||
export type SyncStatusResponse = Schema<'SyncStatusResponse'>;
|
||||
|
||||
export const getSyncStatus = () => api.get<SyncStatusResponse>('/sync-status/');
|
||||
|
||||
@@ -523,10 +334,6 @@ export const getSyncStatus = () => api.get<SyncStatusResponse>('/sync-status/');
|
||||
|
||||
export type UserSettingsData = Record<string, unknown>;
|
||||
|
||||
export interface UserSettingsRead {
|
||||
key: string;
|
||||
data: UserSettingsData;
|
||||
updated_at: string;
|
||||
}
|
||||
export type UserSettingsRead = Schema<'UserSettingsRead'>;
|
||||
|
||||
export const getUserSettings = () => api.get<UserSettingsRead>('/settings/');
|
||||
|
||||
Reference in New Issue
Block a user