Files
accounted/extensions/general/stripe/lib/settings-actions.ts
T
f266c386f3 chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers (#2150)
* chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers

Remove 33 dead files, ~270 unreferenced exports/types, 13 dead i18n
namespaces and 4 unused dependencies; fold byte-identical helper copies
into one canonical home each (lib/utils chunk/sleep/utcDateStamp,
lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format,
lib/pdf/number-text, lib/browser/panel-request, lib/api/v1/body +
v1ValidationError rolled out to ~55 v1 routes, booking-template schemas).

No behaviour change: v1 bodies and status codes, MCP tool schemas, DB
writes and money math are untouched. Naive ore rounding was deliberately
not swapped for roundOre; see DECISIONS.md 2026-09-02 for the full list
of things left alone on purpose.

tsc, lint, 19588 unit tests and check:guards green; antipattern baseline
ratcheted (naive-ore-round 622 -> 620, hand-rolled-invariant 115 -> 113).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(transactions): import RawTransaction from @/types after the ingest re-export removal

CI's type ratchet (check:types, full tsconfig) caught the one test file
that still imported the type through lib/transactions/ingest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 11:51:16 +02:00

135 lines
5.6 KiB
TypeScript

/**
* The Stripe settings panel's server calls, each classified into exactly one
* outcome.
*
* Why this exists: the panel's click handlers were `try { ... } finally {
* setSyncing(false) }` with no `catch`. A fetch that rejected (offline, DNS,
* connection reset, a serverless instance that never answers) skipped every
* toast line and landed in `finally`, so the spinner started, stopped, and the
* user was told nothing at all: not that it failed, not to retry, not to check
* the connection.
*
* The classification lives here rather than inside the component because this
* repo has no component tests (vitest runs in `node`, scoped to `lib/` and
* `app/api/`): logic left in the panel is logic nothing can check.
*
* The failure union is `@/lib/browser/action-failure`'s, so the panel maps a
* failure through the same `failureDescription` as the salary payment panels and
* the three reasons mean the same thing everywhere in the app.
*
* Exactly one toast per click: TOAST_LIMIT is 1 (components/ui/use-toast.tsx),
* so a second toast emitted in the same tick evicts the first and only the last
* one is ever rendered. Every outcome below therefore resolves to one sentence.
*/
import {
panelRequest,
type PanelRequestOptions,
type PanelRequestResult,
} from '@/lib/browser/panel-request'
/**
* Deadline for the panel's quick calls: the status read, the connect handshake,
* disconnect, and the transaction-sync toggle. Each is a single indexed row plus
* at most one outward Stripe call, so the realistic worst case is a cold start
* and one eu-north-1 round trip. Same value as POST_ACTION_TIMEOUT_MS: a shorter
* deadline would report a failure for a write that may well have landed.
*/
export const STRIPE_ACTION_TIMEOUT_MS = 15_000
/**
* Deadline for "Synka nu".
*
* Deliberately far longer than the writes. A first sync backfills 90 days and up
* to 10 000 balance transactions (BACKFILL_DAYS / MAX_TXNS_PER_RUN in
* transaction-sync.ts), which legitimately takes minutes, and aborting early
* would be worse than the missing catch this replaces: the server keeps working
* and advances `last_balance_txn_synced_at`, so the user would be told the sync
* failed and then find nothing on the retry. The only honest bound is the
* route's own ceiling (`export const maxDuration = 300` in
* app/api/extensions/ext/[...path]/route.ts) plus a margin for the platform's
* own error response: past that, no answer is ever coming.
*/
export const STRIPE_SYNC_TIMEOUT_MS = 310_000
export type StripeRequestResult<T> = PanelRequestResult<T>
export type StripeRequestOptions = PanelRequestOptions
export { serverErrorMessage } from '@/lib/browser/panel-request'
/** Call one of the panel's endpoints and report exactly why it failed. */
export function stripeRequest<T>(options: StripeRequestOptions): Promise<StripeRequestResult<T>> {
return panelRequest<T>({ timeoutMs: STRIPE_ACTION_TIMEOUT_MS, ...options })
}
/** Success body of POST /api/extensions/ext/stripe/sync. */
export interface StripeSyncPayload {
success?: boolean
/** `StripeTransactionSyncSummary` from lib/transaction-sync.ts, over the wire. */
transactions?: {
fetched?: number
imported?: number
duplicates?: number
linked?: number
errors?: number
revoked?: boolean
/**
* Only set when the caller passes a time budget. The manual button's route
* passes none, so it cannot appear here; the nightly cron owns that case.
*/
deadlineReached?: boolean
}
}
/**
* A type alias, not an interface: next-intl's `t(key, values)` takes a
* `Record<string, string | number | Date>`, and only anonymous object types get
* the implicit index signature that makes them assignable to it.
*/
type SyncCounts = {
fetched: number
imported: number
linked: number
}
export type StripeSyncSummary =
/**
* Stripe revoked the connection upstream. A 200, but not a completed sync:
* `syncStripeBalanceTransactions` returns `{ revoked: true }` with every count
* at zero, which the panel used to report as a finished sync that "returned no
* transactions for the period", sending the user to check the wrong thing.
*/
| { reason: 'revoked' }
/** The window genuinely held nothing. A real answer, not a silent success. */
| { reason: 'empty' }
/** Rows landed, and some rows did not. Both halves get said. */
| { reason: 'errors'; values: SyncCounts & { errors: number } }
/** Rows landed. */
| { reason: 'feed'; values: SyncCounts }
/**
* 2xx whose body could not be read. The sync ran (the server had already
* committed by the time it flushed headers), but the counts are unknown, so
* the panel says the sync finished and stops there rather than reporting the
* missing numbers as zero and calling the run empty.
*/
| { reason: 'unknown' }
/**
* Turn the sync route's success body into the single sentence the user gets.
*/
export function syncSummary(payload: StripeSyncPayload | null): StripeSyncSummary {
const summary = payload?.transactions
if (!summary) return { reason: 'unknown' }
if (summary.revoked === true) return { reason: 'revoked' }
if (typeof summary.fetched !== 'number') return { reason: 'unknown' }
const fetched = summary.fetched
const imported = typeof summary.imported === 'number' ? summary.imported : 0
const linked = typeof summary.linked === 'number' ? summary.linked : 0
const errors = typeof summary.errors === 'number' ? summary.errors : 0
if (fetched === 0) return { reason: 'empty' }
if (errors > 0) return { reason: 'errors', values: { fetched, imported, linked, errors } }
return { reason: 'feed', values: { fetched, imported, linked } }
}