refactor(ts): type the subscriptions API responses (Track C)
Promoted SubscriptionsPage's Subscription/SubData/Recommendation/ CatalogMatch/SubTx (+ deps) into @/types and wired subscriptions/ recommendations/transaction-matches/create/match-bill with get<T>/ post<T>. Removed the (r: any) in useSubscriptionRecommendations, the 5 page casts, and an unused CatalogMatch import. typecheck/lint/build clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
8265b4a97f
commit
6bb8c63d39
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
### 🔌 API response typing cleanup (Track C) — promoting page shapes into `@/types`, endpoint by endpoint
|
### 🔌 API response typing cleanup (Track C) — promoting page shapes into `@/types`, endpoint by endpoint
|
||||||
|
|
||||||
|
- **[Client] Subscriptions domain typed.** Promoted the `SubscriptionsPage` shapes into `@/types` (`Subscription`, `SubscriptionSummary`/`…TopType`, `SubscriptionsResponse`, `Recommendation` + `RecommendationEvidence`/`…Transaction`/`ExistingBillMatch`/`CatalogMatch`, `SubscriptionTx`) and wired `subscriptions`/`subscriptionRecommendations`/`subscriptionTransactionMatches`/`createSubscriptionFromRecommendation`/`matchRecommendationToBill` with `get<T>`/`post<T>`. Dropped the `(r: any)` in `useSubscriptionRecommendations` (now typed) and the 5 page casts; removed an unused `CatalogMatch` import (Track E).
|
||||||
- **[Client] Spending domain typed.** Promoted `SpendingPage`'s local response interfaces into `@/types` (`SpendingCategoryEntry`, `SpendingSummary`, `SpendingTransaction`/`…Response`, `SpendingIncomeTx`/`…Response`, `SpendingRule`, `CategoryGroup`, `CopyBudgetsResponse`) and wired `spendingSummary`/`spendingTransactions`/`spendingIncome`/`spendingCategoryRules`/`copySpendingBudgets`/`categoryGroups` with `get<T>`/`post<T>`. `useSpendingSummary`/`useSpendingTransactions`/`useCategoryGroups` now return typed data; the 6 spending `as {…}` casts are gone. Removed 2 now-unused type aliases the cast-removal orphaned (Track E).
|
- **[Client] Spending domain typed.** Promoted `SpendingPage`'s local response interfaces into `@/types` (`SpendingCategoryEntry`, `SpendingSummary`, `SpendingTransaction`/`…Response`, `SpendingIncomeTx`/`…Response`, `SpendingRule`, `CategoryGroup`, `CopyBudgetsResponse`) and wired `spendingSummary`/`spendingTransactions`/`spendingIncome`/`spendingCategoryRules`/`copySpendingBudgets`/`categoryGroups` with `get<T>`/`post<T>`. `useSpendingSummary`/`useSpendingTransactions`/`useCategoryGroups` now return typed data; the 6 spending `as {…}` casts are gone. Removed 2 now-unused type aliases the cast-removal orphaned (Track E).
|
||||||
- **[Client] Bank-ledger domain typed end-to-end.** Promoted the `BankTransactionsPage` local interfaces (`Tx`/`Account`/`Ledger`/`LedgerSummary`/`CategoryBreakdown`/auto-categorize preview) into `@/types` as `BankLedgerTransaction` (extends the existing `BankTransaction`, so raw amounts stay branded `Cents`), `BankAccount`, `BankLedger`, `BankLedgerSummary`, `BankCategoryBreakdown`, `AutoCategorizePreview`. Wired the endpoints in `api.ts` (`bankTransactionsLedger` → `get<BankLedger>`, `matchTransaction`/`unmatchTransaction` → `{ transaction }`, `ignoreTransaction`/`unignoreTransaction` → the transaction, `applyTransactionMerchantMatch`/`autoCategorizeTransactions` → their result shapes), so `useBankLedger`'s `data` is now typed and **all 9 `as {…}` casts in the page are gone** — a server field rename now fails in `@/types`, not silently at 9 call sites. Dropped a now-dead `Cents` import (Track E). typecheck/lint/build clean.
|
- **[Client] Bank-ledger domain typed end-to-end.** Promoted the `BankTransactionsPage` local interfaces (`Tx`/`Account`/`Ledger`/`LedgerSummary`/`CategoryBreakdown`/auto-categorize preview) into `@/types` as `BankLedgerTransaction` (extends the existing `BankTransaction`, so raw amounts stay branded `Cents`), `BankAccount`, `BankLedger`, `BankLedgerSummary`, `BankCategoryBreakdown`, `AutoCategorizePreview`. Wired the endpoints in `api.ts` (`bankTransactionsLedger` → `get<BankLedger>`, `matchTransaction`/`unmatchTransaction` → `{ transaction }`, `ignoreTransaction`/`unignoreTransaction` → the transaction, `applyTransactionMerchantMatch`/`autoCategorizeTransactions` → their result shapes), so `useBankLedger`'s `data` is now typed and **all 9 `as {…}` casts in the page are gone** — a server field rename now fails in `@/types`, not silently at 9 call sites. Dropped a now-dead `Cents` import (Track E). typecheck/lint/build clean.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type {
|
||||||
BankLedger, BankLedgerTransaction, AutoCategorizePreview,
|
BankLedger, BankLedgerTransaction, AutoCategorizePreview,
|
||||||
SpendingSummary, SpendingTransactionsResponse, SpendingIncomeResponse,
|
SpendingSummary, SpendingTransactionsResponse, SpendingIncomeResponse,
|
||||||
SpendingRule, CopyBudgetsResponse, CategoryGroup,
|
SpendingRule, CopyBudgetsResponse, CategoryGroup,
|
||||||
|
SubscriptionsResponse, Recommendation, SubscriptionTx,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
|
|
||||||
// Fetch CSRF token from the server once and cache in memory.
|
// Fetch CSRF token from the server once and cache in memory.
|
||||||
|
|
@ -285,19 +286,19 @@ export const api = {
|
||||||
deleteBillTemplate: (id: Id) => del(`/bills/templates/${id}`),
|
deleteBillTemplate: (id: Id) => del(`/bills/templates/${id}`),
|
||||||
|
|
||||||
// Subscriptions
|
// Subscriptions
|
||||||
subscriptions: () => get('/subscriptions'),
|
subscriptions: () => get<SubscriptionsResponse>('/subscriptions'),
|
||||||
confirmTransactionMatch: (transactionId: Id, billId: Id) => post('/matches/confirm', { transaction_id: transactionId, bill_id: billId }),
|
confirmTransactionMatch: (transactionId: Id, billId: Id) => post('/matches/confirm', { transaction_id: transactionId, bill_id: billId }),
|
||||||
matchRecommendationToBill: (transactionIds: unknown, billId: Id, merchant: unknown, catalogId: unknown, confidence: unknown) => post('/subscriptions/recommendations/match-bill', {
|
matchRecommendationToBill: (transactionIds: unknown, billId: Id, merchant: unknown, catalogId: unknown, confidence: unknown) => post<{ matched_count?: number; bill_name?: string }>('/subscriptions/recommendations/match-bill', {
|
||||||
transaction_ids: transactionIds,
|
transaction_ids: transactionIds,
|
||||||
bill_id: billId,
|
bill_id: billId,
|
||||||
merchant,
|
merchant,
|
||||||
catalog_id: catalogId,
|
catalog_id: catalogId,
|
||||||
confidence,
|
confidence,
|
||||||
}),
|
}),
|
||||||
subscriptionRecommendations: () => get('/subscriptions/recommendations'),
|
subscriptionRecommendations: () => get<{ recommendations?: Recommendation[] }>('/subscriptions/recommendations'),
|
||||||
subscriptionTransactionMatches: (params: QueryParams = {}) => get(`/subscriptions/transaction-matches${queryString(params)}`),
|
subscriptionTransactionMatches: (params: QueryParams = {}) => get<SubscriptionTx[] | { transactions?: SubscriptionTx[] }>(`/subscriptions/transaction-matches${queryString(params)}`),
|
||||||
updateSubscription: (id: Id, data: Body) => _fetch('PATCH', `/subscriptions/${id}`, data),
|
updateSubscription: (id: Id, data: Body) => _fetch('PATCH', `/subscriptions/${id}`, data),
|
||||||
createSubscriptionFromRecommendation: (data: Body) => post('/subscriptions/recommendations/create', data),
|
createSubscriptionFromRecommendation: (data: Body) => post<{ id?: number; name?: string }>('/subscriptions/recommendations/create', data),
|
||||||
declineRecommendation: (recommendation: any) => post('/subscriptions/recommendations/decline', {
|
declineRecommendation: (recommendation: any) => post('/subscriptions/recommendations/decline', {
|
||||||
decline_key: recommendation?.decline_key || recommendation,
|
decline_key: recommendation?.decline_key || recommendation,
|
||||||
catalog_id: recommendation?.catalog_match?.id,
|
catalog_id: recommendation?.catalog_match?.id,
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ export function useSubscriptions() {
|
||||||
export function useSubscriptionRecommendations() {
|
export function useSubscriptionRecommendations() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['subscription-recommendations'],
|
queryKey: ['subscription-recommendations'],
|
||||||
queryFn: () => api.subscriptionRecommendations().then((r: any) => r.recommendations || []),
|
queryFn: () => api.subscriptionRecommendations().then(r => r.recommendations || []),
|
||||||
staleTime: 1000 * 60 * 5,
|
staleTime: 1000 * 60 * 5,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,15 @@ import { getLinkImportPref } from '@/pages/SettingsPage';
|
||||||
import { useSearchPanelPreference } from '@/hooks/useSearchPanelPreference';
|
import { useSearchPanelPreference } from '@/hooks/useSearchPanelPreference';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { moveInArray, movedItemId, reorderPayload } from '@/lib/reorder';
|
import { moveInArray, movedItemId, reorderPayload } from '@/lib/reorder';
|
||||||
import type { Bill, Category } from '@/types';
|
import type {
|
||||||
|
Bill, Category,
|
||||||
|
Subscription,
|
||||||
|
SubscriptionsResponse as SubData,
|
||||||
|
SubscriptionSummary as SubSummary,
|
||||||
|
ExistingBillMatch,
|
||||||
|
Recommendation,
|
||||||
|
SubscriptionTx as SubTx,
|
||||||
|
} from '@/types';
|
||||||
import type { DragProps, MoveControls } from '@/components/tracker/MobileTrackerRow';
|
import type { DragProps, MoveControls } from '@/components/tracker/MobileTrackerRow';
|
||||||
|
|
||||||
function fmt(v: number | null | undefined): string {
|
function fmt(v: number | null | undefined): string {
|
||||||
|
|
@ -93,102 +101,6 @@ interface CadenceLike {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Subscription {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
active?: boolean | number;
|
|
||||||
is_subscription?: boolean | number;
|
|
||||||
category_id?: number | null;
|
|
||||||
category_name?: string | null;
|
|
||||||
expected_amount?: number | null;
|
|
||||||
monthly_equivalent?: number | null;
|
|
||||||
yearly_equivalent?: number | null;
|
|
||||||
next_due_date?: string | null;
|
|
||||||
subscription_type?: string;
|
|
||||||
due_day?: number | null;
|
|
||||||
cycle_type?: string | null;
|
|
||||||
billing_cycle?: string | null;
|
|
||||||
cycle_day?: number | string | null;
|
|
||||||
autopay_enabled?: unknown;
|
|
||||||
has_2fa?: unknown;
|
|
||||||
has_merchant_rule?: unknown;
|
|
||||||
has_linked_transactions?: unknown;
|
|
||||||
reminder_days_before?: number | null;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TopType { type: string; monthly_total: number }
|
|
||||||
interface SubSummary {
|
|
||||||
active_count?: number;
|
|
||||||
paused_count?: number;
|
|
||||||
monthly_total?: number;
|
|
||||||
yearly_total?: number;
|
|
||||||
top_type?: TopType | null;
|
|
||||||
}
|
|
||||||
interface SubData { summary?: SubSummary; subscriptions?: Subscription[] }
|
|
||||||
|
|
||||||
interface CatalogMatch {
|
|
||||||
id?: number | string;
|
|
||||||
name: string;
|
|
||||||
subscription_type?: string;
|
|
||||||
website?: string;
|
|
||||||
starting_monthly_usd?: number | null;
|
|
||||||
starting_annual_usd?: number | null;
|
|
||||||
}
|
|
||||||
interface ExistingBillMatch {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
expected_amount?: number | null;
|
|
||||||
due_day?: number | null;
|
|
||||||
reasons?: string[];
|
|
||||||
}
|
|
||||||
interface RecEvidence {
|
|
||||||
identity?: { label?: string };
|
|
||||||
amount?: { label?: string; match?: string };
|
|
||||||
cadence?: { label?: string; recurring?: boolean };
|
|
||||||
amount_range?: { min: number; max: number };
|
|
||||||
ambiguity?: { ambiguous?: boolean; label?: string; reasons?: string[] };
|
|
||||||
}
|
|
||||||
interface RecTransaction { id: number; payee?: string; description?: string; memo?: string; date?: string; account?: string; amount?: number }
|
|
||||||
interface Recommendation {
|
|
||||||
id: number | string;
|
|
||||||
name: string;
|
|
||||||
subscription_type?: string;
|
|
||||||
occurrence_count?: number;
|
|
||||||
last_seen_date?: string;
|
|
||||||
expected_amount?: number | null;
|
|
||||||
monthly_equivalent?: number | null;
|
|
||||||
confidence?: number;
|
|
||||||
cycle_type?: string;
|
|
||||||
merchant?: string;
|
|
||||||
decline_key?: string;
|
|
||||||
transaction_ids?: (number | string)[];
|
|
||||||
accounts?: string[];
|
|
||||||
reasons?: string[];
|
|
||||||
evidence?: RecEvidence;
|
|
||||||
existing_bill_match?: ExistingBillMatch | null;
|
|
||||||
catalog_match?: CatalogMatch | null;
|
|
||||||
transactions?: RecTransaction[];
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SubTx {
|
|
||||||
id: number;
|
|
||||||
amount: number;
|
|
||||||
payee?: string;
|
|
||||||
description?: string;
|
|
||||||
memo?: string;
|
|
||||||
account_name?: string;
|
|
||||||
data_source_name?: string;
|
|
||||||
account?: string;
|
|
||||||
match_status?: string;
|
|
||||||
matched_bill_name?: string;
|
|
||||||
catalog_match?: CatalogMatch | null;
|
|
||||||
posted_date?: string;
|
|
||||||
date?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SubDraft {
|
interface SubDraft {
|
||||||
name?: string;
|
name?: string;
|
||||||
category_id?: number | null;
|
category_id?: number | null;
|
||||||
|
|
@ -1200,9 +1112,9 @@ type FlatItem =
|
||||||
export default function SubscriptionsPage() {
|
export default function SubscriptionsPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: subData, isPending: loading } = useSubscriptions();
|
const { data: subData, isPending: loading } = useSubscriptions();
|
||||||
const data: SubData = (subData as SubData | undefined) || { summary: {}, subscriptions: [] };
|
const data: SubData = subData || { summary: {}, subscriptions: [] };
|
||||||
const { data: recommendationsRaw = [], isPending: recommendationsLoading } = useSubscriptionRecommendations();
|
const { data: recommendationsRaw = [], isPending: recommendationsLoading } = useSubscriptionRecommendations();
|
||||||
const recommendations = recommendationsRaw as Recommendation[];
|
const recommendations = recommendationsRaw;
|
||||||
const { data: categories = [] } = useCategories();
|
const { data: categories = [] } = useCategories();
|
||||||
const { data: bills = [] } = useBills();
|
const { data: bills = [] } = useBills();
|
||||||
// Optimistic updates write through the query cache so every existing call site
|
// Optimistic updates write through the query cache so every existing call site
|
||||||
|
|
@ -1270,7 +1182,7 @@ export default function SubscriptionsPage() {
|
||||||
txDebounce.current = setTimeout(async () => {
|
txDebounce.current = setTimeout(async () => {
|
||||||
setTxSearching(true);
|
setTxSearching(true);
|
||||||
try {
|
try {
|
||||||
const result = await api.subscriptionTransactionMatches({ q, limit: 50 }) as SubTx[] | { transactions?: SubTx[] };
|
const result = await api.subscriptionTransactionMatches({ q, limit: 50 });
|
||||||
setTxResults(Array.isArray(result) ? result : (result?.transactions ?? []));
|
setTxResults(Array.isArray(result) ? result : (result?.transactions ?? []));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setTxResults([]);
|
setTxResults([]);
|
||||||
|
|
@ -1300,7 +1212,7 @@ export default function SubscriptionsPage() {
|
||||||
async function acceptRecommendation(recommendation: Recommendation) {
|
async function acceptRecommendation(recommendation: Recommendation) {
|
||||||
setBusyId(`rec-${recommendation.id}`);
|
setBusyId(`rec-${recommendation.id}`);
|
||||||
try {
|
try {
|
||||||
const created = await api.createSubscriptionFromRecommendation(recommendation) as { id?: number; name?: string } | null;
|
const created = await api.createSubscriptionFromRecommendation(recommendation);
|
||||||
toast.success(`${recommendation.name} is now tracked.`);
|
toast.success(`${recommendation.name} is now tracked.`);
|
||||||
await refreshAll();
|
await refreshAll();
|
||||||
if (getLinkImportPref() && recommendation.merchant && created?.id) {
|
if (getLinkImportPref() && recommendation.merchant && created?.id) {
|
||||||
|
|
@ -1336,7 +1248,7 @@ export default function SubscriptionsPage() {
|
||||||
recommendation.merchant,
|
recommendation.merchant,
|
||||||
recommendation.catalog_match?.id,
|
recommendation.catalog_match?.id,
|
||||||
recommendation.confidence,
|
recommendation.confidence,
|
||||||
) as { matched_count?: number; bill_name?: string };
|
);
|
||||||
toast.success(`Linked ${result.matched_count} transaction${result.matched_count !== 1 ? 's' : ''} to "${result.bill_name}".`);
|
toast.success(`Linked ${result.matched_count} transaction${result.matched_count !== 1 ? 's' : ''} to "${result.bill_name}".`);
|
||||||
setMatchTarget(null);
|
setMatchTarget(null);
|
||||||
setRecommendations(prev => prev.filter(r => r.id !== recommendation.id));
|
setRecommendations(prev => prev.filter(r => r.id !== recommendation.id));
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,105 @@ export interface SpendingRule { id: number; merchant: string; category_name?: st
|
||||||
export interface CategoryGroup { id: number; name: string }
|
export interface CategoryGroup { id: number; name: string }
|
||||||
export interface CopyBudgetsResponse { copied: number; budgets?: { category_id: number; amount: number }[] }
|
export interface CopyBudgetsResponse { copied: number; budgets?: { category_id: number; amount: number }[] }
|
||||||
|
|
||||||
|
// The subscriptions page (GET /subscriptions, /subscriptions/recommendations,
|
||||||
|
// /subscriptions/transaction-matches). A Subscription is a decorated Bill;
|
||||||
|
// amounts here are dollars kept as plain `number` (the page uses a local
|
||||||
|
// formatter), except SubTx.amount which is a raw bank amount in CENTS.
|
||||||
|
export interface Subscription {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
active?: boolean | number;
|
||||||
|
is_subscription?: boolean | number;
|
||||||
|
category_id?: number | null;
|
||||||
|
category_name?: string | null;
|
||||||
|
expected_amount?: number | null;
|
||||||
|
monthly_equivalent?: number | null;
|
||||||
|
yearly_equivalent?: number | null;
|
||||||
|
next_due_date?: string | null;
|
||||||
|
subscription_type?: string;
|
||||||
|
due_day?: number | null;
|
||||||
|
cycle_type?: string | null;
|
||||||
|
billing_cycle?: string | null;
|
||||||
|
cycle_day?: number | string | null;
|
||||||
|
autopay_enabled?: unknown;
|
||||||
|
has_2fa?: unknown;
|
||||||
|
has_merchant_rule?: unknown;
|
||||||
|
has_linked_transactions?: unknown;
|
||||||
|
reminder_days_before?: number | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
export interface SubscriptionTopType { type: string; monthly_total: number }
|
||||||
|
export interface SubscriptionSummary {
|
||||||
|
active_count?: number;
|
||||||
|
paused_count?: number;
|
||||||
|
monthly_total?: number;
|
||||||
|
yearly_total?: number;
|
||||||
|
top_type?: SubscriptionTopType | null;
|
||||||
|
}
|
||||||
|
export interface SubscriptionsResponse { summary?: SubscriptionSummary; subscriptions?: Subscription[] }
|
||||||
|
|
||||||
|
export interface CatalogMatch {
|
||||||
|
id?: number | string;
|
||||||
|
name: string;
|
||||||
|
subscription_type?: string;
|
||||||
|
website?: string;
|
||||||
|
starting_monthly_usd?: number | null;
|
||||||
|
starting_annual_usd?: number | null;
|
||||||
|
}
|
||||||
|
export interface ExistingBillMatch {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
expected_amount?: number | null;
|
||||||
|
due_day?: number | null;
|
||||||
|
reasons?: string[];
|
||||||
|
}
|
||||||
|
export interface RecommendationEvidence {
|
||||||
|
identity?: { label?: string };
|
||||||
|
amount?: { label?: string; match?: string };
|
||||||
|
cadence?: { label?: string; recurring?: boolean };
|
||||||
|
amount_range?: { min: number; max: number };
|
||||||
|
ambiguity?: { ambiguous?: boolean; label?: string; reasons?: string[] };
|
||||||
|
}
|
||||||
|
export interface RecommendationTransaction { id: number; payee?: string; description?: string; memo?: string; date?: string; account?: string; amount?: number }
|
||||||
|
export interface Recommendation {
|
||||||
|
id: number | string;
|
||||||
|
name: string;
|
||||||
|
subscription_type?: string;
|
||||||
|
occurrence_count?: number;
|
||||||
|
last_seen_date?: string;
|
||||||
|
expected_amount?: number | null;
|
||||||
|
monthly_equivalent?: number | null;
|
||||||
|
confidence?: number;
|
||||||
|
cycle_type?: string;
|
||||||
|
merchant?: string;
|
||||||
|
decline_key?: string;
|
||||||
|
transaction_ids?: (number | string)[];
|
||||||
|
accounts?: string[];
|
||||||
|
reasons?: string[];
|
||||||
|
evidence?: RecommendationEvidence;
|
||||||
|
existing_bill_match?: ExistingBillMatch | null;
|
||||||
|
catalog_match?: CatalogMatch | null;
|
||||||
|
transactions?: RecommendationTransaction[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
// A raw bank transaction on the subscription-match search — amount in CENTS.
|
||||||
|
export interface SubscriptionTx {
|
||||||
|
id: number;
|
||||||
|
amount: number;
|
||||||
|
payee?: string;
|
||||||
|
description?: string;
|
||||||
|
memo?: string;
|
||||||
|
account_name?: string;
|
||||||
|
data_source_name?: string;
|
||||||
|
account?: string;
|
||||||
|
match_status?: string;
|
||||||
|
matched_bill_name?: string;
|
||||||
|
catalog_match?: CatalogMatch | null;
|
||||||
|
posted_date?: string;
|
||||||
|
date?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
// A bill's month status. Mirrors the server's statusService.
|
// A bill's month status. Mirrors the server's statusService.
|
||||||
export type TrackerStatus =
|
export type TrackerStatus =
|
||||||
| 'paid' | 'autodraft' | 'upcoming' | 'due_soon' | 'late' | 'missed' | 'skipped';
|
| 'paid' | 'autodraft' | 'upcoming' | 'due_soon' | 'late' | 'missed' | 'skipped';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue