Feat/voucher docs (#664)
* feat: implement inbox document picker and linking functionality * feat: implement self-billing invoice functionality - Added support for registering self-billed invoices received from customers. - Updated the invoice schema to include fields for self-billing metadata such as `is_self_billed`, `external_invoice_number`, `self_billing_agreement_ref`, and `received_date`. - Created API route for handling self-billed invoice submissions, including validation and error handling. - Implemented database migrations to add necessary columns and constraints for self-billing invoices. - Developed tests to ensure correct behavior of self-billing invoice creation and validation rules. - Updated Swedish localization files to include new terms related to self-billing. * feat: enforce SIE import requirement for non-Fortnox providers in migration process * feat: streamline invoice processing and enhance error logging across APIs
This commit is contained in:
@@ -190,28 +190,16 @@ export default function BankIdCompanyPicker({
|
||||
const entityLabel = humanTicEntityType(role.legalEntityType)
|
||||
const mappable = mapEntityType(role.legalEntityType) !== null
|
||||
|
||||
if (status === 'exists') {
|
||||
return (
|
||||
<li key={cleaned}>
|
||||
<div className="w-full rounded-lg border bg-muted/20 p-4 text-left opacity-70">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-sm truncate">{role.legalName}</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5 truncate">
|
||||
{cleaned} · {entityLabel}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-[10px] uppercase tracking-wide text-muted-foreground flex-shrink-0">
|
||||
{t('already_in_app', { appName: branding.appName.toLowerCase() })}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground/70 mt-2">
|
||||
{t('ask_admin_invite')}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
// Companies already in gnubok under another account are still
|
||||
// offered for setup — org-number reuse is allowed (a director
|
||||
// may keep a separate test copy). Keep a muted note so the
|
||||
// "also already in {app}" context isn't lost.
|
||||
const existsNote =
|
||||
status === 'exists' ? (
|
||||
<p className="text-xs text-muted-foreground/70 mt-2">
|
||||
{t('already_in_app', { appName: branding.appName.toLowerCase() })}
|
||||
</p>
|
||||
) : null
|
||||
|
||||
if (!mappable) {
|
||||
return (
|
||||
@@ -236,6 +224,7 @@ export default function BankIdCompanyPicker({
|
||||
{t('setup_manually')}
|
||||
</span>
|
||||
</div>
|
||||
{existsNote}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
@@ -263,6 +252,7 @@ export default function BankIdCompanyPicker({
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
</div>
|
||||
{existsNote}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
|
||||
@@ -12,11 +12,8 @@ import { Label } from '@/components/ui/label'
|
||||
import { Loader2, ArrowRight, ArrowLeft, CheckCircle2, AlertTriangle } from 'lucide-react'
|
||||
import type { EntityType } from '@/types'
|
||||
import type { CompanyLookupResult } from '@/lib/company-lookup/types'
|
||||
import { getBranding } from '@/lib/branding/service'
|
||||
import { normalizeOrgNumber } from '@/lib/company-lookup/normalize-org-number'
|
||||
|
||||
const branding = getBranding()
|
||||
|
||||
const schema = z.object({
|
||||
company_name: z.string().min(1, 'Företagsnamn krävs'),
|
||||
org_number: z.string()
|
||||
@@ -82,7 +79,7 @@ export default function Step2CompanyDetails({
|
||||
const [isLooking, setIsLooking] = useState(false)
|
||||
const [lookupError, setLookupError] = useState<string | null>(null)
|
||||
const [lookupDone, setLookupDone] = useState<CompanyLookupResult | null>(null)
|
||||
const [orgNumberExists, setOrgNumberExists] = useState(false)
|
||||
const [existingOwn, setExistingOwn] = useState<{ id: string; name: string } | null>(null)
|
||||
const abortRef = useRef<AbortController | null>(null)
|
||||
const dupAbortRef = useRef<AbortController | null>(null)
|
||||
// Tracks an orgnr that's been pre-verified (BankID CompanyRoles match) so
|
||||
@@ -96,13 +93,14 @@ export default function Step2CompanyDetails({
|
||||
|
||||
const orgNumber = watch('org_number')
|
||||
|
||||
// Debounced duplicate check against Accounted's own companies table. Runs in
|
||||
// parallel with the TIC lookup — they don't conflict. On match, the submit
|
||||
// button is disabled; the server action would also reject ('org_number_exists')
|
||||
// but blocking client-side avoids a wasted roundtrip.
|
||||
// Soft, account-scoped duplicate warning: if THIS user already has a
|
||||
// (non-archived) company with the same org number, surface a non-blocking
|
||||
// note. Org-number reuse is allowed (see lib/company/actions.ts), so this
|
||||
// never disables submit. The endpoint is RLS-scoped to the caller's own
|
||||
// companies, so it can't reveal or count another account's.
|
||||
useEffect(() => {
|
||||
if (!orgNumber || normalizeOrgNumber(orgNumber) === null) {
|
||||
setOrgNumberExists(false)
|
||||
setExistingOwn(null)
|
||||
return
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
@@ -115,10 +113,10 @@ export default function Step2CompanyDetails({
|
||||
.then(async (res) => {
|
||||
if (controller.signal.aborted || !res.ok) return
|
||||
const { data } = await res.json()
|
||||
setOrgNumberExists(!!data?.exists)
|
||||
setExistingOwn(data?.companies?.[0] ?? null)
|
||||
})
|
||||
.catch(() => {
|
||||
// Network failure is non-fatal — the server action will re-check.
|
||||
// Advisory only — never blocks creation.
|
||||
})
|
||||
}, 500)
|
||||
return () => {
|
||||
@@ -267,12 +265,10 @@ export default function Step2CompanyDetails({
|
||||
{ticEnabled && lookupError && (
|
||||
<p className="text-xs text-muted-foreground">{lookupError}</p>
|
||||
)}
|
||||
{orgNumberExists && (
|
||||
<div className="flex items-start gap-2 text-sm text-destructive">
|
||||
<AlertTriangle className="h-3.5 w-3.5 mt-0.5 flex-shrink-0" />
|
||||
<span>
|
||||
{t('step2_company_exists', { appName: branding.appName.toLowerCase() })}
|
||||
</span>
|
||||
{existingOwn && (
|
||||
<div className="flex items-start gap-2 rounded-md bg-warning/15 px-3 py-2 text-sm text-warning-foreground">
|
||||
<AlertTriangle className="h-4 w-4 mt-0.5 flex-shrink-0 text-warning" />
|
||||
<span>{t('step2_company_exists_own', { name: existingOwn.name })}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -338,7 +334,7 @@ export default function Step2CompanyDetails({
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSaving || orgNumberExists}
|
||||
disabled={isSaving}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{isSaving ? (
|
||||
|
||||
Reference in New Issue
Block a user