@aglyn/aglyn 1.0.0-beta.246 → 1.0.0-beta.247

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 (55) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/contact-custom-fields.d.ts +23 -1
  3. package/src/lib/app-utils/contact-custom-fields.js +84 -2
  4. package/src/lib/app-utils/contact-custom-fields.js.map +1 -1
  5. package/src/lib/app-utils/crm.d.ts +7 -1
  6. package/src/lib/app-utils/crm.js.map +1 -1
  7. package/src/lib/app-utils/default-site.js +21 -33
  8. package/src/lib/app-utils/default-site.js.map +1 -1
  9. package/src/lib/app-utils/docs-help-sections.generated.js +8 -8
  10. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  11. package/src/lib/app-utils/docs-help.generated.d.ts +8 -8
  12. package/src/lib/app-utils/docs-help.generated.js +9 -5
  13. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  14. package/src/lib/app-utils/docs-index.generated.js +129 -59
  15. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  16. package/src/lib/app-utils/element-pick-protocol.d.ts +54 -0
  17. package/src/lib/app-utils/element-pick-protocol.js +41 -0
  18. package/src/lib/app-utils/element-pick-protocol.js.map +1 -0
  19. package/src/lib/app-utils/element-pick-target.d.ts +32 -0
  20. package/src/lib/app-utils/element-pick-target.js +47 -0
  21. package/src/lib/app-utils/element-pick-target.js.map +1 -0
  22. package/src/lib/app-utils/error-beacon.d.ts +19 -0
  23. package/src/lib/app-utils/error-beacon.js +31 -5
  24. package/src/lib/app-utils/error-beacon.js.map +1 -1
  25. package/src/lib/app-utils/form-contract.js +13 -6
  26. package/src/lib/app-utils/form-contract.js.map +1 -1
  27. package/src/lib/app-utils/forms.d.ts +31 -0
  28. package/src/lib/app-utils/forms.js +213 -16
  29. package/src/lib/app-utils/forms.js.map +1 -1
  30. package/src/lib/app-utils/node-definition-sanitizer.js +12 -0
  31. package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -1
  32. package/src/lib/app-utils/seo-locale.d.ts +14 -0
  33. package/src/lib/app-utils/seo-locale.js +15 -0
  34. package/src/lib/app-utils/seo-locale.js.map +1 -1
  35. package/src/lib/app-utils/seo-site-scan.js +18 -3
  36. package/src/lib/app-utils/seo-site-scan.js.map +1 -1
  37. package/src/lib/app-utils/site-live-url.d.ts +24 -0
  38. package/src/lib/app-utils/site-live-url.js +60 -0
  39. package/src/lib/app-utils/site-live-url.js.map +1 -0
  40. package/src/lib/app-utils/starter-template-nodes.d.ts +13 -0
  41. package/src/lib/app-utils/starter-template-nodes.js +40 -0
  42. package/src/lib/app-utils/starter-template-nodes.js.map +1 -1
  43. package/src/lib/app-utils/starter-templates.d.ts +1 -1
  44. package/src/lib/app-utils/starter-templates.js +19 -31
  45. package/src/lib/app-utils/starter-templates.js.map +1 -1
  46. package/src/lib/plugin-manager/feature-plugins.d.ts +74 -3
  47. package/src/lib/plugin-manager/feature-plugins.js +9 -0
  48. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  49. package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +11 -0
  50. package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -1
  51. package/src/lib/plugin-manager/plugin-resource-drafts.d.ts +23 -0
  52. package/src/lib/plugin-manager/plugin-resource-drafts.js.map +1 -1
  53. package/src/lib/plugin-manager/stock-photo-provider.d.ts +67 -0
  54. package/src/lib/plugin-manager/stock-photo-provider.js +14 -1
  55. package/src/lib/plugin-manager/stock-photo-provider.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/aglyn",
3
- "version": "1.0.0-beta.246",
3
+ "version": "1.0.0-beta.247",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -37,16 +37,16 @@
37
37
  "./package.json": "./package.json"
38
38
  },
