Files
Jakob Wennberg 20925f6c65 feat(worklist): the next Skatteverket payment with bankgiro, OCR and due date under Att göra on Hem (#2435)
Part (a) of #2187. A twelfth worklist category, skattekonto_payment_due:
the earliest upcoming skattekonto charge whose sum exceeds the last synced
saldo, computed once in lib/worklist (server-side twin of the /skattekonto
page's Nästa dragning math) and rendered as one Betala row on Hem with the
shortfall, bankgiro 5050-1055, the OCR reference and the due date.

The row appears only when money has to move: a saldo that covers the charge
yields nothing, and no upcoming charge yields nothing. Ignored rows take
part, since Skatteverket draws them regardless of our flag. Without a
balance snapshot the full charge is the amount. Without an org number the
row keeps its bankgiro and date and drops the OCR.

No table, route or migration: /api/worklist/counts picks the category up
through getWorklistCounts, and Hem passes the computed row into the same
options wave as the expense payouts.

Part (b), a betalfil for the skattekonto payment, stays a follow-up: the
existing payment-file route is AGI-scoped.


Claude-Session: https://claude.ai/code/session_0179bdetHyofL6ATfQxB5wP5

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 21:25:01 +02:00

119 lines
4.3 KiB
TypeScript

import type { SupabaseClient } from '@supabase/supabase-js'
import type { ExpensePayoutDue, SkattekontoPaymentDue, SuggestedMatch } from './types'
import type { WorklistCounts } from './types'
import {
countDeadlinesNeedingAction,
countExpensePayoutsDue,
countInboxDocuments,
countOverdueInvoices,
countPendingOperations,
countReconciliationDue,
countSkattekontoPaymentDue,
countSuggestedMatches,
countSupplierInvoicesAwaitingApproval,
countUnbookedSkattekontoRows,
countUnbookedTransactions,
countVerifikatMissingDocument,
} from './categories'
/**
* All worklist counts in one round-trip burst. Each count is a bounded query
* (mostly head-only; suggested_match revalidates its candidates, see
* categories.ts) and individually soft-fails to 0, so this is safe to call
* from layouts and server components on every render.
*
* `total` is the number of distinct actionable items: suggested_match is a
* fast path over transactions already counted in book_transaction, so it is
* excluded to avoid double-counting (see lib/worklist/types.ts).
*/
export interface GetWorklistCountsOptions {
/**
* Suggested matches the caller is already fetching (Hem renders them in
* the Att göra pane): the count is taken from this list instead of a
* second scan of the same rows. A promise is accepted so it can run in
* parallel with the other counts.
*/
suggestedMatches?: SuggestedMatch[] | Promise<SuggestedMatch[]>
/**
* People owed for unpaid utlägg the caller is already fetching (Hem
* renders one row per person): the count is the list's length instead of
* a second scan of expense_claims.
*/
expensePayoutsDue?: ExpensePayoutDue[] | Promise<ExpensePayoutDue[]>
/**
* The next uncovered skattekonto charge the caller is already fetching
* (Hem renders it as one Betala row): the count is 1 or 0 from that value
* instead of a second scan. Pass null for "nothing to pay in".
*/
skattekontoPaymentDue?: SkattekontoPaymentDue | null | Promise<SkattekontoPaymentDue | null>
}
export async function getWorklistCounts(
supabase: SupabaseClient,
companyId: string,
options: GetWorklistCountsOptions = {},
): Promise<WorklistCounts> {
const [
bookTransaction,
bookSkattekonto,
inboxDocument,
suggestedMatch,
supplierInvoiceApproval,
verifikatMissingDocument,
overdueInvoice,
deadlineAction,
pendingOperations,
reconciliationDue,
expensePayout,
skattekontoPaymentDue,
] = await Promise.all([
countUnbookedTransactions(supabase, companyId),
countUnbookedSkattekontoRows(supabase, companyId),
countInboxDocuments(supabase, companyId),
options.suggestedMatches
? Promise.resolve(options.suggestedMatches).then((m) => m.length)
: countSuggestedMatches(supabase, companyId),
countSupplierInvoicesAwaitingApproval(supabase, companyId),
countVerifikatMissingDocument(supabase, companyId),
countOverdueInvoices(supabase, companyId),
countDeadlinesNeedingAction(supabase, companyId),
countPendingOperations(supabase, companyId),
countReconciliationDue(supabase, companyId),
options.expensePayoutsDue
? Promise.resolve(options.expensePayoutsDue).then((p) => p.length)
: countExpensePayoutsDue(supabase, companyId),
options.skattekontoPaymentDue !== undefined
? Promise.resolve(options.skattekontoPaymentDue).then((p) => (p ? 1 : 0))
: countSkattekontoPaymentDue(supabase, companyId),
])
return {
counts: {
book_transaction: bookTransaction,
book_skattekonto: bookSkattekonto,
inbox_document: inboxDocument,
suggested_match: suggestedMatch,
supplier_invoice_approval: supplierInvoiceApproval,
verifikat_missing_document: verifikatMissingDocument,
overdue_invoice: overdueInvoice,
deadline_action: deadlineAction,
pending_operations: pendingOperations,
reconciliation_due: reconciliationDue,
expense_payout: expensePayout,
skattekonto_payment_due: skattekontoPaymentDue,
},
total:
bookTransaction +
bookSkattekonto +
inboxDocument +
supplierInvoiceApproval +
verifikatMissingDocument +
overdueInvoice +
deadlineAction +
pendingOperations +
reconciliationDue +
expensePayout +
skattekontoPaymentDue,
}
}