feat(subscriptions): cash runway per card, price changes, stale bills, unlinked payments
The page opens with how much each paying card needs by a date and where it falls short, a 30-day calendar per card and the annual renewals ahead. The table shows price changes, the paying card, stale subscriptions to deactivate in place and payments that look like a subscription, L links them.
This commit is contained in:
1 parent
153a8cb191
commit
68f64893f7
10 files changed
+1425
-105
No files matched your search
@@ -0,0 +1,46 @@
|
||||
import { api } from "#lib/api/client.js";
|
||||
import { all } from "#lib/api/lists.js";
|
||||
import type { Bill, Item, Page, TransactionGroup } from "#lib/api/types.js";
|
||||
import { addDays } from "#lib/dates.js";
|
||||
import { toPayments } from "./runway.js";
|
||||
|
||||
const SEARCH_LIMIT = 500;
|
||||
const UNLINKED_DAYS = 92;
|
||||
|
||||
async function search(query: string): Promise<Item<TransactionGroup>[]> {
|
||||
const read = (page: number) =>
|
||||
api<Page<TransactionGroup>>("/v1/search/transactions", {
|
||||
query: { limit: SEARCH_LIMIT, page, query },
|
||||
});
|
||||
const first = await read(1);
|
||||
const pages = first.meta.pagination?.total_pages ?? 1;
|
||||
const rest = await Promise.all(
|
||||
Array.from({ length: Math.max(0, pages - 1) }, (_, i) => read(i + 2))
|
||||
);
|
||||
const seen = new Map<string, Item<TransactionGroup>>();
|
||||
for (const group of [first, ...rest].flatMap((page) => page.data)) {
|
||||
seen.set(group.id, group);
|
||||
}
|
||||
return [...seen.values()];
|
||||
}
|
||||
|
||||
export const runwayQueries = {
|
||||
bills: (start: string, end: string) => ({
|
||||
queryFn: () => all<Bill>("/v1/bills", { end, start }),
|
||||
queryKey: ["subscriptions", "runway", start, end],
|
||||
}),
|
||||
paid: () => ({
|
||||
queryFn: async () => toPayments(await search("has_any_bill:true")),
|
||||
queryKey: ["subscriptions", "payments"],
|
||||
}),
|
||||
unlinked: (day: string) => {
|
||||
const after = addDays(day, -UNLINKED_DAYS);
|
||||
return {
|
||||
queryFn: async () =>
|
||||
toPayments(
|
||||
await search(`has_no_bill:true type:withdrawal date_after:${after}`)
|
||||
).filter((payment) => !payment.billId),
|
||||
queryKey: ["subscriptions", "unlinked", after],
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user