'use client' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@/components/ui/dialog' import { formatCurrency, formatDate } from '@/lib/utils' import { roundOre } from '@/lib/money' import { expectedRotRutPayoutAmount, getRotRutPayoutMatchTargetState, } from '@/lib/invoices/rot-rut-payout-matching' import { CheckCircle2, AlertTriangle } from 'lucide-react' import type { TransactionWithInvoice } from './transaction-types' interface RotRutPayoutMatchDialogProps { open: boolean onOpenChange: (open: boolean) => void /** Row carrying `potential_rot_rut_payout`; the dialog renders nothing without it. */ transaction: TransactionWithInvoice | null isConfirming: boolean onConfirm: () => void } /** * Confirm dialog for matching an income bank row to one or several open * ROT/RUT begäran: Skatteverkets utbetalning clears the 1513 receivable * (debit the row's cash account, one 1513 credit per begäran) and the row is * linked to that voucher. * * Kept separate from InvoiceMatchDialog on purpose: no FX, no preview fetch, * no editable lines. The legs are known up front, so everything the user * needs to approve is on screen. A bundle (several begäran paid in one * transfer) is booked at exactly the decided sums: partial and over * variants exist only for a single begäran. */ export default function RotRutPayoutMatchDialog({ open, onOpenChange, transaction, isConfirming, onConfirm, }: RotRutPayoutMatchDialogProps) { const t = useTranslations('tx_rot_rut_match') const requests = transaction?.potential_rot_rut_payout?.requests ?? [] const isSet = requests.length > 1 const single = requests.length === 1 ? requests[0] : null // Every begäran must still be open and unsettled; the first blocked one // explains the refusal. const blockedState = requests.map((request) => getRotRutPayoutMatchTargetState(request)).find((s) => s !== 'matchable') ?? null const targetBlocked = requests.length === 0 || blockedState !== null const txAmount = transaction ? roundOre(transaction.amount) : 0 const expected = roundOre( requests.reduce((sum, request) => sum + expectedRotRutPayoutAmount(request), 0), ) const requestedTotal = roundOre( requests.reduce((sum, request) => sum + roundOre(Number(request.requested_total)), 0), ) const diff = roundOre(Math.abs(txAmount - expected)) const amountsMatch = diff < 0.01 // The settle service refuses a payout below requested_total unless the // beslut (decided_total) is recorded: say so here instead of letting the // button fail. const isPartial = single ? txAmount < requestedTotal - 0.005 : false const partialBlocked = isPartial && single?.decided_total == null // The service refuses more than Skatteverket can owe on this begäran: a // larger row would drive 1513 negative. Block here too, with the reason. const overBlocked = single ? txAmount > expected + 0.005 : false // A bundle only ever books the exact sum of its begäran. const setBlocked = isSet && !amountsMatch // Skatteverket pays out in SEK only; the route refuses anything else. const currencyBlocked = (transaction?.currency || 'SEK').toUpperCase() !== 'SEK' const currency = transaction?.currency || 'SEK' const typeLabel = (deductionType: 'rot' | 'rut') => (deductionType === 'rut' ? 'RUT' : 'ROT') return ( {t('title')} {targetBlocked ? t('description_blocked') : t('description')} {transaction && requests.length > 0 && (

{t('transaction_label')}

{transaction.description}

{formatDate(transaction.date)} +{formatCurrency(transaction.amount, currency)}

{isSet ? t('requests_label', { count: requests.length }) : t('request_label')}

{requests.map((request) => (

{t('request_name', { type: typeLabel(request.deduction_type), name: request.name })}

{t('requested_total', { amount: formatCurrency(roundOre(Number(request.requested_total)), 'SEK'), })} {request.decided_total != null && ( {t('decided_total', { amount: formatCurrency(Number(request.decided_total), 'SEK'), })} )}
{request.invoices.length > 0 && (

{t('invoices_title')}

    {request.invoices.map((inv, i) => (
  • {t('invoice_row', { number: inv.invoice_number ?? '' })} {formatCurrency(Number(inv.requested_amount), 'SEK')}
  • ))}
)}
))} {isSet && (
{t('requests_total_label')} {formatCurrency(expected, 'SEK')}
)}
{targetBlocked ? (

{t(blockedState === 'settled' ? 'target_settled_title' : 'target_not_open_title')}

{t( blockedState === 'settled' ? 'target_settled_description' : 'target_not_open_description', )}

) : amountsMatch ? (

{isSet ? t('amounts_match_set') : t('amounts_match')}

) : (

{isSet ? t('amounts_differ_set') : t('amounts_differ')}

{t('amount_diff', { amount: formatCurrency(diff, currency) })}

{setBlocked &&

{t('set_exact_required')}

} {overBlocked &&

{t('over_payout_blocked')}

} {partialBlocked &&

{t('partial_requires_beslut')}

} {isPartial && !partialBlocked && (

{t('partial_with_beslut_note')}

)}
)} {!targetBlocked && (

{t('booking_title')}

{t('booking_debit')}{' '} {t('booking_bank_line')} {formatCurrency(txAmount, currency)}
{isSet ? ( requests.map((request) => (
{t('booking_credit')}{' '} {t('booking_receivable_line_named', { name: request.name })} {formatCurrency(expectedRotRutPayoutAmount(request), currency)}
)) ) : (
{t('booking_credit')}{' '} {t('booking_receivable_line')} {formatCurrency(txAmount, currency)}
)}
)} {!targetBlocked && (

{t('on_confirm_title')}

  • • {t('on_confirm_link')}
  • • {isSet ? t('on_confirm_requests', { count: requests.length }) : t('on_confirm_request')}
  • • {t('on_confirm_voucher')}
)}
)}
) }