feat(onboarding): activation quick wins on the Hem checklist (#1461)
* feat(onboarding): activation quick wins on the Hem checklist First implementation slice of the approved activation concept (artifact de543d57, dev_docs/onboarding_activation_analysis.md §9): - New "Kvitton och underlag" checklist step, gated on the invoice-inbox extension like the Skatteverket step; done once the company has ever received an inbox item (email/WhatsApp/upload). Non-AI companies route to billing, matching the assistant step. - Personalized VAT line in the Skatteverket step: the company's real next momsdeklaration due date from the deadlines table, with an explicit "välj momsperiod" prompt when vat_registered is set but moms_period is null (that state silently generates zero VAT deadlines). - Truthful Att göra empty state: while the setup checklist is open and no journal entry is posted, the all-clear reads "Bokföringen är tom än" instead of a false "Allt klart!". - Activation funnel events (onboarding_setup_step_started / _completed / _dismissed) via posthog-js, mirroring the existing guarded capture pattern; sandbox never renders the block so no extra gate is needed. Pure helpers live in lib/onboarding/checklist.ts with tests; step numbering now adapts to both optional extensions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: record the receipts-signal and moms-period-guard decisions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(onboarding): review triage: error-safe emptyLedger, stale-state guard, copy - A failed posted-entries count no longer reads as an empty ledger. - Confirming a suggested match books an entry, so the empty-ledger copy retires for the rest of the session (postedSinceLoad). - 'Bokföringen är tom än så länge' reads naturally. 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>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
ddbe9b1379
commit
44cff5e5e4
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { checklistNumbers, vatDeadlineLine } from '../checklist'
|
||||
|
||||
describe('vatDeadlineLine', () => {
|
||||
it('returns null when the company is not VAT-registered', () => {
|
||||
expect(
|
||||
vatDeadlineLine({ vatRegistered: false, momsPeriod: 'quarterly', nextVatDueDate: '2026-11-12' })
|
||||
).toBeNull()
|
||||
expect(
|
||||
vatDeadlineLine({ vatRegistered: null, momsPeriod: null, nextVatDueDate: null })
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('flags the silent zero-deadline misconfiguration when moms_period is unset', () => {
|
||||
expect(
|
||||
vatDeadlineLine({ vatRegistered: true, momsPeriod: null, nextVatDueDate: null })
|
||||
).toEqual({ kind: 'missing_period' })
|
||||
// Even with a stray row, an unset period is still a misconfiguration to surface.
|
||||
expect(
|
||||
vatDeadlineLine({ vatRegistered: true, momsPeriod: undefined, nextVatDueDate: '2026-11-12' })
|
||||
).toEqual({ kind: 'missing_period' })
|
||||
})
|
||||
|
||||
it('returns the due date when registered with a period and an upcoming row', () => {
|
||||
expect(
|
||||
vatDeadlineLine({ vatRegistered: true, momsPeriod: 'quarterly', nextVatDueDate: '2026-11-12' })
|
||||
).toEqual({ kind: 'date', dueDate: '2026-11-12' })
|
||||
})
|
||||
|
||||
it('says nothing when a period is set but no upcoming row surfaced', () => {
|
||||
expect(
|
||||
vatDeadlineLine({ vatRegistered: true, momsPeriod: 'yearly', nextVatDueDate: null })
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('checklistNumbers', () => {
|
||||
it('numbers all five steps when both extensions are on', () => {
|
||||
expect(checklistNumbers({ hasSkatteverket: true, hasInbox: true })).toEqual({
|
||||
count: 5,
|
||||
skv: 3,
|
||||
receipts: 4,
|
||||
assistant: 5,
|
||||
})
|
||||
})
|
||||
|
||||
it('collapses to four steps without the inbox extension', () => {
|
||||
expect(checklistNumbers({ hasSkatteverket: true, hasInbox: false })).toEqual({
|
||||
count: 4,
|
||||
skv: 3,
|
||||
receipts: 4,
|
||||
assistant: 4,
|
||||
})
|
||||
})
|
||||
|
||||
it('collapses to four steps without the skatteverket extension', () => {
|
||||
expect(checklistNumbers({ hasSkatteverket: false, hasInbox: true })).toEqual({
|
||||
count: 4,
|
||||
skv: 3,
|
||||
receipts: 3,
|
||||
assistant: 4,
|
||||
})
|
||||
})
|
||||
|
||||
it('collapses to three steps with neither extension', () => {
|
||||
expect(checklistNumbers({ hasSkatteverket: false, hasInbox: false })).toEqual({
|
||||
count: 3,
|
||||
skv: 3,
|
||||
receipts: 3,
|
||||
assistant: 3,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { MomsPeriod } from '@/types'
|
||||
|
||||
/**
|
||||
* What the Skatteverket checklist step should say about VAT deadlines.
|
||||
*
|
||||
* - 'date': the company's next momsdeklaration due date is known; show it.
|
||||
* - 'missing_period': the company is VAT-registered but moms_period is unset,
|
||||
* which makes the deadline engine silently generate ZERO VAT deadlines
|
||||
* (lib/tax/deadline-config.ts conditions all require a concrete period).
|
||||
* An empty deadlines query in that state means misconfiguration, not
|
||||
* "no VAT duty", so the UI must prompt for the period instead of showing
|
||||
* nothing.
|
||||
* - null: not VAT-registered (no line), or VAT-registered with a period set
|
||||
* but no upcoming row surfaced (transient or horizon gap; say nothing
|
||||
* rather than guessing).
|
||||
*/
|
||||
export type VatDeadlineLine =
|
||||
| { kind: 'date'; dueDate: string }
|
||||
| { kind: 'missing_period' }
|
||||
| null
|
||||
|
||||
export function vatDeadlineLine(input: {
|
||||
vatRegistered: boolean | null | undefined
|
||||
momsPeriod: MomsPeriod | null | undefined
|
||||
nextVatDueDate: string | null | undefined
|
||||
}): VatDeadlineLine {
|
||||
if (!input.vatRegistered) return null
|
||||
if (!input.momsPeriod) return { kind: 'missing_period' }
|
||||
if (!input.nextVatDueDate) return null
|
||||
return { kind: 'date', dueDate: input.nextVatDueDate }
|
||||
}
|
||||
|
||||
/**
|
||||
* Display ordinals for the setup checklist steps. Books and bank are always
|
||||
* present; Skatteverket and the receipts/inbox step render only when their
|
||||
* extensions are enabled; the assistant step is always last. `count` drives
|
||||
* the "{count} steg så är bokföringen igång" title.
|
||||
*/
|
||||
export function checklistNumbers(gates: { hasSkatteverket: boolean; hasInbox: boolean }): {
|
||||
count: number
|
||||
skv: number
|
||||
receipts: number
|
||||
assistant: number
|
||||
} {
|
||||
const skv = 3
|
||||
const receipts = 3 + (gates.hasSkatteverket ? 1 : 0)
|
||||
const assistant = receipts + (gates.hasInbox ? 1 : 0)
|
||||
return { count: assistant, skv, receipts, assistant }
|
||||
}
|
||||
Reference in New Issue
Block a user