39
39
  "dependencies": {
40
- "@aglyn/shared-data-enums": "1.0.0-beta.246",
41
- "@aglyn/shared-data-mdi": "1.0.0-beta.246",
42
- "@aglyn/shared-data-types": "1.0.0-beta.246",
43
- "@aglyn/shared-util-email": "1.0.0-beta.246",
44
- "@aglyn/shared-util-first-touch": "1.0.0-beta.246",
45
- "@aglyn/shared-util-http": "1.0.0-beta.246",
46
- "@aglyn/shared-util-logger": "1.0.0-beta.246",
47
- "@aglyn/shared-util-timestamp": "1.0.0-beta.246",
48
- "@aglyn/shared-util-tools": "1.0.0-beta.246",
49
- "@aglyn/shared-util-vendor": "1.0.0-beta.246",
40
+ "@aglyn/shared-data-enums": "1.0.0-beta.247",
41
+ "@aglyn/shared-data-mdi": "1.0.0-beta.247",
42
+ "@aglyn/shared-data-types": "1.0.0-beta.247",
43
+ "@aglyn/shared-util-email": "1.0.0-beta.247",
44
+ "@aglyn/shared-util-first-touch": "1.0.0-beta.247",
45
+ "@aglyn/shared-util-http": "1.0.0-beta.247",
46
+ "@aglyn/shared-util-logger": "1.0.0-beta.247",
47
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.247",
48
+ "@aglyn/shared-util-tools": "1.0.0-beta.247",
49
+ "@aglyn/shared-util-vendor": "1.0.0-beta.247",
50
50
  "@data-driven-forms/react-form-renderer": "^4.2.0",
51
51
  "@msgpack/msgpack": "^3.1.3",
52
52
  "@types/unist": "^3.0.3",
@@ -49,6 +49,25 @@ export declare const CONTACT_FIELD_TYPES: readonly ContactFieldType[];
49
49
  /** How a field type reads on screen — typed so a type cannot ship unlabeled. */
50
50
  export declare const CONTACT_FIELD_TYPE_LABELS: Record<ContactFieldType, string>;
51
51
  export declare function isContactFieldType(value: unknown): value is ContactFieldType;
52
+ /** The currency a currency field is in when nothing names one: a deal's default. */
53
+ export declare const CONTACT_FIELD_DEFAULT_CURRENCY = "usd";
54
+ /** A currency field's code, lowercase, or the default when it names none. */
55
+ export declare function customFieldCurrency(definition: Pick<ContactFieldDefinition, 'currency'> | null | undefined): string;
56
+ /** Whether `code` is an ISO 4217 code `Intl` can format. */
57
+ export declare function isCustomFieldCurrency(code: unknown): code is string;
58
+ /** How many decimal places a currency's minor unit is — 2 for USD, 0 for JPY — as `Intl` knows it. */
59
+ export declare function currencyFractionDigits(code: string): number;
60
+ /** A stored amount (minor units) as the major-unit number a person types, `1250.5` for 125050 cents. */
61
+ export declare function currencyMinorToMajor(minor: number, code: string): number;
62
+ /**
63
+ * A typed amount — "$1,250.50", "1250.5", "-40" — as the minor units it
64
+ * stores, rounded to the currency's unit, or `null` when it is not one
65
+ * number. Symbols, codes, spaces and thousands separators are dropped, as
66
+ * a deal's amount field drops them.
67
+ */
68
+ export declare function parseCurrencyInput(input: unknown, code: string): number | null;
69
+ /** A stored amount as money in the reader's locale — "$1,250.50" — or the plain number when `Intl` cannot. */
70
+ export declare function formatCurrencyFieldValue(minor: number, code: string, locale?: string): string;
52
71
  /**
53
72
  * Definitions in the order a form, a column set and an export show them.
54
73
  *
@@ -92,8 +111,11 @@ export declare function activeContactFieldDefinitions<T extends Pick<ContactFiel
92
111
  * an export reads as a date, whichever locale typed it.
93
112
  * - `select` stores one of the declared options, exactly, or nothing: a
94
113
  * choice outside the list would be a value the picker could never show.
114
+ * - `currency` stores a whole number of the field's minor unit: a number
115
+ * as given (rounded), which is the stored shape an API reads back, and
116
+ * text — a form's post, "$1,250.50" — read as major units.
95
117
  */
96
- export declare function coerceContactCustomValue(definition: Pick<ContactFieldDefinition, 'type' | 'options'>, raw: unknown): ContactCustomValue | undefined;
118
+ export declare function coerceContactCustomValue(definition: Pick<ContactFieldDefinition, 'type' | 'options' | 'currency'>, raw: unknown): ContactCustomValue | undefined;
97
119
  /**
98
120
  * What a form submission writes into the contact's `custom`: one entry per
99
121
  * declared field that names a contact field, coerced by that field's type.
@@ -47,7 +47,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
47
47
  'date',
48
48
  'select',
49
49
  'checkbox',
50
- 'url'
50
+ 'url',
51
+ 'currency'
51
52
  ];
52
53
  /** How a field type reads on screen — typed so a type cannot ship unlabeled. */ export const CONTACT_FIELD_TYPE_LABELS = {
53
54
  text: 'Text',
@@ -55,11 +56,78 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
55
56
  date: 'Date',
56
57
  select: 'Choice',
57
58
  checkbox: 'Checkbox',
58
- url: 'Link'
59
+ url: 'Link',
60
+ currency: 'Currency'
59
61
  };
60
62
  export function isContactFieldType(value) {
61
63
  return typeof value === 'string' && CONTACT_FIELD_TYPES.includes(value);
62
64
  }
65
+ /*------------------------------------------
66
+ * CURRENCY FIELDS (AGL-3756).
67
+ *
68
+ * A currency field's value is a WHOLE NUMBER of its currency's minor unit —
69
+ * cents for USD, yen for JPY — the convention every amount in the CRM
70
+ * already keeps (a deal's `amountCents`, a company's `annualRevenueCents`)
71
+ * and the import engine's `CurrencyAmount.amountMinor`. The currency is the
72
+ * definition's (`currency`, lowercase ISO 4217), never the value's, so a
73
+ * column of one field is always one currency and sorts and filters as
74
+ * plain numbers.
75
+ *-----------------------------------------*/ /** The currency a currency field is in when nothing names one: a deal's default. */ export const CONTACT_FIELD_DEFAULT_CURRENCY = 'usd';
76
+ /** A currency field's code, lowercase, or the default when it names none. */ export function customFieldCurrency(definition) {
77
+ var _ref;
78
+ const code = String((_ref = definition == null ? void 0 : definition.currency) != null ? _ref : '').trim().toLowerCase();
79
+ return /^[a-z]{3}$/.test(code) ? code : CONTACT_FIELD_DEFAULT_CURRENCY;
80
+ }
81
+ /** Whether `code` is an ISO 4217 code `Intl` can format. */ export function isCustomFieldCurrency(code) {
82
+ if (typeof code !== 'string' || !/^[a-z]{3}$/i.test(code.trim())) return false;
83
+ try {
84
+ new Intl.NumberFormat('en-US', {
85
+ style: 'currency',
86
+ currency: code.trim().toUpperCase()
87
+ });
88
+ return true;
89
+ } catch (unused) {
90
+ return false;
91
+ }
92
+ }
93
+ /** How many decimal places a currency's minor unit is — 2 for USD, 0 for JPY — as `Intl` knows it. */ export function currencyFractionDigits(code) {
94
+ try {
95
+ var _resolvedOptions_maximumFractionDigits;
96
+ return (_resolvedOptions_maximumFractionDigits = new Intl.NumberFormat('en-US', {
97
+ style: 'currency',
98
+ currency: code.toUpperCase()
99
+ }).resolvedOptions().maximumFractionDigits) != null ? _resolvedOptions_maximumFractionDigits : 2;
100
+ } catch (unused) {
101
+ return 2;
102
+ }
103
+ }
104
+ /** A stored amount (minor units) as the major-unit number a person types, `1250.5` for 125050 cents. */ export function currencyMinorToMajor(minor, code) {
105
+ return minor / 10 ** currencyFractionDigits(code);
106
+ }
107
+ /**
108
+ * A typed amount — "$1,250.50", "1250.5", "-40" — as the minor units it
109
+ * stores, rounded to the currency's unit, or `null` when it is not one
110
+ * number. Symbols, codes, spaces and thousands separators are dropped, as
111
+ * a deal's amount field drops them.
112
+ */ export function parseCurrencyInput(input, code) {
113
+ const cleaned = String(input != null ? input : '').replace(/[^0-9.-]/g, '').trim();
114
+ if (!cleaned || !/^-?\d*\.?\d*$/.test(cleaned) || cleaned === '-' || cleaned === '.') return null;
115
+ const value = Number(cleaned);
116
+ if (!Number.isFinite(value)) return null;
117
+ return Math.round(value * 10 ** currencyFractionDigits(code));
118
+ }
119
+ /** A stored amount as money in the reader's locale — "$1,250.50" — or the plain number when `Intl` cannot. */ export function formatCurrencyFieldValue(minor, code, locale) {
120
+ const upper = code.toUpperCase();
121
+ const major = currencyMinorToMajor(minor, code);
122
+ try {
123
+ return new Intl.NumberFormat(locale, {
124
+ style: 'currency',
125
+ currency: upper
126
+ }).format(major);
127
+ } catch (unused) {
128
+ return `${major.toFixed(currencyFractionDigits(code))} ${upper}`;
129
+ }
130
+ }
63
131
  /** The longest string a text or link field stores. */ const CONTACT_TEXT_VALUE_MAX = 2000;
64
132
  /**
65
133
  * Definitions in the order a form, a column set and an export show them.
@@ -123,6 +191,9 @@ export function isContactFieldType(value) {
123
191
  * an export reads as a date, whichever locale typed it.
124
192
  * - `select` stores one of the declared options, exactly, or nothing: a
125
193
  * choice outside the list would be a value the picker could never show.
194
+ * - `currency` stores a whole number of the field's minor unit: a number
195
+ * as given (rounded), which is the stored shape an API reads back, and
196
+ * text — a form's post, "$1,250.50" — read as major units.
126
197
  */ export function coerceContactCustomValue(definition, raw) {
127
198
  if (raw === undefined || raw === null) return undefined;
128
199
  switch(definition.type){
@@ -151,6 +222,15 @@ export function isContactFieldType(value) {
151
222
  const parsed = Number(text);
152
223
  return Number.isFinite(parsed) ? parsed : undefined;
153
224
  }
225
+ case 'currency':
226
+ {
227
+ // A number is minor units, the stored shape an API client reads back;
228
+ // text is what a person typed or a form posted, in major units.
229
+ if (typeof raw === 'number') return Number.isFinite(raw) ? Math.round(raw) : undefined;
230
+ if (typeof raw === 'boolean' || typeof raw === 'object') return undefined;
231
+ const minor = parseCurrencyInput(raw, customFieldCurrency(definition));
232
+ return minor === null ? undefined : minor;
233
+ }
154
234
  case 'checkbox':
155
235
  {
156
236
  if (typeof raw === 'boolean') return raw;
@@ -260,6 +340,8 @@ export function isContactFieldType(value) {
260
340
  switch(definition.type){
261
341
  case 'number':
262
342
  return 'Must be a number';
343
+ case 'currency':
344
+ return `Must be a whole number of ${customFieldCurrency(definition).toUpperCase()} minor units, or an amount as text`;
263
345
  case 'checkbox':
264
346
  return 'Must be true or false';
265
347
  case 'date':
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/contact-custom-fields.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Custom contact fields (AGL-2601): what a VALUE under a definition may be,\n * and the three doors a value comes in through.\n *\n * `crm.ts` declares the definition — `ContactFieldDefinition`, its key\n * grammar and its collection. This module is everything that happens between\n * a definition and a value: a form submission carrying a mapped field, an API\n * body carrying a `custom` map, and a publish that must not lose the mapping\n * an author drew. Each door coerces by the definition's TYPE, and the rule is\n * one function so a number typed into a form and a number sent over the API\n * cannot be stored as different things.\n *\n * Pure like the module it extends: no Firestore, no React. The bound on how\n * many definitions one org keeps is stated here because every reader of the\n * collection — the console hook, the submit route, the API — has to read it\n * whole, and a reader without a limit is a reader whose cost is somebody\n * else's decision.\n */\n\nimport {\n type ContactCustomValue,\n type ContactFieldDefinition,\n type ContactFieldType,\n type CrmFieldObject,\n fieldDefinitionObject,\n} from './crm'\nimport type { FormFieldDecl } from './forms'\n\n/**\n * How many field definitions one organization may hold, and therefore the\n * `limit()` every reader of `orgs/{orgId}/contactFields` applies.\n *\n * The collection is read WHOLE — there is no index on it and the list is\n * sorted by `order` in memory — so this is the ceiling on that read. A\n * hundred is far past what a profile form can carry; an org that wants more\n * has outgrown a per-contact map and needs a dataset.\n */\nexport const CONTACT_FIELDS_MAX_PER_ORG = 100\n\n/** Every value type a definition may declare, in the order a picker lists them. */\nexport const CONTACT_FIELD_TYPES: readonly ContactFieldType[] = [\n 'text',\n 'number',\n 'date',\n 'select',\n 'checkbox',\n 'url',\n]\n\n/** How a field type reads on screen — typed so a type cannot ship unlabeled. */\nexport const CONTACT_FIELD_TYPE_LABELS: Record<ContactFieldType, string> = {\n text: 'Text',\n number: 'Number',\n date: 'Date',\n select: 'Choice',\n checkbox: 'Checkbox',\n url: 'Link',\n}\n\nexport function isContactFieldType(value: unknown): value is ContactFieldType {\n return (\n typeof value === 'string' &&\n (CONTACT_FIELD_TYPES as readonly string[]).includes(value)\n )\n}\n\n/** The longest string a text or link field stores. */\nconst CONTACT_TEXT_VALUE_MAX = 2000\n\n/**\n * Definitions in the order a form, a column set and an export show them.\n *\n * `order` first, then the key, so two definitions that were given the same\n * position — which a reorder in flight can produce for one write — still\n * come out in one stable order on every surface rather than flickering.\n */\nexport function sortContactFieldDefinitions<\n T extends Pick<ContactFieldDefinition, 'order' | 'key'>,\n>(definitions: readonly T[]): T[] {\n return [...definitions].sort(\n (a, b) =>\n (Number(a.order) || 0) - (Number(b.order) || 0) ||\n a.key.localeCompare(b.key),\n )\n}\n\n/**\n * The definitions that describe ONE object, in `order` (AGL-2661).\n *\n * Every reader of the collection gets it whole — it is one bounded read —\n * and narrows here, through the one reader of `object`, so a definition\n * written before the key existed lands on the contacts tab and nowhere\n * else. Keys are unique within what this returns, not across it.\n */\nexport function fieldDefinitionsForObject<\n T extends Pick<ContactFieldDefinition, 'order' | 'key' | 'object'>,\n>(definitions: readonly T[], object: CrmFieldObject): T[] {\n return sortContactFieldDefinitions(\n definitions.filter((definition) => fieldDefinitionObject(definition) === object),\n )\n}\n\n/** The definitions a value may still be WRITTEN under — sorted, not retired. */\nexport function activeContactFieldDefinitions<\n T extends Pick<ContactFieldDefinition, 'order' | 'key' | 'retiredAt'>,\n>(definitions: readonly T[]): T[] {\n return sortContactFieldDefinitions(\n definitions.filter((definition) => !definition.retiredAt),\n )\n}\n\n/** Checkbox values a browser form actually posts for a ticked box. */\nconst AFFIRMATIVE = new Set(['true', 'on', 'yes', '1', 'checked'])\n/** And what it posts, or what a client sends, for an unticked one. */\nconst NEGATIVE = new Set(['false', 'off', 'no', '0', 'unchecked'])\n\n/**\n * One raw value, as the definition's type stores it — or `undefined` when\n * nothing usable was given.\n *\n * `undefined` and `null` are different answers on purpose. `null` is the\n * explicit \"cleared\" that keeps the key present for a `where` clause to find;\n * it is what a console save or an API body writes when it MEANS to clear.\n * `undefined` is \"there is no value here\", which a form submission produces\n * for a field left blank — and a blank field on a form must not clear a\n * value the merchant set by hand, so the caller drops the key rather than\n * writing it.\n *\n * - `text` and `url` store the trimmed string, capped. A link has to parse\n * as an `http(s)` URL, because the column renders it as one and a bare\n * word would be a broken anchor on every row.\n * - `number` stores a finite number. A numeric STRING is accepted because a\n * form posts every field as text, but anything that is not one number is\n * dropped rather than stored as `NaN`, which Firestore would refuse.\n * - `checkbox` stores a boolean, reading the strings a browser posts for a\n * ticked or an unticked box.\n * - `date` stores an ISO 8601 string, so a date filter compares as text and\n * an export reads as a date, whichever locale typed it.\n * - `select` stores one of the declared options, exactly, or nothing: a\n * choice outside the list would be a value the picker could never show.\n */\nexport function coerceContactCustomValue(\n definition: Pick<ContactFieldDefinition, 'type' | 'options'>,\n raw: unknown,\n): ContactCustomValue | undefined {\n if (raw === undefined || raw === null) return undefined\n switch (definition.type) {\n case 'text': {\n const text = String(raw).trim().slice(0, CONTACT_TEXT_VALUE_MAX)\n return text ? text : undefined\n }\n case 'url': {\n const text = String(raw).trim().slice(0, CONTACT_TEXT_VALUE_MAX)\n if (!text) return undefined\n try {\n const url = new URL(text)\n return url.protocol === 'http:' || url.protocol === 'https:'\n ? text\n : undefined\n } catch {\n return undefined\n }\n }\n case 'number': {\n if (typeof raw === 'number') return Number.isFinite(raw) ? raw : undefined\n if (typeof raw === 'boolean') return undefined\n const text = String(raw).trim()\n if (!text) return undefined\n const parsed = Number(text)\n return Number.isFinite(parsed) ? parsed : undefined\n }\n case 'checkbox': {\n if (typeof raw === 'boolean') return raw\n const text = String(raw).trim().toLowerCase()\n if (AFFIRMATIVE.has(text)) return true\n if (NEGATIVE.has(text)) return false\n return undefined\n }\n case 'date': {\n if (typeof raw === 'boolean') return undefined\n const ms =\n typeof raw === 'number'\n ? raw\n : raw instanceof Date\n ? raw.getTime()\n : Date.parse(String(raw).trim())\n return Number.isFinite(ms) ? new Date(ms).toISOString() : undefined\n }\n case 'select': {\n const text = String(raw).trim()\n const options = Array.isArray(definition.options) ? definition.options : []\n return text && options.includes(text) ? text : undefined\n }\n default:\n return undefined\n }\n}\n\n/**\n * What a form submission writes into the contact's `custom`: one entry per\n * declared field that names a contact field, coerced by that field's type.\n *\n * Only ACTIVE definitions receive a value — a mapping onto a retired field\n * is a mapping the author has not yet removed, and writing under a retired\n * key would put a value where no surface offers to show it. A field the\n * visitor left blank contributes nothing (see `coerceContactCustomValue`),\n * so a submission never clears a value a merchant set by hand.\n *\n * Two declared fields mapped onto one key is an authoring mistake this\n * function does not refuse; the FIRST declared field to yield a value wins,\n * in declaration order, so the answer is at least stable.\n */\nexport function collectMappedContactCustom(options: {\n /** The submitted values, keyed by the declared `fieldName`. */\n fields: Record<string, unknown>\n /** The form's declaration, as stored on its document. */\n decls: readonly FormFieldDecl[] | null | undefined\n /** The org's definitions, retired ones included — they are filtered here. */\n definitions: readonly ContactFieldDefinition[]\n}): Record<string, ContactCustomValue> {\n const active = new Map(\n activeContactFieldDefinitions(options.definitions).map((definition) => [\n definition.key,\n definition,\n ]),\n )\n const custom: Record<string, ContactCustomValue> = {}\n for (const decl of options.decls ?? []) {\n const key = decl.contactFieldKey\n if (!key || key in custom) continue\n const definition = active.get(key)\n if (!definition) continue\n const value = coerceContactCustomValue(definition, options.fields[decl.fieldName])\n if (value !== undefined) custom[key] = value\n }\n return custom\n}\n\n/**\n * The declaration a publish writes, with the mappings the author drew on the\n * previous one carried across.\n *\n * `formFieldDeclsFromNodes` reads a declaration off the DESIGN — the names,\n * types and options the nodes carry — and knows nothing about where a field\n * saves to, because that is edited on the form's own page rather than drawn\n * on the canvas. A publish that replaced the stored `fields` wholesale would\n * therefore drop every mapping on every publish, and the author would find\n * their profile fields silently empty after the next design change.\n *\n * Carried BY FIELD NAME, which is the submission key and the identity a\n * declared field has. A field renamed on the canvas is a new field and starts\n * unmapped; a field removed takes its mapping with it. Nothing else on the\n * previous declaration survives — the design is still the source of truth\n * for everything it draws.\n */\nexport function carryContactFieldMappings(\n previous: readonly FormFieldDecl[] | null | undefined,\n next: readonly FormFieldDecl[],\n): FormFieldDecl[] {\n const carried = new Map<string, string>()\n for (const decl of previous ?? []) {\n if (decl?.fieldName && decl.contactFieldKey) {\n carried.set(decl.fieldName, decl.contactFieldKey)\n }\n }\n return next.map((decl) => {\n const key = carried.get(decl.fieldName)\n return key ? { ...decl, contactFieldKey: key } : decl\n })\n}\n\n/**\n * The declaration with ONE field's destination changed: `key` maps it onto\n * that custom field, `null` unmaps it.\n *\n * Unmapping DELETES the property rather than setting it to `undefined`. The\n * declaration is written back to the form document as one array, and an\n * `undefined` inside an array is a value Firestore refuses outright — the\n * save would fail on the field the author just cleared, which is the one\n * they are least likely to suspect. Every other field, and every other\n * property of this one, comes through untouched: the design is still the\n * source of everything else on a declared field.\n */\nexport function withContactFieldMapping(\n decls: readonly FormFieldDecl[],\n fieldName: string,\n key: string | null,\n): FormFieldDecl[] {\n return decls.map((decl) => {\n if (decl.fieldName !== fieldName) return decl\n const next: FormFieldDecl = { ...decl }\n delete next.contactFieldKey\n return key ? { ...next, contactFieldKey: key } : next\n })\n}\n\n/** What one coercion refusal tells an API caller, per type. */\nfunction customValueExpectation(\n definition: Pick<ContactFieldDefinition, 'type' | 'options'>,\n): string {\n switch (definition.type) {\n case 'number':\n return 'Must be a number'\n case 'checkbox':\n return 'Must be true or false'\n case 'date':\n return 'Must be an ISO 8601 date'\n case 'url':\n return 'Must be an http(s) URL'\n case 'select':\n return `Must be one of: ${(definition.options ?? []).join(', ')}`\n default:\n return 'Must be text'\n }\n}\n\n/**\n * A `custom` map sent to the API, validated against the org's definitions.\n *\n * Unknown keys are REFUSED rather than dropped, following the contact\n * resource's own rule for its top-level keys: a silent drop reads as \"we\n * stored your value\" when nothing was stored, and an integration pushing a\n * field it spelled wrong would never learn. Each refusal is named\n * `custom.<key>` so a client can match it to the entry it sent.\n *\n * A retired field is refused too. Its values are still READ — an export has\n * to show what was written under it — but nothing writes under a key the\n * console no longer offers, or the field could never actually go away.\n *\n * `null` is accepted for every type and means \"clear\": the key stays present\n * with an explicit nothing, which is the shape the console writes and the\n * shape a `where` clause can find.\n */\nexport function readContactCustomInput(\n raw: unknown,\n definitions: readonly ContactFieldDefinition[],\n /**\n * The object the map is written on, for the refusals' wording — the\n * caller has already narrowed `definitions` to it. Contacts when unsaid.\n */\n object: CrmFieldObject = 'contact',\n): { values: Record<string, ContactCustomValue> } | { errors: Record<string, string> } {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n return { errors: { custom: 'Must be an object of field values keyed by field key' } }\n }\n const byKey = new Map(\n sortContactFieldDefinitions(definitions).map((definition) => [\n definition.key,\n definition,\n ]),\n )\n const values: Record<string, ContactCustomValue> = {}\n const errors: Record<string, string> = {}\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n const definition = byKey.get(key)\n if (!definition) {\n errors[`custom.${key}`] = `No such ${object} field`\n continue\n }\n if (definition.retiredAt) {\n errors[`custom.${key}`] = `Retired ${object} field — restore it to write it`\n continue\n }\n if (value === null) {\n values[key] = null\n continue\n }\n const coerced = coerceContactCustomValue(definition, value)\n if (coerced === undefined) {\n errors[`custom.${key}`] = customValueExpectation(definition)\n continue\n }\n values[key] = coerced\n }\n return Object.keys(errors).length ? { errors } : { values }\n}\n\n/**\n * `readContactCustomInput` for ANY object (AGL-2661): the org's whole\n * definition list in, the ones describing `object` judged.\n *\n * The narrowing happens here rather than at every caller so a company\n * body cannot be validated against a contact field of the same key —\n * `region` on both is two fields, and a value sent to one must never be\n * read by the other's type. What comes back is exactly what the contact\n * reader answers: a map to store, or refusals named `custom.<key>`.\n */\nexport function readCrmCustomInput(\n raw: unknown,\n definitions: readonly ContactFieldDefinition[],\n object: CrmFieldObject,\n): { values: Record<string, ContactCustomValue> } | { errors: Record<string, string> } {\n return readContactCustomInput(raw, fieldDefinitionsForObject(definitions, object), object)\n}\n"],"names":["fieldDefinitionObject","CONTACT_FIELDS_MAX_PER_ORG","CONTACT_FIELD_TYPES","CONTACT_FIELD_TYPE_LABELS","text","number","date","select","checkbox","url","isContactFieldType","value","includes","CONTACT_TEXT_VALUE_MAX","sortContactFieldDefinitions","definitions","sort","a","b","Number","order","key","localeCompare","fieldDefinitionsForObject","object","filter","definition","activeContactFieldDefinitions","retiredAt","AFFIRMATIVE","Set","NEGATIVE","coerceContactCustomValue","raw","undefined","type","String","trim","slice","URL","protocol","isFinite","parsed","toLowerCase","has","ms","Date","getTime","parse","toISOString","options","Array","isArray","collectMappedContactCustom","active","Map","map","custom","decl","decls","contactFieldKey","get","fields","fieldName","carryContactFieldMappings","previous","next","carried","set","withContactFieldMapping","customValueExpectation","join","readContactCustomInput","errors","byKey","values","Object","entries","coerced","keys","length","readCrmCustomInput"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/contact-custom-fields.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Custom contact fields (AGL-2601): what a VALUE under a definition may be,\n * and the three doors a value comes in through.\n *\n * `crm.ts` declares the definition — `ContactFieldDefinition`, its key\n * grammar and its collection. This module is everything that happens between\n * a definition and a value: a form submission carrying a mapped field, an API\n * body carrying a `custom` map, and a publish that must not lose the mapping\n * an author drew. Each door coerces by the definition's TYPE, and the rule is\n * one function so a number typed into a form and a number sent over the API\n * cannot be stored as different things.\n *\n * Pure like the module it extends: no Firestore, no React. The bound on how\n * many definitions one org keeps is stated here because every reader of the\n * collection — the console hook, the submit route, the API — has to read it\n * whole, and a reader without a limit is a reader whose cost is somebody\n * else's decision.\n */\n\nimport {\n type ContactCustomValue,\n type ContactFieldDefinition,\n type ContactFieldType,\n type CrmFieldObject,\n fieldDefinitionObject,\n} from './crm'\nimport type { FormFieldDecl } from './forms'\n\n/**\n * How many field definitions one organization may hold, and therefore the\n * `limit()` every reader of `orgs/{orgId}/contactFields` applies.\n *\n * The collection is read WHOLE — there is no index on it and the list is\n * sorted by `order` in memory — so this is the ceiling on that read. A\n * hundred is far past what a profile form can carry; an org that wants more\n * has outgrown a per-contact map and needs a dataset.\n */\nexport const CONTACT_FIELDS_MAX_PER_ORG = 100\n\n/** Every value type a definition may declare, in the order a picker lists them. */\nexport const CONTACT_FIELD_TYPES: readonly ContactFieldType[] = [\n 'text',\n 'number',\n 'date',\n 'select',\n 'checkbox',\n 'url',\n 'currency',\n]\n\n/** How a field type reads on screen — typed so a type cannot ship unlabeled. */\nexport const CONTACT_FIELD_TYPE_LABELS: Record<ContactFieldType, string> = {\n text: 'Text',\n number: 'Number',\n date: 'Date',\n select: 'Choice',\n checkbox: 'Checkbox',\n url: 'Link',\n currency: 'Currency',\n}\n\nexport function isContactFieldType(value: unknown): value is ContactFieldType {\n return (\n typeof value === 'string' &&\n (CONTACT_FIELD_TYPES as readonly string[]).includes(value)\n )\n}\n\n/*------------------------------------------\n * CURRENCY FIELDS (AGL-3756).\n *\n * A currency field's value is a WHOLE NUMBER of its currency's minor unit —\n * cents for USD, yen for JPY — the convention every amount in the CRM\n * already keeps (a deal's `amountCents`, a company's `annualRevenueCents`)\n * and the import engine's `CurrencyAmount.amountMinor`. The currency is the\n * definition's (`currency`, lowercase ISO 4217), never the value's, so a\n * column of one field is always one currency and sorts and filters as\n * plain numbers.\n *-----------------------------------------*/\n\n/** The currency a currency field is in when nothing names one: a deal's default. */\nexport const CONTACT_FIELD_DEFAULT_CURRENCY = 'usd'\n\n/** A currency field's code, lowercase, or the default when it names none. */\nexport function customFieldCurrency(\n definition: Pick<ContactFieldDefinition, 'currency'> | null | undefined,\n): string {\n const code = String(definition?.currency ?? '').trim().toLowerCase()\n return /^[a-z]{3}$/.test(code) ? code : CONTACT_FIELD_DEFAULT_CURRENCY\n}\n\n/** Whether `code` is an ISO 4217 code `Intl` can format. */\nexport function isCustomFieldCurrency(code: unknown): code is string {\n if (typeof code !== 'string' || !/^[a-z]{3}$/i.test(code.trim())) return false\n try {\n new Intl.NumberFormat('en-US', { style: 'currency', currency: code.trim().toUpperCase() })\n return true\n } catch {\n return false\n }\n}\n\n/** How many decimal places a currency's minor unit is — 2 for USD, 0 for JPY — as `Intl` knows it. */\nexport function currencyFractionDigits(code: string): number {\n try {\n return (\n new Intl.NumberFormat('en-US', { style: 'currency', currency: code.toUpperCase() }).resolvedOptions()\n .maximumFractionDigits ?? 2\n )\n } catch {\n return 2\n }\n}\n\n/** A stored amount (minor units) as the major-unit number a person types, `1250.5` for 125050 cents. */\nexport function currencyMinorToMajor(minor: number, code: string): number {\n return minor / 10 ** currencyFractionDigits(code)\n}\n\n/**\n * A typed amount — \"$1,250.50\", \"1250.5\", \"-40\" — as the minor units it\n * stores, rounded to the currency's unit, or `null` when it is not one\n * number. Symbols, codes, spaces and thousands separators are dropped, as\n * a deal's amount field drops them.\n */\nexport function parseCurrencyInput(input: unknown, code: string): number | null {\n const cleaned = String(input ?? '')\n .replace(/[^0-9.-]/g, '')\n .trim()\n if (!cleaned || !/^-?\\d*\\.?\\d*$/.test(cleaned) || cleaned === '-' || cleaned === '.') return null\n const value = Number(cleaned)\n if (!Number.isFinite(value)) return null\n return Math.round(value * 10 ** currencyFractionDigits(code))\n}\n\n/** A stored amount as money in the reader's locale — \"$1,250.50\" — or the plain number when `Intl` cannot. */\nexport function formatCurrencyFieldValue(minor: number, code: string, locale?: string): string {\n const upper = code.toUpperCase()\n const major = currencyMinorToMajor(minor, code)\n try {\n return new Intl.NumberFormat(locale, { style: 'currency', currency: upper }).format(major)\n } catch {\n return `${major.toFixed(currencyFractionDigits(code))} ${upper}`\n }\n}\n\n/** The longest string a text or link field stores. */\nconst CONTACT_TEXT_VALUE_MAX = 2000\n\n/**\n * Definitions in the order a form, a column set and an export show them.\n *\n * `order` first, then the key, so two definitions that were given the same\n * position — which a reorder in flight can produce for one write — still\n * come out in one stable order on every surface rather than flickering.\n */\nexport function sortContactFieldDefinitions<\n T extends Pick<ContactFieldDefinition, 'order' | 'key'>,\n>(definitions: readonly T[]): T[] {\n return [...definitions].sort(\n (a, b) =>\n (Number(a.order) || 0) - (Number(b.order) || 0) ||\n a.key.localeCompare(b.key),\n )\n}\n\n/**\n * The definitions that describe ONE object, in `order` (AGL-2661).\n *\n * Every reader of the collection gets it whole — it is one bounded read —\n * and narrows here, through the one reader of `object`, so a definition\n * written before the key existed lands on the contacts tab and nowhere\n * else. Keys are unique within what this returns, not across it.\n */\nexport function fieldDefinitionsForObject<\n T extends Pick<ContactFieldDefinition, 'order' | 'key' | 'object'>,\n>(definitions: readonly T[], object: CrmFieldObject): T[] {\n return sortContactFieldDefinitions(\n definitions.filter((definition) => fieldDefinitionObject(definition) === object),\n )\n}\n\n/** The definitions a value may still be WRITTEN under — sorted, not retired. */\nexport function activeContactFieldDefinitions<\n T extends Pick<ContactFieldDefinition, 'order' | 'key' | 'retiredAt'>,\n>(definitions: readonly T[]): T[] {\n return sortContactFieldDefinitions(\n definitions.filter((definition) => !definition.retiredAt),\n )\n}\n\n/** Checkbox values a browser form actually posts for a ticked box. */\nconst AFFIRMATIVE = new Set(['true', 'on', 'yes', '1', 'checked'])\n/** And what it posts, or what a client sends, for an unticked one. */\nconst NEGATIVE = new Set(['false', 'off', 'no', '0', 'unchecked'])\n\n/**\n * One raw value, as the definition's type stores it — or `undefined` when\n * nothing usable was given.\n *\n * `undefined` and `null` are different answers on purpose. `null` is the\n * explicit \"cleared\" that keeps the key present for a `where` clause to find;\n * it is what a console save or an API body writes when it MEANS to clear.\n * `undefined` is \"there is no value here\", which a form submission produces\n * for a field left blank — and a blank field on a form must not clear a\n * value the merchant set by hand, so the caller drops the key rather than\n * writing it.\n *\n * - `text` and `url` store the trimmed string, capped. A link has to parse\n * as an `http(s)` URL, because the column renders it as one and a bare\n * word would be a broken anchor on every row.\n * - `number` stores a finite number. A numeric STRING is accepted because a\n * form posts every field as text, but anything that is not one number is\n * dropped rather than stored as `NaN`, which Firestore would refuse.\n * - `checkbox` stores a boolean, reading the strings a browser posts for a\n * ticked or an unticked box.\n * - `date` stores an ISO 8601 string, so a date filter compares as text and\n * an export reads as a date, whichever locale typed it.\n * - `select` stores one of the declared options, exactly, or nothing: a\n * choice outside the list would be a value the picker could never show.\n * - `currency` stores a whole number of the field's minor unit: a number\n * as given (rounded), which is the stored shape an API reads back, and\n * text — a form's post, \"$1,250.50\" — read as major units.\n */\nexport function coerceContactCustomValue(\n definition: Pick<ContactFieldDefinition, 'type' | 'options' | 'currency'>,\n raw: unknown,\n): ContactCustomValue | undefined {\n if (raw === undefined || raw === null) return undefined\n switch (definition.type) {\n case 'text': {\n const text = String(raw).trim().slice(0, CONTACT_TEXT_VALUE_MAX)\n return text ? text : undefined\n }\n case 'url': {\n const text = String(raw).trim().slice(0, CONTACT_TEXT_VALUE_MAX)\n if (!text) return undefined\n try {\n const url = new URL(text)\n return url.protocol === 'http:' || url.protocol === 'https:'\n ? text\n : undefined\n } catch {\n return undefined\n }\n }\n case 'number': {\n if (typeof raw === 'number') return Number.isFinite(raw) ? raw : undefined\n if (typeof raw === 'boolean') return undefined\n const text = String(raw).trim()\n if (!text) return undefined\n const parsed = Number(text)\n return Number.isFinite(parsed) ? parsed : undefined\n }\n case 'currency': {\n // A number is minor units, the stored shape an API client reads back;\n // text is what a person typed or a form posted, in major units.\n if (typeof raw === 'number') return Number.isFinite(raw) ? Math.round(raw) : undefined\n if (typeof raw === 'boolean' || typeof raw === 'object') return undefined\n const minor = parseCurrencyInput(raw, customFieldCurrency(definition))\n return minor === null ? undefined : minor\n }\n case 'checkbox': {\n if (typeof raw === 'boolean') return raw\n const text = String(raw).trim().toLowerCase()\n if (AFFIRMATIVE.has(text)) return true\n if (NEGATIVE.has(text)) return false\n return undefined\n }\n case 'date': {\n if (typeof raw === 'boolean') return undefined\n const ms =\n typeof raw === 'number'\n ? raw\n : raw instanceof Date\n ? raw.getTime()\n : Date.parse(String(raw).trim())\n return Number.isFinite(ms) ? new Date(ms).toISOString() : undefined\n }\n case 'select': {\n const text = String(raw).trim()\n const options = Array.isArray(definition.options) ? definition.options : []\n return text && options.includes(text) ? text : undefined\n }\n default:\n return undefined\n }\n}\n\n/**\n * What a form submission writes into the contact's `custom`: one entry per\n * declared field that names a contact field, coerced by that field's type.\n *\n * Only ACTIVE definitions receive a value — a mapping onto a retired field\n * is a mapping the author has not yet removed, and writing under a retired\n * key would put a value where no surface offers to show it. A field the\n * visitor left blank contributes nothing (see `coerceContactCustomValue`),\n * so a submission never clears a value a merchant set by hand.\n *\n * Two declared fields mapped onto one key is an authoring mistake this\n * function does not refuse; the FIRST declared field to yield a value wins,\n * in declaration order, so the answer is at least stable.\n */\nexport function collectMappedContactCustom(options: {\n /** The submitted values, keyed by the declared `fieldName`. */\n fields: Record<string, unknown>\n /** The form's declaration, as stored on its document. */\n decls: readonly FormFieldDecl[] | null | undefined\n /** The org's definitions, retired ones included — they are filtered here. */\n definitions: readonly ContactFieldDefinition[]\n}): Record<string, ContactCustomValue> {\n const active = new Map(\n activeContactFieldDefinitions(options.definitions).map((definition) => [\n definition.key,\n definition,\n ]),\n )\n const custom: Record<string, ContactCustomValue> = {}\n for (const decl of options.decls ?? []) {\n const key = decl.contactFieldKey\n if (!key || key in custom) continue\n const definition = active.get(key)\n if (!definition) continue\n const value = coerceContactCustomValue(definition, options.fields[decl.fieldName])\n if (value !== undefined) custom[key] = value\n }\n return custom\n}\n\n/**\n * The declaration a publish writes, with the mappings the author drew on the\n * previous one carried across.\n *\n * `formFieldDeclsFromNodes` reads a declaration off the DESIGN — the names,\n * types and options the nodes carry — and knows nothing about where a field\n * saves to, because that is edited on the form's own page rather than drawn\n * on the canvas. A publish that replaced the stored `fields` wholesale would\n * therefore drop every mapping on every publish, and the author would find\n * their profile fields silently empty after the next design change.\n *\n * Carried BY FIELD NAME, which is the submission key and the identity a\n * declared field has. A field renamed on the canvas is a new field and starts\n * unmapped; a field removed takes its mapping with it. Nothing else on the\n * previous declaration survives — the design is still the source of truth\n * for everything it draws.\n */\nexport function carryContactFieldMappings(\n previous: readonly FormFieldDecl[] | null | undefined,\n next: readonly FormFieldDecl[],\n): FormFieldDecl[] {\n const carried = new Map<string, string>()\n for (const decl of previous ?? []) {\n if (decl?.fieldName && decl.contactFieldKey) {\n carried.set(decl.fieldName, decl.contactFieldKey)\n }\n }\n return next.map((decl) => {\n const key = carried.get(decl.fieldName)\n return key ? { ...decl, contactFieldKey: key } : decl\n })\n}\n\n/**\n * The declaration with ONE field's destination changed: `key` maps it onto\n * that custom field, `null` unmaps it.\n *\n * Unmapping DELETES the property rather than setting it to `undefined`. The\n * declaration is written back to the form document as one array, and an\n * `undefined` inside an array is a value Firestore refuses outright — the\n * save would fail on the field the author just cleared, which is the one\n * they are least likely to suspect. Every other field, and every other\n * property of this one, comes through untouched: the design is still the\n * source of everything else on a declared field.\n */\nexport function withContactFieldMapping(\n decls: readonly FormFieldDecl[],\n fieldName: string,\n key: string | null,\n): FormFieldDecl[] {\n return decls.map((decl) => {\n if (decl.fieldName !== fieldName) return decl\n const next: FormFieldDecl = { ...decl }\n delete next.contactFieldKey\n return key ? { ...next, contactFieldKey: key } : next\n })\n}\n\n/** What one coercion refusal tells an API caller, per type. */\nfunction customValueExpectation(\n definition: Pick<ContactFieldDefinition, 'type' | 'options' | 'currency'>,\n): string {\n switch (definition.type) {\n case 'number':\n return 'Must be a number'\n case 'currency':\n return `Must be a whole number of ${customFieldCurrency(definition).toUpperCase()} minor units, or an amount as text`\n case 'checkbox':\n return 'Must be true or false'\n case 'date':\n return 'Must be an ISO 8601 date'\n case 'url':\n return 'Must be an http(s) URL'\n case 'select':\n return `Must be one of: ${(definition.options ?? []).join(', ')}`\n default:\n return 'Must be text'\n }\n}\n\n/**\n * A `custom` map sent to the API, validated against the org's definitions.\n *\n * Unknown keys are REFUSED rather than dropped, following the contact\n * resource's own rule for its top-level keys: a silent drop reads as \"we\n * stored your value\" when nothing was stored, and an integration pushing a\n * field it spelled wrong would never learn. Each refusal is named\n * `custom.<key>` so a client can match it to the entry it sent.\n *\n * A retired field is refused too. Its values are still READ — an export has\n * to show what was written under it — but nothing writes under a key the\n * console no longer offers, or the field could never actually go away.\n *\n * `null` is accepted for every type and means \"clear\": the key stays present\n * with an explicit nothing, which is the shape the console writes and the\n * shape a `where` clause can find.\n */\nexport function readContactCustomInput(\n raw: unknown,\n definitions: readonly ContactFieldDefinition[],\n /**\n * The object the map is written on, for the refusals' wording — the\n * caller has already narrowed `definitions` to it. Contacts when unsaid.\n */\n object: CrmFieldObject = 'contact',\n): { values: Record<string, ContactCustomValue> } | { errors: Record<string, string> } {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n return { errors: { custom: 'Must be an object of field values keyed by field key' } }\n }\n const byKey = new Map(\n sortContactFieldDefinitions(definitions).map((definition) => [\n definition.key,\n definition,\n ]),\n )\n const values: Record<string, ContactCustomValue> = {}\n const errors: Record<string, string> = {}\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n const definition = byKey.get(key)\n if (!definition) {\n errors[`custom.${key}`] = `No such ${object} field`\n continue\n }\n if (definition.retiredAt) {\n errors[`custom.${key}`] = `Retired ${object} field — restore it to write it`\n continue\n }\n if (value === null) {\n values[key] = null\n continue\n }\n const coerced = coerceContactCustomValue(definition, value)\n if (coerced === undefined) {\n errors[`custom.${key}`] = customValueExpectation(definition)\n continue\n }\n values[key] = coerced\n }\n return Object.keys(errors).length ? { errors } : { values }\n}\n\n/**\n * `readContactCustomInput` for ANY object (AGL-2661): the org's whole\n * definition list in, the ones describing `object` judged.\n *\n * The narrowing happens here rather than at every caller so a company\n * body cannot be validated against a contact field of the same key —\n * `region` on both is two fields, and a value sent to one must never be\n * read by the other's type. What comes back is exactly what the contact\n * reader answers: a map to store, or refusals named `custom.<key>`.\n */\nexport function readCrmCustomInput(\n raw: unknown,\n definitions: readonly ContactFieldDefinition[],\n object: CrmFieldObject,\n): { values: Record<string, ContactCustomValue> } | { errors: Record<string, string> } {\n return readContactCustomInput(raw, fieldDefinitionsForObject(definitions, object), object)\n}\n"],"names":["fieldDefinitionObject","CONTACT_FIELDS_MAX_PER_ORG","CONTACT_FIELD_TYPES","CONTACT_FIELD_TYPE_LABELS","text","number","date","select","checkbox","url","currency","isContactFieldType","value","includes","CONTACT_FIELD_DEFAULT_CURRENCY","customFieldCurrency","definition","code","String","trim","toLowerCase","test","isCustomFieldCurrency","Intl","NumberFormat","style","toUpperCase","currencyFractionDigits","resolvedOptions","maximumFractionDigits","currencyMinorToMajor","minor","parseCurrencyInput","input","cleaned","replace","Number","isFinite","Math","round","formatCurrencyFieldValue","locale","upper","major","format","toFixed","CONTACT_TEXT_VALUE_MAX","sortContactFieldDefinitions","definitions","sort","a","b","order","key","localeCompare","fieldDefinitionsForObject","object","filter","activeContactFieldDefinitions","retiredAt","AFFIRMATIVE","Set","NEGATIVE","coerceContactCustomValue","raw","undefined","type","slice","URL","protocol","parsed","has","ms","Date","getTime","parse","toISOString","options","Array","isArray","collectMappedContactCustom","active","Map","map","custom","decl","decls","contactFieldKey","get","fields","fieldName","carryContactFieldMappings","previous","next","carried","set","withContactFieldMapping","customValueExpectation","join","readContactCustomInput","errors","byKey","values","Object","entries","coerced","keys","length","readCrmCustomInput"],"mappings":"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"}
@@ -1006,7 +1006,7 @@ export declare function crmActivityRecordLink(activity: Pick<CrmActivity, 'conta
1006
1006
  * English-only, and the thresholds are what the spec pins.
1007
1007
  */
1008
1008
  export declare function activityTimeLabel(atMs: number, nowMs: number): string;
1009
- export type ContactFieldType = 'text' | 'number' | 'date' | 'select' | 'checkbox' | 'url';
1009
+ export type ContactFieldType = 'text' | 'number' | 'date' | 'select' | 'checkbox' | 'url' | 'currency';
1010
1010
  /**
1011
1011
  * `orgs/{orgId}/contactFields/{fieldId}` — a custom field a holder defined.
1012
1012
  *
@@ -1023,6 +1023,12 @@ export interface ContactFieldDefinition extends CrmScoped {
1023
1023
  type: ContactFieldType;
1024
1024
  /** The choices, for `select`. */
1025
1025
  options?: string[];
1026
+ /**
1027
+ * For `currency` (AGL-3756): the ISO 4217 code its amounts are in,
1028
+ * lowercase as a deal's `currency` is. Fixed once the field exists,
1029
+ * because a value is a count of this currency's minor unit.
1030
+ */
1031
+ currency?: string;
1026
1032
  required?: boolean;
1027
1033
  /** Position in the form and the export, ascending. */
1028
1034
  order: number;