* fix(sandbox): lock what the sandbox cannot actually do Three surfaces in the sandbox advertised capability the sandbox blocks outright, or rendered a staged preview wrong. Skatteverket promo card: hidden for sandbox companies. The sandbox landing page tells users Skatteverket is off, and the authorize route 403s via guardSandbox, so the dashboard nudge was a dead end. Same precedent as TaxSettingsContent, which already hides its Skatteverket section on is_sandbox. Dokumentinkorg: locked with a state that says what the workspace does and sends the user to registration. Checked before the capability gate on purpose: the seed_trial trigger grants every new company (sandbox included) 30 days of every paid capability, so the existing paywall waved a demo company straight through. The CTA signs the anonymous session out first, mirroring SandboxBanner. Staged categorize_transaction preview: the seed wrote its kontering under the generic preview_lines key, but categorize_transaction is the one type with a dedicated preview component, and it reads `lines`. The card fell through to its legacy summary branch and rendered blank Debetkonto and Kreditkonto plus "NaN kr" from formatCurrency(undefined). The seeded blob now mirrors what gnubok_categorize_transaction stages, extracted into buildSandboxPendingOperations so both shapes are unit-testable. CategorizePreview also learns to read preview_lines and to show a missing amount as a gap, so a live 24h sandbox stops showing NaN before its data expires. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(sandbox): don't leave for /register when sign-out failed CodeRabbit review: the ExtensionSandboxLockState CTA ignored the signOut() result, so a failure routed to /register with the anonymous session still live, which registers INTO the sandbox instead of leaving it: exactly what the sign-out exists to prevent. Surface the failure and stay put so the user can retry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
102 lines
4.1 KiB
TypeScript
102 lines
4.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { roundOre } from '@/lib/money'
|
|
import { buildSandboxPendingOperations } from '../pending-operations'
|
|
|
|
const input = {
|
|
userId: 'user-1',
|
|
companyId: 'company-1',
|
|
inboxItemId: 'inbox-1',
|
|
supplierId: 'supplier-1',
|
|
invoiceDate: '2026-07-25',
|
|
dueDate: '2026-08-06',
|
|
transactionId: 'tx-1',
|
|
}
|
|
|
|
/** Sum of a kontering in the `lines` (account_number/debit_amount) spelling. */
|
|
function sums(lines: Array<{ debit_amount: number; credit_amount: number }>) {
|
|
return {
|
|
debit: lines.reduce((n, l) => n + l.debit_amount, 0),
|
|
credit: lines.reduce((n, l) => n + l.credit_amount, 0),
|
|
}
|
|
}
|
|
|
|
describe('sandbox pending-operation seed data', () => {
|
|
it('stages both demo operations under the RLS-required actor shape', () => {
|
|
const ops = buildSandboxPendingOperations(input)
|
|
|
|
expect(ops).toHaveLength(2)
|
|
// pending_operations_chat_insert is the only policy that lets a
|
|
// user-scoped client INSERT here, and it requires both fields.
|
|
expect(ops.every((op) => op.actor_type === 'agent_chat')).toBe(true)
|
|
expect(ops.every((op) => op.risk_level === 'low')).toBe(true)
|
|
expect(ops.every((op) => op.status === 'pending')).toBe(true)
|
|
expect(ops.every((op) => op.user_id === 'user-1' && op.company_id === 'company-1')).toBe(true)
|
|
})
|
|
|
|
it('threads the seeded row ids into the executor params', () => {
|
|
const [supplierInvoice, categorize] = buildSandboxPendingOperations(input)
|
|
|
|
expect(supplierInvoice.params).toMatchObject({
|
|
inbox_item_id: 'inbox-1',
|
|
supplier_id: 'supplier-1',
|
|
invoice_date: '2026-07-25',
|
|
due_date: '2026-08-06',
|
|
})
|
|
expect(categorize.params).toMatchObject({ transaction_id: 'tx-1' })
|
|
})
|
|
|
|
it('gives categorize_transaction the preview shape CategorizePreview reads', () => {
|
|
const categorize = buildSandboxPendingOperations(input).find(
|
|
(op) => op.operation_type === 'categorize_transaction',
|
|
)!
|
|
const preview = categorize.preview_data as {
|
|
amount?: unknown
|
|
debit_account?: unknown
|
|
credit_account?: unknown
|
|
lines?: Array<{ account_number: string; debit_amount: number; credit_amount: number }>
|
|
}
|
|
|
|
// The regression this guards: seeding only the generic `preview_lines` key
|
|
// dropped the card onto the legacy summary branch, which rendered blank
|
|
// accounts and "NaN kr" from formatCurrency(undefined).
|
|
expect(preview.lines).toBeDefined()
|
|
expect(preview.lines!.length).toBeGreaterThan(0)
|
|
expect(typeof preview.amount).toBe('number')
|
|
expect(Number.isFinite(preview.amount as number)).toBe(true)
|
|
expect(preview.debit_account).toBe('1930')
|
|
expect(preview.credit_account).toBe('3001')
|
|
})
|
|
|
|
it('previews a balanced verifikat for the 1 200 kr deposit', () => {
|
|
const categorize = buildSandboxPendingOperations(input).find(
|
|
(op) => op.operation_type === 'categorize_transaction',
|
|
)!
|
|
const preview = categorize.preview_data as {
|
|
amount: number
|
|
lines: Array<{ account_number: string; debit_amount: number; credit_amount: number }>
|
|
}
|
|
|
|
const { debit, credit } = sums(preview.lines)
|
|
expect(debit).toBe(credit)
|
|
// Gross on the bank line matches the summary amount the card headlines.
|
|
expect(debit).toBe(preview.amount)
|
|
expect(preview.lines.map((l) => l.account_number)).toEqual(['1930', '2611', '3001'])
|
|
})
|
|
|
|
it('keeps the supplier-invoice preview on the generic preview_lines shape', () => {
|
|
const supplierInvoice = buildSandboxPendingOperations(input).find(
|
|
(op) => op.operation_type === 'create_supplier_invoice_from_inbox',
|
|
)!
|
|
const preview = supplierInvoice.preview_data as {
|
|
preview_lines: Array<{ account: string; debit: number; credit: number }>
|
|
}
|
|
|
|
// No dedicated preview component for this type: GenericPreview renders a
|
|
// kontering under `preview_lines` in the account/debit/credit spelling.
|
|
expect(preview.preview_lines).toHaveLength(3)
|
|
const debit = preview.preview_lines.reduce((n, l) => n + l.debit, 0)
|
|
const credit = preview.preview_lines.reduce((n, l) => n + l.credit, 0)
|
|
expect(roundOre(debit)).toBe(roundOre(credit))
|
|
})
|
|
})
|