refactor(onboarding): extract first-year defaults + shared TIC lookup client (journey PR A) (#1141)
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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
2bec5acedb
commit
f9ef8913ae
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { createCompanyFromOnboarding } from '@/lib/company/actions'
|
||||
import { computeFiscalPeriod } from '@/lib/company/compute-fiscal-period'
|
||||
import { deriveFirstYearDefaults, parseStartMonthDay } from '@/lib/company/first-year-defaults'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { Building2 } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -66,42 +67,6 @@ function logError(message: string, extra?: Record<string, unknown>) {
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// Parse TIC v2's `startMonthDay` ("MM-DD": e.g. "07-01") into a month
|
||||
// number 1-12. Returns null on missing / malformed input so the caller
|
||||
// can fall through to the manual picker default.
|
||||
function parseStartMonthDay(value: string | null | undefined): number | null {
|
||||
if (!value) return null
|
||||
const match = /^(\d{1,2})-\d{1,2}$/.exec(value)
|
||||
if (!match) return null
|
||||
const month = Number(match[1])
|
||||
if (!Number.isInteger(month) || month < 1 || month > 12) return null
|
||||
return month
|
||||
}
|
||||
|
||||
// Derive the Step-3 first-year defaults from TIC's `registrationDate`.
|
||||
// A company is treated as "first year" when registered less than 12 months
|
||||
// ago: fits BFL's 6-18 month opening-period window comfortably. Returns
|
||||
// both the toggle state and a seeded `first_year_start` (always the 1st of
|
||||
// the registration month, the format Step 3's date inputs expect).
|
||||
function deriveFirstYearDefaults(registrationDate: number | null | undefined): {
|
||||
isFirstFiscalYear: boolean
|
||||
firstYearStart: string | undefined
|
||||
} {
|
||||
if (!registrationDate || !Number.isFinite(registrationDate)) {
|
||||
return { isFirstFiscalYear: false, firstYearStart: undefined }
|
||||
}
|
||||
const regDate = new Date(registrationDate)
|
||||
if (Number.isNaN(regDate.getTime())) {
|
||||
return { isFirstFiscalYear: false, firstYearStart: undefined }
|
||||
}
|
||||
const monthsAgo =
|
||||
(Date.now() - regDate.getTime()) / (1000 * 60 * 60 * 24 * 30.44)
|
||||
if (monthsAgo >= 12) return { isFirstFiscalYear: false, firstYearStart: undefined }
|
||||
const year = regDate.getUTCFullYear()
|
||||
const month = String(regDate.getUTCMonth() + 1).padStart(2, '0')
|
||||
return { isFirstFiscalYear: true, firstYearStart: `${year}-${month}-01` }
|
||||
}
|
||||
|
||||
interface WelcomeOnboardingProps {
|
||||
firstName?: string | null
|
||||
teamId: string
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Loader2, ArrowRight, ArrowLeft, CheckCircle2, AlertTriangle } from 'luc
|
||||
import type { EntityType } from '@/types'
|
||||
import type { CompanyLookupResult } from '@/lib/company-lookup/types'
|
||||
import { normalizeOrgNumber } from '@/lib/company-lookup/normalize-org-number'
|
||||
import { fetchCompanyLookup } from '@/lib/company-lookup/fetch-company-lookup'
|
||||
|
||||
const schema = z.object({
|
||||
company_name: z.string().min(1, 'Företagsnamn krävs'),
|
||||
@@ -152,32 +153,27 @@ export default function Step2CompanyDetails({
|
||||
|
||||
setIsLooking(true)
|
||||
|
||||
fetch(`/api/extensions/ext/tic/lookup?org_number=${encodeURIComponent(orgNumber)}`, {
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (controller.signal.aborted) return
|
||||
fetchCompanyLookup(orgNumber, { ticEnabled: true, signal: controller.signal })
|
||||
.then((outcome) => {
|
||||
if (controller.signal.aborted || outcome.status === 'aborted') return
|
||||
|
||||
if (res.status === 403) {
|
||||
// Extension disabled: silently ignore
|
||||
if (outcome.status === 'disabled') {
|
||||
// Lookup surface unavailable (extension off / dispatcher miss):
|
||||
// degrade silently to manual entry, the input is not at fault.
|
||||
return
|
||||
}
|
||||
if (res.status === 404) {
|
||||
if (outcome.status === 'not_found') {
|
||||
setLookupError(t('step2_lookup_not_found'))
|
||||
onTicLookup?.(null)
|
||||
return
|
||||
}
|
||||
if (!res.ok) {
|
||||
if (outcome.status === 'error') {
|
||||
setLookupError(t('step2_lookup_failed'))
|
||||
onTicLookup?.(null)
|
||||
return
|
||||
}
|
||||
|
||||
const { data } = (await res.json()) as { data: CompanyLookupResult }
|
||||
|
||||
// Guard: only apply if org_number still matches (user may have changed it)
|
||||
if (controller.signal.aborted) return
|
||||
|
||||
const data = outcome.result
|
||||
setLookupDone(data)
|
||||
onTicLookup?.(data)
|
||||
|
||||
@@ -187,11 +183,6 @@ export default function Step2CompanyDetails({
|
||||
if (data.address?.postalCode) setValue('postal_code', data.address.postalCode)
|
||||
if (data.address?.city) setValue('city', data.address.city)
|
||||
})
|
||||
.catch((err) => {
|
||||
if ((err as Error).name === 'AbortError') return
|
||||
setLookupError(t('step2_lookup_failed'))
|
||||
onTicLookup?.(null)
|
||||
})
|
||||
.finally(() => {
|
||||
if (!controller.signal.aborted) setIsLooking(false)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user