The buyer's party for a Turkish e-Fatura / e-Arşiv invoice: an individual / company toggle, first and last name with a TCKN, or a company title with tax office and a VKN (or the owner's TCKN for a sole proprietorship), e-mail, phone and a structured address. Fields only — the same validateBillingDetails runs in the browser and on the server and returns a normalised party with its UBL-TR scheme (TCKN / VKN). No e-invoice integration.
Installation
The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";
Usage
import { BillingDetailsFields, billingDetailsFromFormData, billingErrorMessages, validateBillingDetails } from "@vamos-ui/react/business";
// page
<form action={saveBilling}>
<BillingDetailsFields name="billing" defaultValue={{ type: "individual", address: { country: "TR" } }} />
</form>
// server action
async function saveBilling(data: FormData) {
"use server";
const v = validateBillingDetails(billingDetailsFromFormData(data, "billing"));
if (!v.ok) return { errors: billingErrorMessages(v.errors) }; // pass back as errors={…}
await customers.saveBillingParty(v.party!); // { type, name, taxId, taxScheme: "TCKN" | "VKN", taxOffice?, email, phone?, address }
}A sole proprietorship (şahıs şirketi) invoices with the owner's TCKN, so the company tax number accepts 10-digit VKNs and 11-digit TCKNs; taxScheme tells which one it is.
The tax office is free text: there is no bundled list of tax offices.
Whether the buyer is registered for e-Fatura (and so gets an e-Fatura instead of an e-Arşiv invoice) is answered by your e-invoice provider, not by this form.
Examples
Turkish labels
The tr dictionary (@vamos-ui/react/locale/tr) gives Bireysel / Kurumsal, Unvan, Vergi dairesi, VKN and the Turkish error messages; billingErrorMessages(errors, tr.billingDetails) formats server errors the same way.
Companies only, with neighbourhoods
types={["company"]} hides the toggle (a hidden billing[type] is still posted); addressProps passes a neighbourhood source to the address fieldset; phone="required".
Sole proprietorships use the owner's TCKN.
The invoice is sent here.
API
BillingDetailsFields
| Prop | Type | Default |
|---|---|---|
value / defaultValue / onValueChange{ type, firstName?, lastName?, nationalId?, companyName?, taxId?, taxOffice?, email?, phone?, address? }. | BillingDetails | — |
typesOffered party types; one type hides the toggle. | BillingPartyType[] | ["individual", "company"] |
nameForm prefix: billing → billing[type], billing[taxId], billing[address][region1]… Read back with billingDetailsFromFormData(data, name). | string | — |
errorsPer-field messages (billingErrorMessages). The ID inputs also show their own checksum errors on blur. | Partial<Record<BillingField, ReactNode>> | — |
phoneThe phone field. | "optional" | "required" | "hidden" | "optional" |
addressPropsPassed to the address fieldset: sources (neighbourhoods), countries (default ["TR"]), fields, layout. | Partial<AddressFieldProps> | — |
legend / hint / size / disabled / labelsFieldset legend (default "Billing details"), helper text, control size, built-in strings (billingDetails namespace). | … | — |
validateBillingDetails(details, opts)
| Prop | Type | Default |
|---|---|---|
opts.allowUnknownTcknAccept 11111111111, the placeholder TCKN e-Arşiv invoices use for a consumer who gives no ID number. | boolean | false |
opts.requirePhonePhone becomes required. | boolean | false |
returnserrors: field → "required" | "invalid" | "tooLong" | "unknownTckn". party: name (full name or title), taxId digits, taxScheme, taxOffice, trimmed address. | { ok, errors, party? } | — |
Helpers
| Prop | Type | Default |
|---|---|---|
billingDetailsFromFormData(data, name = "billing")Reads the posted fields. | BillingDetails | — |
billingErrorMessages(errors, labels?, format?)Codes to messages; labels from a dictionary (tr.billingDetails) for other languages. | Partial<Record<BillingField, string>> | — |