@open-mercato/core 0.7.1-develop.7122.1.421cefe668 → 0.7.1-develop.7130.1.fef2396fd8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/api/deals/aggregate/route.js +85 -0
  3. package/dist/modules/customers/api/deals/aggregate/route.js.map +2 -2
  4. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +5 -117
  5. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  6. package/dist/modules/customers/backend/customers/deals/[id]/page.js +1 -3
  7. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  8. package/dist/modules/customers/commands/deals.js +33 -9
  9. package/dist/modules/customers/commands/deals.js.map +3 -3
  10. package/dist/modules/customers/components/AddressEditor.js +71 -1
  11. package/dist/modules/customers/components/AddressEditor.js.map +2 -2
  12. package/dist/modules/customers/utils/addressFormat.js +18 -2
  13. package/dist/modules/customers/utils/addressFormat.js.map +2 -2
  14. package/dist/modules/sales/components/documents/AddressesSection.js +17 -2
  15. package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
  16. package/dist/modules/sales/components/documents/SalesDocumentForm.js +7 -24
  17. package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
  18. package/dist/modules/sales/components/documents/normalizeAddressDraft.js +29 -0
  19. package/dist/modules/sales/components/documents/normalizeAddressDraft.js.map +7 -0
  20. package/package.json +7 -7
  21. package/src/modules/customers/api/deals/aggregate/route.ts +102 -0
  22. package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +0 -23
  23. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +11 -172
  24. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +1 -1
  25. package/src/modules/customers/commands/deals.ts +84 -9
  26. package/src/modules/customers/components/AddressEditor.tsx +116 -1
  27. package/src/modules/customers/i18n/de.json +6 -0
  28. package/src/modules/customers/i18n/en.json +6 -0
  29. package/src/modules/customers/i18n/es.json +6 -0
  30. package/src/modules/customers/i18n/ko.json +6 -0
  31. package/src/modules/customers/i18n/pl.json +6 -0
  32. package/src/modules/customers/utils/addressFormat.tsx +59 -1
  33. package/src/modules/sales/components/documents/AddressesSection.tsx +48 -5
  34. package/src/modules/sales/components/documents/SalesDocumentForm.tsx +5 -23
  35. package/src/modules/sales/components/documents/normalizeAddressDraft.ts +29 -0
@@ -24,7 +24,7 @@ import {
24
24
  } from '@open-mercato/ui/primitives/dialog'
25
25
  import { buildCountryOptions } from '@open-mercato/shared/lib/location/countries'
26
26
  import { buildHrefWithReturnTo } from '@open-mercato/shared/lib/navigation/returnTo'
27
- import type { AddressFormatStrategy } from '../utils/addressFormat'
27
+ import { resolveTaxIdLabel, type AddressFormatStrategy } from '../utils/addressFormat'
28
28
  import { useAddressTypes } from './detail/hooks/useAddressTypes'
29
29
 
30
30
  type Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string
