feat(ts): type Bill + Payment API responses with branded Dollars (A2-A3)

Adds Bill and reuses Payment domain types (money → Dollars, mirroring
serializeBill/serializePayment which spread the DB row and convert the money
columns). Wired: bills/allBills/deletedBills → Bill[], bill/createBill/updateBill
→ Bill; quickPay/createPayment/updatePayment/restorePayment → Payment. Replaced
api.ts's minimal PaymentRecord with the richer @/types Payment (usePaymentActions
still resolves payment.id). typecheck 0, build green, 48 client tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-03 22:24:35 -05:00
parent 3c7a55c3a1
commit 337ad95a3e
2 changed files with 55 additions and 17 deletions

View File

@ -1,4 +1,4 @@
import type { TrackerResponse } from '@/types'; import type { Bill, Payment, TrackerResponse } from '@/types';
// Fetch CSRF token from the server once and cache in memory. // Fetch CSRF token from the server once and cache in memory.
// The cookie is httpOnly so document.cookie cannot access it directly. // The cookie is httpOnly so document.cookie cannot access it directly.
@ -31,12 +31,6 @@ export interface ApiError extends Error {
code?: string; code?: string;
} }
/** Minimal shape of a payment record returned by the pay endpoints. */
export interface PaymentRecord {
id: number;
[key: string]: unknown;
}
/** Result of toggling a tracker row paid/unpaid. */ /** Result of toggling a tracker row paid/unpaid. */
export interface TogglePaidResult { export interface TogglePaidResult {
paymentId?: number; paymentId?: number;
@ -239,13 +233,13 @@ export const api = {
updateMonthlyStartingAmounts: (data: Body) => put('/monthly-starting-amounts', data), updateMonthlyStartingAmounts: (data: Body) => put('/monthly-starting-amounts', data),
// Bills // Bills
bills: (params: QueryParams = {}) => get(`/bills${queryString(params)}`), bills: (params: QueryParams = {}) => get<Bill[]>(`/bills${queryString(params)}`),
allBills: (params: QueryParams = {}) => get(`/bills${queryString({ inactive: true, ...params })}`), allBills: (params: QueryParams = {}) => get<Bill[]>(`/bills${queryString({ inactive: true, ...params })}`),
deletedBills: () => get('/bills/deleted'), deletedBills: () => get<Bill[]>('/bills/deleted'),
billAudit: (includeInactive = false) => get(`/bills/audit${includeInactive ? '?inactive=true' : ''}`), billAudit: (includeInactive = false) => get(`/bills/audit${includeInactive ? '?inactive=true' : ''}`),
bill: (id: Id) => get(`/bills/${id}`), bill: (id: Id) => get<Bill>(`/bills/${id}`),
createBill: (data: Body) => post('/bills', data), createBill: (data: Body) => post<Bill>('/bills', data),
updateBill: (id: Id, data: Body) => put(`/bills/${id}`, data), updateBill: (id: Id, data: Body) => put<Bill>(`/bills/${id}`, data),
reorderBills: (order: Body) => put('/bills/reorder', order), reorderBills: (order: Body) => put('/bills/reorder', order),
archiveBill: (id: Id, archived = true) => put(`/bills/${id}/archived`, { archived }), archiveBill: (id: Id, archived = true) => put(`/bills/${id}/archived`, { archived }),
updateBillBalance: (id: Id, bal: unknown) => _fetch('PATCH', `/bills/${id}/balance`, { current_balance: bal }), updateBillBalance: (id: Id, bal: unknown) => _fetch('PATCH', `/bills/${id}/balance`, { current_balance: bal }),
@ -311,14 +305,14 @@ export const api = {
deleteCatalogDescriptor: (id: Id) => _fetch('DELETE', `/subscriptions/catalog/descriptors/${id}`), deleteCatalogDescriptor: (id: Id) => _fetch('DELETE', `/subscriptions/catalog/descriptors/${id}`),
// Payments // Payments
quickPay: (data: Body) => post<PaymentRecord>('/payments/quick', data), quickPay: (data: Body) => post<Payment>('/payments/quick', data),
confirmAutopaySuggestion: (billId: Id, data: Body) => post(`/payments/autopay-suggestions/${billId}/confirm`, data), confirmAutopaySuggestion: (billId: Id, data: Body) => post(`/payments/autopay-suggestions/${billId}/confirm`, data),
dismissAutopaySuggestion: (billId: Id, data: Body) => post(`/payments/autopay-suggestions/${billId}/dismiss`, data), dismissAutopaySuggestion: (billId: Id, data: Body) => post(`/payments/autopay-suggestions/${billId}/dismiss`, data),
bulkPay: (items: Body) => post('/payments/bulk', items), bulkPay: (items: Body) => post('/payments/bulk', items),
createPayment: (data: Body) => post('/payments', data), createPayment: (data: Body) => post<Payment>('/payments', data),
updatePayment: (id: Id, data: Body) => put(`/payments/${id}`, data), updatePayment: (id: Id, data: Body) => put<Payment>(`/payments/${id}`, data),
deletePayment: (id: Id) => del(`/payments/${id}`), deletePayment: (id: Id) => del(`/payments/${id}`),
restorePayment: (id: Id) => post(`/payments/${id}/restore`), restorePayment: (id: Id) => post<Payment>(`/payments/${id}/restore`),
recentAutoMatched: () => get('/payments/recent-auto'), recentAutoMatched: () => get('/payments/recent-auto'),
undoAutoMatch: (id: Id) => post(`/payments/${id}/undo-auto`), undoAutoMatch: (id: Id) => post(`/payments/${id}/undo-auto`),

View File

@ -30,6 +30,50 @@ export interface Payment {
[key: string]: unknown; [key: string]: unknown;
} }
// A bill as serialized by the server (billsService.serializeBill spreads the DB
// row and converts the three money columns to dollars). SQLite booleans come back
// as 0/1 integers. Extra joined columns (sparkline, has_merchant_rule, …) pass
// through as `unknown` via the index signature.
export interface Bill {
id: number;
user_id?: number;
name: string;
category_id: number | null;
category_name?: string | null;
due_day: number | null;
override_due_date: string | null;
bucket: string;
expected_amount: Dollars;
interest_rate: number | null;
billing_cycle: string | null;
autopay_enabled: number;
autodraft_status: string | null;
auto_mark_paid: number;
website: string | null;
username: string | null;
account_info: string | null;
has_2fa: number;
notes: string | null;
history_visibility: string | null;
active: number;
cycle_type: string | null;
cycle_day: string | number | null;
current_balance: Dollars | null;
minimum_payment: Dollars | null;
snowball_order: number | null;
snowball_include: number;
snowball_exempt: number;
is_subscription: number;
subscription_type: string | null;
reminder_days_before: number | null;
subscription_source: string | null;
subscription_detected_at: string | null;
created_at?: string;
updated_at?: string;
deleted_at?: string | null;
[key: string]: unknown;
}
// One tracker row: a bill's state for a given month. Built by // One tracker row: a bill's state for a given month. Built by
// statusService.buildTrackerRow and augmented by trackerService.getTracker. // statusService.buildTrackerRow and augmented by trackerService.getTracker.
export interface TrackerRow { export interface TrackerRow {