c90ccc9c429277299fd4ecec0652cd8b6fa080be
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fc2d78a7c4 |
feat(onboarding): the orgnr step suggests companies as you type (SCB search, TIC on the pick) (#2452)
* feat(onboarding): the orgnr step suggests companies as you type, SCB search, TIC on the pick Most people do not know their organisationsnummer. They left the onboarding for allabolag, searched their company name there, copied the number and pasted it back. #2421 let the field take a name, but only on Enter and behind a screen that still said "organisationsnummer", so the detour stayed. Now the field suggests companies while a name is typed (name, orgnr or "Enskild firma", city; arrow keys or click to pick), the pick fills the company like a typed orgnr, and the screen says "Vilket företag är det?" with "Företagsnamn eller organisationsnummer" as the placeholder. Why the problem occurred: the one identifier the step asked for is the one the user is least likely to remember, and the free-text path added in #2421 was invisible (copy unchanged) and had to be guessed (Enter only), because the only search index behind it was TIC, whose Lens budget cannot take a call per keystroke. What was removed or simplified: nothing is stored and no new state model: a picked suggestion is an ORG_SUBMITTED with prefill, so the existing LOOKUP_RESULT transitions (found, not found, disabled, error) decide the step exactly as for a typed number. SCB's name search already existed for the parties picker; it gained one option (sole traders) instead of a second client. No rate limiting anywhere, per the founder. Why this shape: SCB's företagsregister is free and already configured for the parties picker, so search-as-you-type costs nothing while typing; TIC runs once, on the pick, as it always did on Enter. TIC per keystroke was rejected (3000/month). SCB alone was rejected for the pick because it knows no F-skatt, VAT registration or fiscal year. The Enter path and the chip row from #2421 stay as the fallback when no row is picked. Sole traders are offered (they are half the users) but their row names the form and never prints the personnummer, and the field shows the company name after a pick for the same reason. Changes: - app/api/company/search: GET ?q= over the SCB client with sole traders included, top 6 rows plus a truncated flag; requireAuth() (no company yet), 400 for short or numeric q, 503 without SCB credentials, 502 when SCB does not answer. - lib/parties/scb/client.ts: searchByName(query, { includeSoleTraders }), legalFormCode on every candidate; the parties picker is unchanged. - lib/company-lookup: CompanySuggestion, COMPANY_SUGGEST_MAX, fetchCompanySuggestions (503 is disabled, everything else error, never throws), toCompanySuggestion (SCB legal form 49/10/61 into the TIC vocabulary mapSetupEntityType reads). - lib/onboarding-journey/reducer.ts: SUGGESTION_PICKED (orgnr, name and form as prefill, lookupPending; lookupRan stays false until TIC answers). - components/onboarding/journey: 300 ms debounced SCB search with abort of the superseded request, listbox under the field (combobox ARIA, arrow keys, Escape, Enter picks the highlighted row, otherwise the Enter path), copy switches with companySearchEnabled or ticEnabled; both journey pages pass isScbConfigured(). - messages sv+en: five strings. Tests: route (401, 400 short, 400 missing, 400 numeric, 503, happy with a sole trader, cap at 6, flood, 502); fetchCompanySuggestions (every outcome); toCompanySuggestion; reducer (pick equals typed orgnr after TIC, TIC overrides prefill, TIC off keeps the AB past form and name, unmapped form falls to the picker, sole trader confirms the name, replaces a previous orgnr, ignored off-step); SCB client sole-trader option. Fixes #2448 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNDuYBHVu172tesKfJmcmi * fix(onboarding): the suggestion list stays visible and stands alone (skeptic on e56eb242c) Three independent refuters on the frozen commit; every refutation that stood is fixed here. - The listbox was position: absolute inside the field, but the step scrolls (.jny-qstep is overflow-y: auto), so the list was clipped to the first row and mouse picks were unreachable (measured in headless Chrome). It now renders in flow under the field, where the chip row from #2421 already lives. - After Enter on a name (the #2421 path), SEARCH_RESULT flipped lookupPending back and the debounced effect refetched SCB, laying the listbox over the chip row or next to the nomatch note. The effect is now quiet while searchHits is non-empty and for text the user already confirmed (Enter or a pick), until the text changes. - The "many matches, type more" hint only rendered inside the list, so the flood case (SCB counts over 100 rows and sends none) showed nothing. The hint now renders on its own for that case. - app/companies/new-client (byrå adds a client) renders the same journey and now passes companySearchEnabled like the other two pages. - A stale mouse highlight could commit a row from the previous text on Enter: typing resets the highlight. - Any 503 switched the picker off for the session; only the route's own SCB_NOT_CONFIGURED does now. - NOTFOUND_EDIT / CEASED_EDIT dropped only the number and kept the abandoned pick's name and form, which a later TIC error path would have written into the company. Both now drop name and form too, unless they came from BankID's CompanyRoles prefill, which is not about the number. Not changed, recorded: a sole trader picked from SCB whom TIC does not know lands on the "no company on that number" step with the name in the field; the flow continues with the SCB name prefilled. The search JSON carries the personnummer of sole-trader rows to the authenticated browser (the row prints "Enskild firma"), same class as #2421's Enter search; flagged to the founder. Refs #2448 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNDuYBHVu172tesKfJmcmi --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
26e29f47bc |
feat(company): ideell förening as a third legal form, behind a flag (#2072 step 1) (#2423)
* feat(company): ideell förening as a third legal form, behind a flag (#2072 step 1) Why the problem occurred: the legal form was modelled as a binary flag in ~300 files. `EntityType` was a two-member union, but nothing dispatched on it exhaustively: 28 sites defaulted `?? 'enskild_firma'` (invoice, categorize, match, stripe, invoice-inbox) or `?? 'aktiebolag'` (year-end, bokslut, MCP), and every form-dependent choice was an `=== 'aktiebolag' ? A : B` ternary. Widening the union compiled everywhere and changed nothing, so a förening would have booked as an enskild firma in the app and as an aktiebolag in bokslut and MCP, with no error anywhere. The lookup refused föreningar at the door (mapEntityType returned null), which is what the tester hit. What was removed or simplified: the silent defaults. One module, lib/company/entity-type.ts, now holds the list (ENTITY_TYPES), the parser (never defaults), the resolver (settings hint, then companies.entity_type, then throw) and `byEntityType`, whose Record arms make the compiler refuse the next widening until each site has an answer. The form-dependent facts (closing account, owner settlement account, calendar-year lock, default method, K1/K2 label, personnummer vs 16-prefix) live there once instead of in the ternaries. On the SQL side supported_entity_types() replaces four copies of the literal list in the create RPCs. Why this shape and not the proposed one: the tracker asked for the enum widening plus a chart; that alone was the dangerous version (compiles, books wrong). Bundling stiftelse was considered and dropped: identical plumbing but no chart block. Creation sits behind NEXT_PUBLIC_IDEELL_FORENING_ENABLED so the CHECK, RPCs and seed can ship now and the first partner is switched on without a migration; the flag goes when Phase 2 (packs, INK3, årsbokslut, Swish) lands on the tracker. Domain choices (DECISIONS.md 2026-09-08, verify with an accountant before Phase 2): result closes to 2069 with 2068 as prior-year carry; no owner accounts, member settlement on 2890; accrual default; brutet räkenskapsår allowed; K1 label for the 5 000 kr accrual threshold (BFNAR 2010:1); org number gets the 16 prefix. Migration 20260908110835 widens the three CHECK constraints, adds supported_entity_types(), re-creates the three create RPCs with the widened guard and adds the förening block to seed_chart_of_accounts. Applied to staging and covered by ideell-forening-entity-type.pg.test.ts. Part of #2072 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdGafpUA7jVV1oYjkwfQCh * fix(company): close the förening paths the skeptic refuted (#2072) Five refutations from the /skeptic pass on 7a05c54d2, each fixed at the shared definition rather than the reported site: 1. Privately paid supplier invoices and the utlägg dialog resolved the owner account in lib/expenses/payer.ts with its own AB/EF ternary, so a förening member's invoice was built on 2893 and then refused by the expense-claim service (which already said 2890), burning an ankomstnummer. The helper now uses ownerSettlementAccount. 2. Booking templates substitute their `_ab` accounts only for an aktiebolag; the `private_expense` template kept its base 2013 for a förening. Template accounts now resolve through templateAccountForForm: EF base, AB override, förening base with owner accounts translated to 2890 (booking-templates.ts and proposal-lines.ts share it). 3. A VAT-registered förening with helårsmoms got no momsdeklaration deadline: the annual VAT rule bailed on anything but AB/EF. A förening is a juridisk person and follows the räkenskapsår schedule (SFL 26 kap 33 §), so the rule now keys on fiscalYearLockedToCalendar instead of the two literals; same in the MCP VAT report. 4. 2069 would have accumulated across years: the year-open omföring was AB-only with 2099/2098 hard-coded. planResultAppropriation now takes the pair from resultClosingAccounts (AB 2099 -> 2098, förening 2069 -> 2068) and skips forms with no carry (EF). 5. With the flag off, a registry lookup that returned "Ideell förening" was prefilled into the onboarding journey, the form picker was skipped and the create step answered "Ogiltig företagsform" with no way back. The journey, the BankID picker, the onboarding page and the MCP lookup now use mapSetupEntityType, which maps only creatable forms, so a flagged-off form falls through to the picker as before. Also: form picker keeps its AB-first order; tests for each fix. Part of #2072 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdGafpUA7jVV1oYjkwfQCh * chore(migrations): move ideell förening migration after main's latest version (20260908143051) Two migrations landed on main after the branch forked; a lower version would be skipped by the merge-time apply. Staging history row renamed to match. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdGafpUA7jVV1oYjkwfQCh * chore(skills): regenerate accounted-api reference for the widened entity_type enum Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdGafpUA7jVV1oYjkwfQCh --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
1157ff1b66 |
feat(onboarding): the orgnr field also accepts a company name (#2421)
* feat(onboarding): the orgnr field also accepts a company name The journey's first question kept asking for an organisationsnummer, and people who do not know theirs by heart left to look it up. The same field now takes either: digits (with dashes or spaces) run the existing orgnr lookup unchanged; anything else with three or more characters runs a free-text name search against the same TIC index. One hit continues exactly as a typed orgnr would; several hits render as a chip row "Name / orgnr / city" inside the same question, and the pick applies the hit's already-fetched lookup result. No hits stays on the step with a note to refine or type the number. The screen, placeholder and hint are otherwise untouched; only the mobile keyboard changes from numeric to text. Why the problem occurred: the lookup was keyed on the one identifier the user is least likely to remember, while the provider index behind it is a full-text index that already answers names. What was removed or simplified: nothing new is stored. The TIC search document carries every field /lookup returns, so a name hit is mapped by the same mapper and a picked hit costs no second provider call. The reducer gained one shared "TIC answered" transition (applyLookupFound) that the typed-orgnr path, the single-hit path and the pick path all use, instead of three copies of the fact-to-settings mapping. Why this shape and not the proposed one: search-as-you-type autocomplete would burn the 3000/mo TIC budget in days, so the search fires on Enter only, like the orgnr lookup. Taking the top hit blind on several matches was rejected: name ranking is fuzzy and common names or sole-trader surnames would land on a stranger's company; a five-chip pick row is the smallest thing that keeps the user in control. The route answers 400 under three characters, 404 in the handler's own "Company not found" shape so the client's existing dispatcher-vs-handler mapping applies, and every TIC failure code maps through the same handler as /lookup. Fixes #2418 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKHTqmnvBJAgdW4V7wZsAW * fix(onboarding): reduce Lens registration numbers to the 10-digit form for name-search hits Skeptic pass on 1d70716a8 (issue #2418). A sole trader found by name got Lens's 16-digit registration number (century-prefixed personnummer plus a 4-digit serial) stored as org_number; createCompany refuses anything normalizeOrgNumber rejects, so the journey dead-ended at the last step and the returned orgnr step could only shake. The typed-orgnr path never stored Lens's number, so this was the first place it reached settings. - searchCompaniesForLookup derives orgNumber through the new lensRegistrationToOrgNumber (16-prefixed 12 digits and the 16-digit enskild-firma form reduce to the 10-digit key; hits that do not normalize are dropped, never dead-ended). - Sole-trader chips show "Enskild firma" and city instead of the number, which is the owner's personnummer. - The name path resets the duplicate note on submit, so an earlier orgnr's "you already have X" no longer sits above the chip row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKHTqmnvBJAgdW4V7wZsAW * fix(onboarding): keep the typed name in the field after a search pick Compliance swarm on PR #2421: writing the picked hit's org number into the visible input printed a sole trader's personnummer in plain text on Back, the one thing the chip row masks. The field now keeps the name the user typed; Back re-searches it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKHTqmnvBJAgdW4V7wZsAW --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
0938646693 |
feat(onboarding): the branch question becomes its own journey step (#1615)
Founder feedback from a real signup: the done screen stacked the welcome, the 8-row company profile card AND the branch question, pushing the question below the fold, and the tiny favicons-in-ellipses provider chips looked bad. The done screen now ends in a revealed Fortsatt action; a new 'source' step at the existing KLART station (same station grammar as momsyn/moms under MOMSEN) shows only "Var fanns bokforingen innan?" with redesigned provider tiles: a 2-column grid of generously sized choices, each with the real logo on a small white bordered mark (the LogoMark grammar from NewUserChecklist), SIE-fil and new-business as equal-weight text tiles, flat hover, no lift. Everything fits one viewport. Behavior preserved exactly: mode='first' only (reducer-guarded DONE_CONTINUE), the single-choice latch, fire-and-forget keepalive PATCH /api/onboarding/state, captureBranch analytics, branchDestination routing, and the quiet skip escape. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b771c1f923 |
feat(onboarding): journey PR B — reducer, orb, track, question primitives (behind /sandbox demo) (#1145)
* refactor(onboarding): extract first-year defaults + shared TIC lookup client (journey PR A)
First of four PRs replacing the onboarding wizard with the journey flow
(dev_docs/onboarding_migration_plan.md, local). No UI change.
- Move deriveFirstYearDefaults + parseStartMonthDay out of
WelcomeOnboarding into lib/company/first-year-defaults.ts and unit-test
them (11-vs-13-months boundary, UTC month seeding, malformed input).
- Add the missing computeFiscalPeriod unit tests (calendar year, brutet
ar, first year short/extended, EF calendar-year rule, period names,
BFL 3 kap. 6-18 month window errors).
- New shared fetchCompanyLookup() client: the single client path to the
Lens-backed /lookup, typed outcomes (found / not_found / disabled /
error / aborted), never throws. Fixes the 403/404 conflation: the
dispatcher's 404 ("Extension not found") and feature-flag 503
(EXTENSION_DISABLED) now degrade silently instead of rendering as
"company not found"; only the TIC handler's own 404 does.
- Step2CompanyDetails consumes the helper; identical UX otherwise.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(onboarding): journey state machine reducer with full branch coverage (journey PR B, 1/3)
Pure reducer for the journey onboarding: owns every transition and every
CompanySettings write; the component layer only renders steps, runs the
single TIC lookup, and calls the server action.
Encodes the plan's invariants: entry-snapshot history (Back rolls answers
AND stations), lookupRan gates fact-vs-question per field (BankID prefill
without lookup degrades to questions), vat_registered is never defaulted
without lookup data or an explicit answer, entity change wipes downstream,
org_number_invalid bounces to the Företaget station, station jumps rewind
to a station's first step.
34 unit tests: AB/EF found, not-found manual, ceased, BankID prefill
(found + degraded + disabled), first year, brutet år, moms nej, Back from
every step, station jumps, server-error bounces.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(onboarding): journey visual primitives + sandbox gallery (journey PR B, 2/3)
Ports the founder-approved concept (artifact c82c9358) to React:
- JourneyOrb: 320-particle canvas sphere with comet travel, check morph
and the monogram finale (glyph sampled live from --font-display). Own
component per plan, NOT thinking-orbs. rAF pauses on document.hidden;
reduced motion renders static frames.
- JourneyTrack: five stations with inked answers; completed stations are
keyboard-accessible jump-back buttons; answers mirrored to an aria-live
region.
- Question primitives: Question (ink title + "?" popover, Esc closes),
ChipRow (fly-to-orb ghost), YearBand (springy fiscal-year preview),
JourneyDatePicker (year -> month by name -> day), AddressFields
(Enter-chained, skippable).
- journey.css: concept stylesheet namespaced under .jny on app tokens,
incl. the no-scroll composition (100dvh + optical-centering balance
spacer) and the dawn layer.
- /sandbox/journey: internal primitive gallery (auth-free sandbox path),
demo data only: this page makes ZERO TIC calls.
i18n note: primitives are copy-agnostic (strings via props); the real
flow's sv/en keys land with their consumer in PR C.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: log journey reducer location decision
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(onboarding): annotate ENTITY_PICKED settings as Partial<CompanySettings>
The wipeDownstream return narrows against the inferred initializer type;
tsc strict rejects the reassignment without the explicit annotation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|