diff --git a/app/(dashboard)/invoices/[id]/page.tsx b/app/(dashboard)/invoices/[id]/page.tsx index ba431916..20380e7c 100644 --- a/app/(dashboard)/invoices/[id]/page.tsx +++ b/app/(dashboard)/invoices/[id]/page.tsx @@ -12,7 +12,7 @@ import { Separator } from '@/components/ui/separator' import { useToast } from '@/components/ui/use-toast' import { formatCurrency, formatDate, cn } from '@/lib/utils' import { getVatTreatmentLabel } from '@/lib/invoices/vat-rules' -import { invoiceNumberDisplay } from '@/lib/invoices/display' +import { invoiceNumberDisplay, invoiceDisplayNumber } from '@/lib/invoices/display' import { getDisplayTotal } from '@/lib/invoices/rounding' import { Loader2, @@ -427,6 +427,9 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st const isProforma = docType === 'proforma' const isDeliveryNote = docType === 'delivery_note' const isRealInvoice = docType === 'invoice' + // Self-billing invoices we received: the document is the counterparty's, so + // there is no own PDF to render and no send step — it arrives already booked. + const isSelfBilled = !!invoice.is_self_billed return (
{t('bank_missing_warning')}
@@ -635,8 +725,8 @@ export default function NewInvoicePage() { {/* Customer selection */}{errors.external_invoice_number.message}
+ )} +{errors.received_date.message}
+ )} +Inställningar → Import/Export → Exportera SIE
Visma eEkonomi
+Visma
Rapporter → Övrigt → Exportera till SIE
{role.legalName}
-- {cleaned} · {entityLabel} -
-- {t('ask_admin_invite')} -
-+ {t('already_in_app', { appName: branding.appName.toLowerCase() })} +
+ ) : null if (!mappable) { return ( @@ -236,6 +224,7 @@ export default function BankIdCompanyPicker({ {t('setup_manually')}{lookupError}
)} - {orgNumberExists && ( -{t('booking_unbalanced', { - diff: formatCurrency(Math.abs(editValidation.diff), transaction.currency), + diff: formatCurrency(Math.abs(editValidation.diff), 'SEK'), })}
)} diff --git a/extensions/general/arcim-migration/__tests__/migrate-guard.test.ts b/extensions/general/arcim-migration/__tests__/migrate-guard.test.ts new file mode 100644 index 00000000..f9368bc8 --- /dev/null +++ b/extensions/general/arcim-migration/__tests__/migrate-guard.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest' +import { createMockSupabase, createMockRequest, parseJsonResponse } from '@/tests/helpers' +import type { ExtensionContext } from '@/lib/extensions/types' + +/** + * Guards the server-side "SIE import required first" rule on the entity-migration + * route (POST /migrate). + * + * Provider API import only ever writes subledger entities (customers, suppliers, + * invoices) — it never posts to the general ledger. The GL (kontoplan, ingående + * balanser, verifikationer) arrives via SIE. Importing entities without the + * SIE-derived ledger leaves an incomplete bokföring under BFL, so the route MUST + * refuse to run for non-Fortnox providers until a completed SIE import exists. + * Fortnox is exempt because it pulls SIE itself via API. + * + * Previously this was only an advisory banner + step-gating in the React wizard, + * which a direct API call or a stale client could bypass. This test locks the + * enforcement at the authoritative seam: the route handler. + */ + +vi.mock('../lib/migration-orchestrator', () => ({ + executeMigration: vi.fn().mockResolvedValue({ customers: { total: 0, imported: 0, skipped: 0 } }), +})) + +// index.ts imports many helpers from provider-client at module load; stub the +// whole module and give getConsent/acceptConsent controllable behaviour. +vi.mock('../lib/provider-client', () => ({ + createConsent: vi.fn(), + getConsent: vi.fn(), + listConsents: vi.fn(), + generateOtc: vi.fn(), + getAuthUrl: vi.fn(), + exchangeAuthToken: vi.fn(), + submitProviderToken: vi.fn(), + acceptConsent: vi.fn().mockResolvedValue(undefined), + deleteConsent: vi.fn(), + resolveConsent: vi.fn(), + fetchCompanyInfoDirect: vi.fn(), +})) + +import { arcimMigrationExtension } from '../index' +import { executeMigration } from '../lib/migration-orchestrator' +import { getConsent } from '../lib/provider-client' + +const migrateRoute = (arcimMigrationExtension.apiRoutes ?? []).find( + (r) => r.method === 'POST' && r.path === '/migrate', +)! + +type RouteHandler = (request: Request, ctx?: ExtensionContext) => Promise