Files
accounted/docs/support/company-migration-reset.md
T
c0ecb34a2b fix(company): migration reset no longer blocks on existing vouchers, sequences, or invoices (#1977)
* fix(company): migration reset no longer blocks on existing vouchers, sequences, or invoices

The 2026-08-18 eligibility rule stopped the archive-and-replace reset before
the first journal entry, voucher sequence, or invoice. The reset deletes
nothing: the source stays a write-closed, downloadable retention container,
and the unchecked Radera foretag path already produced the same outcome
without any of those guards, so the blockers only led owners into a dead
end (Carrierstories, 2026-08-26). External-state blockers are unchanged.

Closes #1916

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(decisions): move the migration-reset entry to the end of the log

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 14:35:19 +02:00

265 lines
14 KiB
Markdown

# Company migration reset support runbook
This runbook covers the owner-only self-service flow for redoing a failed
company migration. The operation is deliberately an archive-and-replace reset.
It is not a deletion or an in-place rewrite.
## Legal and product boundary
Swedish Bookkeeping Act (1999:1078) 1 kap. 2 § defines räkenskapsinformation
broadly. It includes journal and ledger information, vouchers, supporting
systems information, important agreements, and other information needed to
understand the postings. Chapter 7 requires electronic accounting information
to remain durable, accessible, and preserved through the seventh year after
the relevant calendar year. BFNAR 2013:2 also requires treatment history to
show added postings and system changes that affect how accounting information
is processed.
Authoritative references:
- [Bokföringslag (1999:1078), especially 1 kap. 2 § and 7 kap. 1-2 §§](https://www.riksdagen.se/sv/dokument-och-lagar/dokument/svensk-forfattningssamling/bokforingslag-19991078_sfs-1999-1078/)
- [BFNAR 2013:2 Bokföring, especially points 2.17 and 9.16](https://www.bfn.se/wp-content/uploads/2020/06/bfnar13-2-grund.pdf)
Accounted cannot reliably prove that every provider-imported customer,
invoice, transaction, or document is disposable test data. Some provider
imports do not carry row-level provenance, and Accounted cannot observe every
filing made directly at Skatteverket or Bolagsverket. Therefore this feature
never deletes or rewrites source data.
The self-service boundary is intentionally narrow:
- The company must be active and no more than 30 days old.
- Sandbox companies are excluded because they have a separate disposable-data
cleanup lifecycle and must not become retained legal archives.
- The caller must be an owner, not merely an admin or member.
- No company lock date may exist, and no fiscal period may be locked, closed,
or marked closed in the previous bookkeeping system.
- Journal entries, voucher sequences, and customer or supplier invoices do not
block (since migration 20260826150000; the 2026-08-18 rule blocked on the
first of any of them). They stay unchanged in the retained source, which is
write-closed and downloadable from the replacement. The replacement starts a
fresh voucher namespace and continues the invoice and arrival-number
counters. The unchecked "Radera företag" path produced the same two
companies with none of the retention guards, so blocking here only routed
owners into a dead end.
- No bank connection may be pending or active. This prevents the service-role
sync job from writing new transactions into the retained source after reset.
- No SIE, bank-file, or tax-account-file import may be pending or processing.
- No commerce, Stripe, or Skatteverket connection, recurring invoice schedule,
or pending accrual installment may still be able to write in the background.
- No import, OCR, API operation, invoice delivery, payment sync, or messaging
worker may still be queued or processing for the company.
- No AGI upload, VAT declaration draft, lock or submission, ROT/RUT request,
or production annual report submission may exist. Persisted VAT and AGI
workflow state blocks even when an older direct action has no audit row.
Every generated ROT/RUT payout file blocks because upload and signing happen
outside Accounted before the local request can be marked submitted.
- The owner must separately attest that nothing was filed outside Accounted.
Accounted cannot verify that external fact. If the owner is unsure, support
must stop the reset and escalate instead of interpreting silence as consent.
- The owner must acknowledge that the source copy is retained, provide an
audit reason, and type the exact displayed company name.
If any condition fails, self-service returns `COMPANY_RESET_INELIGIBLE` and
makes no change. Support must not override the result with deletion SQL.
## What the reset does
`reset_company_for_migration` executes in one database transaction:
1. It proves the caller is an owner before taking tenant-wide locks, then locks
the source and repeats authorization, confirmation, and eligibility checks.
2. It archives the source company by setting `archived_at` and `archived_by`.
3. It creates a replacement company with the same legal identity, team, owner
and member roles, company settings, and invoice-number counters. Setup state
and the company bookkeeping lock date are cleared on the replacement.
4. It seeds a fresh chart of accounts and primary `1930` cash account.
5. It transfers provider migration consents, subscription state, and existing
capability grants. The automatically created fresh trial is removed before
the original grants move, so resetting cannot extend a trial.
6. It transfers the active inbound email address and any custom inbound domain
to the replacement, so future documents are routed to the active company.
7. It transfers unexpired pending member invitations. Accepted, revoked, and
expired invitation history stays on the source.
8. It switches active-company preferences to the replacement.
9. It writes an immutable `company_migration_resets` audit record and two
append-only `audit_log` records.
10. Database guards make the retained source's accounting and import rows
write-closed, including bank connections, payroll and AGI records, annual
report submissions, ROT/RUT requests, authority audit rows, and VAT or AGI
workflow rows from requests that were already waiting on external signing.
The transaction does not disable a trigger. It does not delete, detach,
renumber, recalculate, copy, or mutate any source bookkeeping record.
The following stay on the archived source company unchanged:
- SIE, bank-file, and tax-account-file import records
- bank transactions and expired or revoked bank connections
- fiscal periods and their lock or close state
- documents, hashes, version chains, and voucher links
- customers and suppliers
- journal entries, journal-entry lines, and voucher sequences
- customer and supplier invoices
- authority and general audit logs
- extension runtime state, including any non-blocking provider history
Their counts are recorded in `company_migration_resets.source_counts`.
Those tables remain covered by the immutable-source guards and audit counts as
defense in depth. The replacement company intentionally has no fiscal period,
journal entry, transaction, document, import record, or voucher sequence. The
new migration creates the applicable periods and its first sequence state.
Provider migration consent is transferred so the owner can start again from
the import workspace. Bank connections are not transferred. Pending or active
connections block the reset and must be disconnected first. Reconnect
deliberately after the replacement migration is verified. The active inbound
email address and custom inbound domain move to the replacement; already
received documents do not move.
## Support checks
Start with the request ID from the API response or browser network panel. The
routes log operations `company.migration-reset.preview`,
`company.migration-reset.execute`, and `company.migration-reset.archive`.
Use read-only checks only. Do not run a reset RPC on behalf of a customer, do
not run `scripts/clear-user-data.sql`, and do not disable retention or journal
enforcement triggers.
The retained source is hidden from normal company selection. A current owner
of the active replacement can use **Settings > Company > Previous migration >
Download archive**. That owner-only route verifies the immutable reset link,
current replacement ownership, and the source archive marker before using the
read-only archive exporter. It deliberately does not rely on retained-source
membership, which can change through team removal or account anonymization. It
never makes the source active. If documents push
the direct ZIP over the response limit, the owner can download the structured
data without documents and support must provide the complete document package
through an approved read-only export path. Never unarchive the source merely to
reuse ordinary write-capable screens.
The replacement represents the same legal entity, not a newly formed business.
Its `next_invoice_number` and `next_arrival_number` therefore continue from the
source settings whether or not invoice rows exist on the source.
Those counters can reflect an imported or previously allocated series, and
resetting them to 1 could reuse a number or conceal a gap. Do not manually reset
either counter as part of migration recovery. Escalate a suspected numbering
error for a separate, documented compliance review.
After deployment, the audit chain can be inspected read-only with:
```sql
select
id,
source_company_id,
replacement_company_id,
actor_id,
reason,
confirmation_snapshot,
source_counts,
created_at
from public.company_migration_resets
where source_company_id = '<source-company-id>'
or replacement_company_id = '<replacement-company-id>';
```
Confirm the source was retained and the replacement is active:
```sql
select id, name, org_number, archived_at, archived_by, created_at
from public.companies
where id in ('<source-company-id>', '<replacement-company-id>');
```
Compare source record counts without selecting personal or accounting content:
```sql
select
(select count(*) from public.journal_entries where company_id = '<source-company-id>') as journal_entries,
(select count(*)
from public.journal_entry_lines line
join public.journal_entries entry on entry.id = line.journal_entry_id
where entry.company_id = '<source-company-id>') as journal_entry_lines,
(select count(*) from public.transactions where company_id = '<source-company-id>') as transactions,
(select count(*) from public.document_attachments where company_id = '<source-company-id>') as documents,
(select count(*) from public.fiscal_periods where company_id = '<source-company-id>') as fiscal_periods,
(select count(*) from public.voucher_sequences where company_id = '<source-company-id>') as voucher_sequences;
```
The counts should match `company_migration_resets.source_counts`. A mismatch is
an incident requiring investigation. Do not repair it by editing the source.
## Common blocker interpretation
- `migration_window_expired`: the company is older than the self-service
recovery window. Escalate for a case-specific legal and accounting review.
- `sandbox_company`: use the existing sandbox cleanup lifecycle. Do not convert
disposable sandbox data into a retained migration archive.
- `locked_or_closed_periods`: a company lock date or finalized period exists.
Do not clear or unlock it to enable reset.
- Journal entries, voucher sequences, and invoices are reported in the retained
counts and no longer block (20260826150000). They stay on the source; never
delete, reverse, renumber, or detach any of them.
- `authority_submission_detected`: Accounted has evidence of an authority
interaction, including a persisted VAT draft, an AGI upload awaiting BankID
signing, or any generated ROT/RUT payout file. Do not reset even if the owner
believes it was a test without first establishing the authority environment
and legal status. Remove only a local VAT draft that never left Accounted,
and only through the product flow; never clear extension data, payout
requests, or audit rows manually. An AGI upload, generated payout file,
locked declaration, or possibly signed declaration requires legal escalation,
not cleanup.
- `live_bank_connections`: disconnect every pending or active bank connection
first. Do not bypass the blocker because the sync cron can import without an
interactive company session.
- `imports_in_progress`: wait until every import completes or fails. Never
change an import status manually to bypass this concurrency guard.
- `active_integrations_or_schedules`: disable the reported integrations and
automatic schedules first. Team-level grants and service credentials can
otherwise keep background writers active after the session switches company.
- `background_work_in_progress`: wait for every queued or processing import,
OCR, API, delivery, payment, or messaging job to reach a terminal state.
- `COMPANY_RESET_CONFIRMATION_MISMATCH`: confirm the user typed the displayed
name from `company_settings.company_name`, not a stale internal name.
## Deployment and rollback
The migration files are
`supabase/migrations/20260818084050_company_migration_reset.sql` and
`supabase/migrations/20260818141018_harden_company_migration_reset_eligibility.sql`
and
`supabase/migrations/20260818143004_close_migration_reset_archive_gaps.sql` and
`supabase/migrations/20260818224000_block_vat_state_migration_reset.sql` and
`supabase/migrations/20260818231500_block_external_filing_staging_state.sql` and
`supabase/migrations/20260826150000_allow_migration_reset_with_accounting_records.sql`.
Apply them only to the permitted `erpbase` staging branch through the normal
migration workflow, then deploy application code. Never deploy the UI/API
before all listed migrations exist.
Before production rollout:
1. Run the pg-real suite against an approved disposable test database.
2. Reconcile every remote migration version with the repository.
3. Verify anon has no execute privilege and authenticated has execute only on
the preview and execution RPCs. The archive route uses the audit table's
replacement-membership RLS policy and repeats owner checks with its
service-role export client.
4. Exercise the flow with synthetic data in an approved non-production
environment, including every blocker and a forced transaction failure.
5. Confirm monitoring captures the request ID and structured error code without
logging the typed company name or reason.
Application rollback can hide the UI and route, but it must not drop the audit
table or delete reset history. Database rollback is additive only: revoke new
RPC execution if necessary and ship a new migration. Never modify or remove the
applied migration file.
## Escalation boundary
Escalate to Emil and an accounting/legal reviewer when the owner cannot make
the external-filing attestation, the company is outside the 30-day window, a
period is closed or locked, or a known authority submission exists. A
live bank connection is operational, not a legal override case: disconnect it
before retrying. The safe fallback is to
retain the source company and perform no reset.