@@ -41,6 +41,20 @@ export type AddressEditorDraft = {
41
41
  region: string
42
42
  postalCode: string
43
43
  country: string
44
+ /**
45
+ * Contact details that belong to the ADDRESS rather than to the customer: the tax identifier it was
46
+ * invoiced under, and the phone a carrier calls about a delivery. Optional so every existing caller
47
+ * keeps compiling; callers must opt in with the matching `show*Field` props before they render.
48
+ */
49
+ taxId?: string
50
+ /**
51
+ * Which scheme the identifier belongs to, in Stripe's `{country}_{kind}` vocabulary. Chosen, not
52
+ * inferred: `PL1234567890` and `1234567890` are the same business written two ways, and a rule
53
+ * that reads the form of the value is guessing — the more schemes the vocabulary carries, the more
54
+ * often it guesses wrong, and a wrong type is worse than none because it names the number.
55
+ */
56
+ taxIdType?: string
57
+ phone?: string
44
58
  latitude?: string
45
59
  longitude?: string
46
60
  isPrimary: boolean
@@ -58,6 +72,9 @@ export type AddressEditorField =
58
72
  | 'region'
59
73
  | 'postalCode'
60
74
  | 'country'
75
+ | 'taxId'
76
+ | 'taxIdType'
77
+ | 'phone'
61
78
  | 'latitude'
62
79
  | 'longitude'
63
80
  | 'isPrimary'
@@ -72,8 +89,33 @@ type AddressEditorProps = {
72
89
  hidePrimaryToggle?: boolean
73
90
  showFormatHint?: boolean
74
91
  showCoordinateFields?: boolean
92
+ /**
93
+ * Render the phone. Off by default, opt-in for the same reason `showCoordinateFields` is: only a
94
+ * caller whose storage can hold a field should offer it.
95
+ */
96
+ showPhoneField?: boolean
97
+ /**
98
+ * Render the tax identifier and its scheme. Separate from the phone, and not for symmetry — a
99
+ * phone is a contact detail and a tax identifier is not, and the two stop travelling together at
100
+ * the next phase: `CustomerAddress` gains a `phone` column and no tax id, so the address book will
101
+ * offer one and not the other.
102
+ *
103
+ * Both gate the CALLER, not the field. Inside a tile that opts in, the input renders whether or not
104
+ * it carries a value and takes the same `disabled` as every neighbour.
105
+ */
106
+ showTaxIdField?: boolean
75
107
  }
76
108
 
109
+ /**
110
+ * The schemes offered in the picker, in the order a Polish deployment meets them. The vocabulary is
111
+ * additive under the backward-compatibility contract, so a new scheme is a new entry here rather
112
+ * than a new branch in a rule that has to guess.
113
+ *
114
+ * The labels are the ones `resolveTaxIdLabel` resolves, so the picker and the marker beside the
115
+ * filled field always read the same.
116
+ */
117
+ const TAX_ID_TYPES = ['pl_nip', 'eu_vat', 'other'] as const
118
+
77
119
  export function AddressEditor({
78
120
  value,
79
121
  onChange,
@@ -84,6 +126,8 @@ export function AddressEditor({
84
126
  hidePrimaryToggle = false,
85
127
  showFormatHint = true,
86
128
  showCoordinateFields = false,
129
+ showPhoneField = false,
130
+ showTaxIdField = false,
87
131
  }: AddressEditorProps) {
88
132
  const pathname = usePathname()
89
133
  const searchParams = useSearchParams()
@@ -102,6 +146,11 @@ export function AddressEditor({
102
146
  [t],
103
147
  )
104
148
 
149
+ const taxIdLabels = {
150
+ plNip: t('customers.people.detail.addresses.fields.taxId.plNip', 'Tax ID'),
151
+ euVat: t('customers.people.detail.addresses.fields.taxId.euVat', 'EU VAT'),
152
+ other: t('customers.people.detail.addresses.fields.taxId.other', 'Tax number'),
153
+ }
105
154
  const current: AddressEditorDraft = {
106
155
  name: value.name ?? '',
107
156
  purpose: value.purpose ?? '',
@@ -114,6 +163,9 @@ export function AddressEditor({
114
163
  region: value.region ?? '',
115
164
  postalCode: value.postalCode ?? '',
116
165
  country: value.country ?? '',
166
+ taxId: value.taxId ?? '',
167
+ taxIdType: value.taxIdType ?? '',
168
+ phone: value.phone ?? '',
117
169
  ...(showCoordinateFields
118
170
  ? { latitude: value.latitude ?? '', longitude: value.longitude ?? '' }
119
171
  : {}),
@@ -472,6 +524,69 @@ export function AddressEditor({
472
524
  {errors.longitude ? <p className="text-xs text-destructive">{errors.longitude}</p> : null}
473
525
  </>
474
526
  ) : null}
527
+ {showTaxIdField ? (
528
+ <>
529
+ <Select
530
+ value={current.taxIdType || undefined}
531
+ onValueChange={(next) => update('taxIdType', next ?? '')}
532
+ disabled={disabled}
533
+ >
534
+ <SelectTrigger
535
+ className={errors.taxIdType ? 'border-destructive' : undefined}
536
+ aria-label={t('customers.people.detail.addresses.fields.taxIdType', 'Tax id type')}
537
+ aria-invalid={errors.taxIdType ? 'true' : undefined}
538
+ >
539
+ <SelectValue
540
+ placeholder={t('customers.people.detail.addresses.fields.taxIdType', 'Tax id type')}
541
+ />
542
+ </SelectTrigger>
543
+ <SelectContent>
544
+ {TAX_ID_TYPES.map((type) => (
545
+ <SelectItem key={type} value={type}>
546
+ {resolveTaxIdLabel(taxIdLabels, type)}
547
+ </SelectItem>
548
+ ))}
549
+ </SelectContent>
550
+ </Select>
551
+ <Input
552
+ className={inputClass('taxId')}
553
+ placeholder={t('customers.people.detail.addresses.fields.taxId', 'Tax number')}
554
+ aria-label={t('customers.people.detail.addresses.fields.taxId', 'Tax number')}
555
+ rightIcon={
556
+ current.taxId ? (
557
+ <span className="text-xs">{resolveTaxIdLabel(taxIdLabels, current.taxIdType)}</span>
558
+ ) : null
559
+ }
560
+ value={current.taxId ?? ''}
561
+ onChange={(evt) => update('taxId', evt.target.value)}
562
+ disabled={disabled}
563
+ aria-invalid={errors.taxId ? 'true' : undefined}
564
+ />
565
+ {errors.taxId ? <p className="text-xs text-destructive">{errors.taxId}</p> : null}
566
+ </>
567
+ ) : null}
568
+ {showPhoneField ? (
569
+ <>
570
+ <Input
571
+ className={inputClass('phone')}
572
+ placeholder={t('customers.people.detail.addresses.fields.phone', 'Phone')}
573
+ aria-label={t('customers.people.detail.addresses.fields.phone', 'Phone')}
574
+ rightIcon={
575
+ current.phone ? (
576
+ <span className="text-xs">
577
+ {t('customers.people.detail.addresses.fields.phone', 'Phone')}
578
+ </span>
579
+ ) : null
580
+ }
581
+ inputMode="tel"
582
+ value={current.phone ?? ''}
583
+ onChange={(evt) => update('phone', evt.target.value)}
584
+ disabled={disabled}
585
+ aria-invalid={errors.phone ? 'true' : undefined}
586
+ />
587
+ {errors.phone ? <p className="text-xs text-destructive">{errors.phone}</p> : null}
588
+ </>
589
+ ) : null}
475
590
  </div>
476
591
  {!hidePrimaryToggle ? (
477
592
  <label className="inline-flex items-center gap-2 text-sm">
@@ -1867,11 +1867,17 @@
1867
1867
  "customers.people.detail.addresses.fields.line1": "Adresszeile 1",
1868
1868
  "customers.people.detail.addresses.fields.line2": "Adresszeile 2",
1869
1869
  "customers.people.detail.addresses.fields.longitude": "Längengrad",
1870
+ "customers.people.detail.addresses.fields.phone": "Telefon",
1870
1871
  "customers.people.detail.addresses.fields.postalCode": "Postleitzahl",
1871
1872
  "customers.people.detail.addresses.fields.primary": "Als primär markieren",
1872
1873
  "customers.people.detail.addresses.fields.region": "Bundesland / Region",
1873
1874
  "customers.people.detail.addresses.fields.street": "Straße",
1874
1875
  "customers.people.detail.addresses.fields.streetExtra": "Weitere Adressangaben",
1876
+ "customers.people.detail.addresses.fields.taxId": "Steuernummer",
1877
+ "customers.people.detail.addresses.fields.taxId.euVat": "USt-IdNr.",
1878
+ "customers.people.detail.addresses.fields.taxId.other": "Steuerliche Nummer",
1879
+ "customers.people.detail.addresses.fields.taxId.plNip": "Steuernummer",
1880
+ "customers.people.detail.addresses.fields.taxIdType": "Art der Steuernummer",
1875
1881
  "customers.people.detail.addresses.fields.type": "Adresstyp",
1876
1882
  "customers.people.detail.addresses.formatLoadError": "Adresskonfiguration konnte nicht geladen werden",
1877
1883
  "customers.people.detail.addresses.formatLoading": "Adresspräferenzen werden geladen…",
@@ -1867,11 +1867,17 @@
1867
1867
  "customers.people.detail.addresses.fields.line1": "Address line 1",
1868
1868
  "customers.people.detail.addresses.fields.line2": "Address line 2",
1869
1869
  "customers.people.detail.addresses.fields.longitude": "Longitude",
1870
+ "customers.people.detail.addresses.fields.phone": "Phone",
1870
1871
  "customers.people.detail.addresses.fields.postalCode": "Postal code",
1871
1872
  "customers.people.detail.addresses.fields.primary": "Set as primary",
1872
1873
  "customers.people.detail.addresses.fields.region": "Region / State",
1873
1874
  "customers.people.detail.addresses.fields.street": "Street",
1874
1875
  "customers.people.detail.addresses.fields.streetExtra": "Additional address details",
1876
+ "customers.people.detail.addresses.fields.taxId": "Tax number",
1877
+ "customers.people.detail.addresses.fields.taxId.euVat": "EU VAT",
1878
+ "customers.people.detail.addresses.fields.taxId.other": "Tax number",
1879
+ "customers.people.detail.addresses.fields.taxId.plNip": "Tax ID",
1880
+ "customers.people.detail.addresses.fields.taxIdType": "Tax id type",
1875
1881
  "customers.people.detail.addresses.fields.type": "Address type",
1876
1882
  "customers.people.detail.addresses.formatLoadError": "Failed to load address configuration",
1877
1883
  "customers.people.detail.addresses.formatLoading": "Loading address preferences…",
@@ -1867,11 +1867,17 @@
1867
1867
  "customers.people.detail.addresses.fields.line1": "Dirección (línea 1)",
1868
1868
  "customers.people.detail.addresses.fields.line2": "Dirección (línea 2)",
1869
1869
  "customers.people.detail.addresses.fields.longitude": "Longitud",
1870
+ "customers.people.detail.addresses.fields.phone": "Teléfono",
1870
1871
  "customers.people.detail.addresses.fields.postalCode": "Código postal",
1871
1872
  "customers.people.detail.addresses.fields.primary": "Marcar como principal",
1872
1873
  "customers.people.detail.addresses.fields.region": "Provincia / región",
1873
1874
  "customers.people.detail.addresses.fields.street": "Calle",
1874
1875
  "customers.people.detail.addresses.fields.streetExtra": "Detalles adicionales de la dirección",
1876
+ "customers.people.detail.addresses.fields.taxId": "Número fiscal",
1877
+ "customers.people.detail.addresses.fields.taxId.euVat": "IVA UE",
1878
+ "customers.people.detail.addresses.fields.taxId.other": "Número fiscal",
1879
+ "customers.people.detail.addresses.fields.taxId.plNip": "NIF",
1880
+ "customers.people.detail.addresses.fields.taxIdType": "Tipo de número fiscal",
1875
1881
  "customers.people.detail.addresses.fields.type": "Tipo de dirección",
1876
1882
  "customers.people.detail.addresses.formatLoadError": "No se pudo cargar la configuración de la dirección",
1877
1883
  "customers.people.detail.addresses.formatLoading": "Cargando preferencias de dirección…",
@@ -1867,11 +1867,17 @@
1867
1867
  "customers.people.detail.addresses.fields.line1": "주소 1",
1868
1868
  "customers.people.detail.addresses.fields.line2": "주소 2",
1869
1869
  "customers.people.detail.addresses.fields.longitude": "경도",
1870
+ "customers.people.detail.addresses.fields.phone": "전화번호",
1870
1871
  "customers.people.detail.addresses.fields.postalCode": "우편번호",
1871
1872
  "customers.people.detail.addresses.fields.primary": "기본 주소로 설정",
1872
1873
  "customers.people.detail.addresses.fields.region": "지역 / 주",
1873
1874
  "customers.people.detail.addresses.fields.street": "도로명",
1874
1875
  "customers.people.detail.addresses.fields.streetExtra": "추가 주소 정보",
1876
+ "customers.people.detail.addresses.fields.taxId": "세금 번호",
1877
+ "customers.people.detail.addresses.fields.taxId.euVat": "EU 부가세 번호",
1878
+ "customers.people.detail.addresses.fields.taxId.other": "세금 번호",
1879
+ "customers.people.detail.addresses.fields.taxId.plNip": "납세자 번호",
1880
+ "customers.people.detail.addresses.fields.taxIdType": "세금 번호 유형",
1875
1881
  "customers.people.detail.addresses.fields.type": "주소 유형",
1876
1882
  "customers.people.detail.addresses.formatLoadError": "주소 설정을 불러오지 못했습니다",
1877
1883
  "customers.people.detail.addresses.formatLoading": "주소 설정을 불러오는 중…",
@@ -1867,11 +1867,17 @@
1867
1867
  "customers.people.detail.addresses.fields.line1": "Adres (linia 1)",
1868
1868
  "customers.people.detail.addresses.fields.line2": "Adres (linia 2)",
1869
1869
  "customers.people.detail.addresses.fields.longitude": "Długość geograficzna",
1870
+ "customers.people.detail.addresses.fields.phone": "Telefon",
1870
1871
  "customers.people.detail.addresses.fields.postalCode": "Kod pocztowy",
1871
1872
  "customers.people.detail.addresses.fields.primary": "Ustaw jako główny",
1872
1873
  "customers.people.detail.addresses.fields.region": "Region / województwo",
1873
1874
  "customers.people.detail.addresses.fields.street": "Ulica",
1874
1875
  "customers.people.detail.addresses.fields.streetExtra": "Dodatkowe informacje adresowe",
1876
+ "customers.people.detail.addresses.fields.taxId": "Nr podatkowy",
1877
+ "customers.people.detail.addresses.fields.taxId.euVat": "VAT UE",
1878
+ "customers.people.detail.addresses.fields.taxId.other": "Nr podatkowy",
1879
+ "customers.people.detail.addresses.fields.taxId.plNip": "NIP",
1880
+ "customers.people.detail.addresses.fields.taxIdType": "Typ numeru",
1875
1881
  "customers.people.detail.addresses.fields.type": "Typ adresu",
1876
1882
  "customers.people.detail.addresses.formatLoadError": "Nie udało się wczytać konfiguracji adresów",
1877
1883
  "customers.people.detail.addresses.formatLoading": "Ładowanie preferencji adresowych…",
@@ -13,6 +13,34 @@ export type AddressValue = {
13
13
  postalCode?: string | null
14
14
  country?: string | null
15
15
  companyName?: string | null
16
+ /**
17
+ * Contact details that belong to the ADDRESS rather than to the customer: who to call about this
18
+ * delivery, and the tax identifier this invoice address was billed under. They remain available to
19
+ * address editors and snapshot payloads, but are deliberately excluded from `formatAddressLines`
20
+ * and `AddressView`, whose existing contract remains postal-only.
21
+ *
22
+ * `taxIdType` interprets the value in Stripe's `{country}_{kind}` vocabulary (`pl_nip`, `eu_vat`,
23
+ * `other`, widened additively): `1234567890` and `PL1234567890` are the same business, and only the
24
+ * type tells a domestic identifier from an EU VAT number. It is metadata about `taxId`, never a
25
+ * displayed field of its own.
26
+ */
27
+ phone?: string | null
28
+ taxId?: string | null
29
+ taxIdType?: string | null
30
+ }
31
+
32
+ /**
33
+ * Tax-id labels keyed by `taxIdType`, for a caller that wants the identifier named correctly rather
34
+ * than generically.
35
+ *
36
+ * The stored scheme is chosen explicitly rather than inferred from the identifier. `other` also
37
+ * covers an address written before `taxIdType` existed. An unrecognised type takes the `other` route
38
+ * instead of guessing a domestic scheme.
39
+ */
40
+ export type TaxIdLabelByType = {
41
+ plNip: string
42
+ euVat: string
43
+ other: string
16
44
  }
17
45
 
18
46
  export type AddressJsonShape = {
@@ -100,6 +128,31 @@ export function formatAddressString(address: AddressValue, format: AddressFormat
100
128
  return formatAddressLines(address, format).filter(Boolean).join(separator)
101
129
  }
102
130
 
131
+ /**
132
+ * Which member of a label map names which scheme. Private, and deliberately not exhaustive: an
133
+ * unrecognised type resolves to `other`, so the vocabulary can widen without every caller being
134
+ * updated in the same release.
135
+ */
136
+ const TAX_ID_LABEL_KEY_BY_TYPE: Record<string, keyof TaxIdLabelByType> = {
137
+ pl_nip: 'plNip',
138
+ eu_vat: 'euVat',
139
+ }
140
+
141
+ /**
142
+ * The label a tax identifier should carry, given its type. Exported because the editor renders the
143
+ * same identifier as an input and must name it the same way this formatter does — two copies of the
144
+ * mapping is exactly how a foreign number ends up under a domestic scheme's name.
145
+ */
146
+ export function resolveTaxIdLabel(
147
+ label: string | TaxIdLabelByType | undefined,
148
+ taxIdType: string | null | undefined,
149
+ ): string | undefined {
150
+ if (!label) return undefined
151
+ if (typeof label === 'string') return label
152
+ const key = TAX_ID_LABEL_KEY_BY_TYPE[typeof taxIdType === 'string' ? taxIdType : ''] ?? 'other'
153
+ return label[key]
154
+ }
155
+
103
156
  type AddressViewProps = {
104
157
  address: AddressValue
105
158
  format: AddressFormatStrategy
@@ -107,7 +160,12 @@ type AddressViewProps = {
107
160
  lineClassName?: string
108
161
  }
109
162
 
110
- export function AddressView({ address, format, className, lineClassName }: AddressViewProps): React.ReactElement | null {
163
+ export function AddressView({
164
+ address,
165
+ format,
166
+ className,
167
+ lineClassName,
168
+ }: AddressViewProps): React.ReactElement | null {
111
169
  const lines = formatAddressLines(address, format)
112
170
  if (!lines.length) return null
113
171
  return (
@@ -1,5 +1,3 @@
1
- // @ts-nocheck
2
-
3
1
  "use client"
4
2
 
5
3
  import * as React from 'react'
@@ -72,17 +70,47 @@ const emptyDraft: AddressEditorDraft = {
72
70
  region: '',
73
71
  postalCode: '',
74
72
  country: '',
73
+ taxId: '',
74
+ taxIdType: '',
75
+ phone: '',
75
76
  isPrimary: false,
76
77
  }
77
78
 
78
79
  const EDITABLE_SNAPSHOT_KEYS = new Set(Object.keys(emptyDraft))
79
80
 
81
+ /**
82
+ * The fields the editor owns, typed, plus whatever else the caller's snapshot carried.
83
+ *
84
+ * The index signature is what lets an integration's extra keys — anything a future
85
+ * integration writes — survive the merge-back below, while the named fields stay `string` for callers
86
+ * that read them. `taxId` and `phone` are NOT among them any more: the editor renders both, so they
87
+ * are assigned from the draft like every other field. Reading
88
+ * `normalized.city` off a bare `Record<string, unknown>` yields `unknown`, which is what forced the
89
+ * `@ts-nocheck` on this file.
90
+ */
91
+ type NormalizedAddressDraft = {
92
+ name?: string
93
+ purpose?: string
94
+ companyName?: string
95
+ addressLine1?: string
96
+ addressLine2?: string
97
+ buildingNumber?: string
98
+ flatNumber?: string
99
+ city?: string
100
+ region?: string
101
+ postalCode?: string
102
+ country?: string
103
+ taxId?: string
104
+ phone?: string
105
+ isPrimary?: boolean
106
+ } & Record<string, unknown>
107
+
80
108
  function normalizeAddressDraft(
81
109
  draft?: AddressEditorDraft | null,
82
110
  previous?: Record<string, unknown> | null,
83
- ): Record<string, unknown> | null {
111
+ ): NormalizedAddressDraft | null {
84
112
  if (!draft) return null
85
- const normalized: Record<string, unknown> = {}
113
+ const normalized: NormalizedAddressDraft = {}
86
114
  let hasEditableContent = false
87
115
  const assign = (key: keyof AddressEditorDraft, target: string) => {
88
116
  const value = draft[key]
@@ -103,6 +131,9 @@ function normalizeAddressDraft(
103
131
  assign('region', 'region')
104
132
  assign('postalCode', 'postalCode')
105
133
  assign('country', 'country')
134
+ assign('taxId', 'taxId')
135
+ assign('taxIdType', 'taxIdType')
136
+ assign('phone', 'phone')
106
137
  assign('isPrimary', 'isPrimary')
107
138
  if (!hasEditableContent) return null
108
139
  if (previous) {
@@ -129,6 +160,9 @@ function draftFromSnapshot(snapshot?: Record<string, unknown> | null): AddressEd
129
160
  region: typeof record.region === 'string' ? record.region : '',
130
161
  postalCode: typeof record.postalCode === 'string' ? record.postalCode : '',
131
162
  country: typeof record.country === 'string' ? record.country : '',
163
+ taxId: typeof record.taxId === 'string' ? record.taxId : '',
164
+ taxIdType: typeof record.taxIdType === 'string' ? record.taxIdType : '',
165
+ phone: typeof record.phone === 'string' ? record.phone : '',
132
166
  isPrimary: record.isPrimary === true,
133
167
  }
134
168
  }
@@ -370,7 +404,10 @@ export function SalesDocumentAddressesSection({
370
404
  companyName: value.companyName ?? null,
371
405
  }
372
406
  })
373
- .filter((entry): entry is DocumentAddressAssignment => entry !== null)
407
+ // Narrow to non-null only. Asserting `DocumentAddressAssignment` here was wrong: its
408
+ // optional fields (`name?`, `purpose?`, …) admit `undefined`, which the mapped literal's
409
+ // `string | null` does not, so the predicate was not assignable to what it filtered.
410
+ .filter((entry): entry is NonNullable<typeof entry> => entry !== null)
374
411
  setDocumentAddresses(mapped)
375
412
  } else {
376
413
  setDocumentAddresses([])
@@ -1093,9 +1130,12 @@ export function SalesDocumentAddressesSection({
1093
1130
  <AddressEditor
1094
1131
  value={shippingDraft}
1095
1132
  format={addressFormat}
1133
+ showPhoneField
1134
+ showTaxIdField
1096
1135
  t={t as Translator}
1097
1136
  onChange={(next) => setShippingDraft(next)}
1098
1137
  hidePrimaryToggle
1138
+ disabled={locked}
1099
1139
  />
1100
1140
  <SwitchField
1101
1141
  label={t('sales.documents.form.address.saveToCustomer', 'Save this address to the customer')}
@@ -1160,9 +1200,12 @@ export function SalesDocumentAddressesSection({
1160
1200
  <AddressEditor
1161
1201
  value={billingDraft}
1162
1202
  format={addressFormat}
1203
+ showPhoneField
1204
+ showTaxIdField
1163
1205
  t={t as Translator}
1164
1206
  onChange={(next) => setBillingDraft(next)}
1165
1207
  hidePrimaryToggle
1208
+ disabled={locked}
1166
1209
  />
1167
1210
  <SwitchField
1168
1211
  label={t('sales.documents.form.address.saveToCustomer', 'Save this address to the customer')}
@@ -60,6 +60,7 @@ import { AddressEditor, type AddressEditorDraft } from '@open-mercato/core/modul
60
60
  import { useSalesChannelsEnabled } from '../useSalesChannelsEnabled'
61
61
  import { createLogger } from '@open-mercato/shared/lib/logger'
62
62
  import { SalesOrderDraftLines, createSalesOrderLineDraft, type SalesOrderLineDraft } from './SalesOrderDraftLines'
63
+ import { normalizeAddressDraft } from './normalizeAddressDraft'
63
64
 
64
65
  const logger = createLogger('sales')
65
66
 
@@ -435,29 +436,6 @@ function parseCustomerOptions(items: unknown[], kind: 'person' | 'company'): Cus
435
436
  return parsed
436
437
  }
437
438
 
438
- function normalizeAddressDraft(draft?: AddressDraft | null): Record<string, unknown> | null {
439
- if (!draft) return null
440
- const normalized: Record<string, unknown> = {}
441
- const assign = (key: keyof AddressDraft, target: string) => {
442
- const value = draft[key]
443
- if (typeof value === 'string' && value.trim().length) normalized[target] = value.trim()
444
- if (typeof value === 'boolean') normalized[target] = value
445
- }
446
- assign('name', 'name')
447
- assign('purpose', 'purpose')
448
- assign('companyName', 'companyName')
449
- assign('addressLine1', 'addressLine1')
450
- assign('addressLine2', 'addressLine2')
451
- assign('buildingNumber', 'buildingNumber')
452
- assign('flatNumber', 'flatNumber')
453
- assign('city', 'city')
454
- assign('region', 'region')
455
- assign('postalCode', 'postalCode')
456
- assign('country', 'country')
457
- assign('isPrimary', 'isPrimary')
458
- return Object.keys(normalized).length ? normalized : null
459
- }
460
-
461
439
  type DocumentNumberFieldProps = CrudCustomFieldRenderProps & { t: Translator }
462
440
 
463
441
  type BillingAddressSectionFieldProps = CrudCustomFieldRenderProps & {
@@ -655,6 +633,8 @@ function BillingAddressSectionField({ values, setFormValue, t, addressesLoading,
655
633
  t={t}
656
634
  onChange={(next) => updateValue('billingAddressDraft', next)}
657
635
  hidePrimaryToggle
636
+ showPhoneField
637
+ showTaxIdField
658
638
  />
659
639
  <SwitchField
660
640
  containerClassName="col-span-2"
@@ -1267,6 +1247,8 @@ export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind
1267
1247
  t={t}
1268
1248
  onChange={(next) => updateValue('shippingAddressDraft', next)}
1269
1249
  hidePrimaryToggle
1250
+ showPhoneField
1251
+ showTaxIdField
1270
1252
  />
1271
1253
  <SwitchField
1272
1254
  containerClassName="col-span-2"
@@ -0,0 +1,29 @@
1
+ import type { AddressEditorDraft } from '@open-mercato/core/modules/customers/components/AddressEditor'
2
+
3
+ export function normalizeAddressDraft(
4
+ draft?: AddressEditorDraft | null,
5
+ ): Record<string, unknown> | null {
6
+ if (!draft) return null
7
+ const normalized: Record<string, unknown> = {}
8
+ const assign = (key: keyof AddressEditorDraft, target: string) => {
9
+ const value = draft[key]
10
+ if (typeof value === 'string' && value.trim().length) normalized[target] = value.trim()
11
+ if (typeof value === 'boolean') normalized[target] = value
12
+ }
13
+ assign('name', 'name')
14
+ assign('purpose', 'purpose')
15
+ assign('companyName', 'companyName')
16
+ assign('addressLine1', 'addressLine1')
17
+ assign('addressLine2', 'addressLine2')
18
+ assign('buildingNumber', 'buildingNumber')
19
+ assign('flatNumber', 'flatNumber')
20
+ assign('city', 'city')
21
+ assign('region', 'region')
22
+ assign('postalCode', 'postalCode')
23
+ assign('country', 'country')
24
+ assign('taxId', 'taxId')
25
+ assign('taxIdType', 'taxIdType')
26
+ assign('phone', 'phone')
27
+ assign('isPrimary', 'isPrimary')
28
+ return Object.keys(normalized).length ? normalized : null
29
+ }