20925f6c65
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>
119 lines
4.3 KiB
TypeScript
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,
|
|
}
|
|
}
|