jsfe 1.4.1 → 1.5.0
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.
- package/README.md +25 -0
- package/dist/helpers/amount-to-speech.d.ts +6 -0
- package/dist/helpers/amount-to-speech.d.ts.map +1 -0
- package/dist/helpers/amount-to-speech.js +195 -0
- package/dist/helpers/answer.d.ts +50 -0
- package/dist/helpers/answer.d.ts.map +1 -0
- package/dist/helpers/answer.js +409 -0
- package/dist/helpers/capture.d.ts +5 -0
- package/dist/helpers/capture.d.ts.map +1 -0
- package/dist/helpers/capture.js +50 -0
- package/dist/helpers/card-checks.d.ts +8 -0
- package/dist/helpers/card-checks.d.ts.map +1 -0
- package/dist/helpers/card-checks.js +46 -0
- package/dist/helpers/contact.d.ts +50 -0
- package/dist/helpers/contact.d.ts.map +1 -0
- package/dist/helpers/contact.js +178 -0
- package/dist/helpers/date-words.d.ts +2 -0
- package/dist/helpers/date-words.d.ts.map +1 -0
- package/dist/helpers/date-words.js +29 -0
- package/dist/helpers/index.d.ts +72 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/index.js +34 -0
- package/dist/helpers/match-inputs.d.ts +28 -0
- package/dist/helpers/match-inputs.d.ts.map +1 -0
- package/dist/helpers/match-inputs.js +223 -0
- package/dist/helpers/matches-choice.d.ts +26 -0
- package/dist/helpers/matches-choice.d.ts.map +1 -0
- package/dist/helpers/matches-choice.js +61 -0
- package/dist/helpers/otp-check.d.ts +36 -0
- package/dist/helpers/otp-check.d.ts.map +1 -0
- package/dist/helpers/otp-check.js +75 -0
- package/dist/helpers/phrases.d.ts +12 -0
- package/dist/helpers/phrases.d.ts.map +1 -0
- package/dist/helpers/phrases.js +31 -0
- package/dist/helpers/url-speech.d.ts +8 -0
- package/dist/helpers/url-speech.d.ts.map +1 -0
- package/dist/helpers/url-speech.js +28 -0
- package/dist/helpers/validators.d.ts +13 -0
- package/dist/helpers/validators.d.ts.map +1 -0
- package/dist/helpers/validators.js +32 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/package.json +5 -1
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform's answer vocabulary, ONE place for every flow set (APPROVED_FUNCTIONS isAnswer, isAgentRequest, isOtherRequest,
|
|
3
|
+
* isAmountAnswer, pickChoice). Before it, the YES / NO / AGENT / EXIT / TEXT words were copied by hand into system, ai-ar and Shopify
|
|
4
|
+
* flows and had drifted: four AGENT lists (25, 21, 15 and 12 words), nine YES lists, and a Shopify search that
|
|
5
|
+
* matched AGENT words as substrings, so "olive oil" and "televisor inteligente" went to an agent (review 2026-10-13).
|
|
6
|
+
*
|
|
7
|
+
* Words match whole words or word runs (matchesChoice), never substrings. Keypad conventions are platform-wide:
|
|
8
|
+
* 0 is an agent, * is exit.
|
|
9
|
+
*
|
|
10
|
+
* Part of jsfe/helpers; imports only its sibling matches-choice.js.
|
|
11
|
+
*/
|
|
12
|
+
import { matchesChoice } from './matches-choice.js';
|
|
13
|
+
export const ANSWER_WORDS = {
|
|
14
|
+
yes: ['yes', 'yeah', 'yea', 'yep', 'yup', 'sure', 'ok', 'okay', 'okey', 'please', 'of course', 'correct', 'right', 'go ahead',
|
|
15
|
+
'alright', 'all right', 'absolutely', 'definitely', 'certainly', 'sounds good', "that's fine", 'that is fine', 'fine',
|
|
16
|
+
"that's ok", "that's okay", "that's right", 'that is right', "that's correct", 'that works', 'agree', 'i agree', 'agreed',
|
|
17
|
+
'approve', 'i approve', 'approved', 'confirm', 'i confirm', 'confirmed', 'proceed', 'do it', 'perfect', 'exactly', 'i do',
|
|
18
|
+
'si', 'sí', 'claro', 'claro que sí', 'claro que si', 'seguro', 'por favor', 'correcto', 'está bien', 'esta bien', 'de acuerdo',
|
|
19
|
+
'estoy de acuerdo', 'dale', 'por supuesto', 'bueno', 'confirmo', 'confirmado', 'apruebo', 'aprobado', 'adelante', 'hazlo',
|
|
20
|
+
'procede', 'perfecto', 'exacto', 'eso es', 'así es', 'asi es',
|
|
21
|
+
// agreeing to what the question offers: "Sí, quiero continuar" to "diga SÍ para continuar" (replay of prod, 2026-10-14).
|
|
22
|
+
// A bare "I want" / "quiero" is never a confirmation (owner decision 2026-10-15): "Quiero pagar" at the express offer,
|
|
23
|
+
// which charges on its YES, was a YES because 'pagar' is that choice's own word.
|
|
24
|
+
'continue', 'i want to continue', 'go on', 'go ahead please', 'please do', 'yes i do',
|
|
25
|
+
'sí quiero', 'si quiero', 'continuar', 'quiero continuar', 'seguir', 'sigue', 'continúe', 'continue por favor',
|
|
26
|
+
'proceder', 'procedo', 'procede por favor', 'proceda', 'proceda por favor', 'sí proceda', 'si proceda'],
|
|
27
|
+
no: ['no', 'nope', 'nah', 'no thanks', 'no thank you', 'not now', 'not today', 'no gracias', 'ahora no', 'hoy no'],
|
|
28
|
+
agent: ['live agent', 'agent', 'agents', 'representative', 'rep', 'human', 'operator', 'manager', 'supervisor', 'someone',
|
|
29
|
+
'somebody', 'real person', 'a person', 'customer service', 'support', 'agente', 'agentes', 'gente', 'representante',
|
|
30
|
+
'humano', 'operador', 'operadora', 'gerente', 'alguien', 'persona', 'servicio al cliente', 'al cliente', 'servicio',
|
|
31
|
+
'recepcionista', 'recepcion', 'recepción'],
|
|
32
|
+
exit: ['exit', 'quit', 'abort', 'salir', 'cancel', 'cancelar'],
|
|
33
|
+
text: ['text', 'texto', 'sms', 'message', 'mensaje', 'text me', 'by text', 'por texto', 'mensaje de texto', 'whatsapp',
|
|
34
|
+
'escríbeme', 'escribeme'],
|
|
35
|
+
};
|
|
36
|
+
// The ONLY words that confirm a choice that charges or cancels (`exact`), besides the choice's own words and keys; courtesy
|
|
37
|
+
// around them is allowed (owner decision 2026-10-15: "clear charge confirmation exclusively"). Left out on purpose:
|
|
38
|
+
// acknowledgements ("ok", "está bien", "fine", "sounds good", "perfecto": "I heard you", not "charge me"), "bueno" (the
|
|
39
|
+
// Mexican phone "hello?"), courtesy alone ("please", "por favor"), words that are questions once ASR drops the "?"
|
|
40
|
+
// ("right", "seguro", "exactly", "así es"), and agreeing to continue ("continue", "continuar").
|
|
41
|
+
export const CONFIRM_WORDS = ['yes', 'yeah', 'yep', 'yup', 'sure', 'of course', 'absolutely', 'definitely', 'confirm', 'i confirm',
|
|
42
|
+
'confirmed', 'approve', 'i approve', 'approved', 'agree', 'i agree', 'agreed', 'proceed', 'go ahead', 'go ahead please', 'do it',
|
|
43
|
+
'correct', "that's correct", 'that is correct', "that's right", 'that is right', 'yes i do',
|
|
44
|
+
'si', 'sí', 'claro', 'claro que sí', 'claro que si', 'por supuesto', 'confirmo', 'confirmado', 'apruebo', 'aprobado', 'adelante',
|
|
45
|
+
'dale', 'hazlo', 'procede', 'proceda', 'proceder', 'procedo', 'sí proceda', 'si proceda', 'correcto', 'de acuerdo',
|
|
46
|
+
'estoy de acuerdo', 'sí quiero', 'si quiero'];
|
|
47
|
+
// Acknowledgements: never a confirmation of an `exact` choice on their own, but said beside one ("Sí, está bien", "Yes,
|
|
48
|
+
// okay") they change nothing, like courtesy. Beside an exact choice's own words, a CONFIRM word is the same ("Yes, the
|
|
49
|
+
// full balance").
|
|
50
|
+
// AGENT words that also mean something else inside a sentence ("quiero pagar mi servicio", "someone else will pay"): they
|
|
51
|
+
// ask for a person only as the whole answer (AGENT_ONLY), never from inside one.
|
|
52
|
+
const AGENT_AMBIGUOUS = new Set(['servicio', 'support', 'someone', 'somebody', 'alguien', 'persona', 'a person', 'gente', 'al cliente']);
|
|
53
|
+
// A word that negates what follows it in its clause ("not sure", "no quiero un agente").
|
|
54
|
+
const NEGATIONS = ['no', 'not', 'nope', 'nah', 'never', 'dont', 'doesnt', 'didnt', 'cant', 'cannot', 'wont', 'nunca', 'ni',
|
|
55
|
+
'tampoco', 'jamas', 'jamás'];
|
|
56
|
+
// Phrases that hold a negation word but agree ("no problem", "why not").
|
|
57
|
+
const AGREEING = /\b(no problem|no worries|no hay problema|no te preocupes|why not|por que no|por qué no)\b/g;
|
|
58
|
+
const DEFERRAL = /\b(later|tomorrow|next week|not now|not today|another time|mañana|manana|después|despues|luego|otro día|otro dia|ahora no|hoy no)\b/;
|
|
59
|
+
const AGENT_ONLY = /^(?:(?:i want|i need|i would like|id like|can i|could i|let me|please|quiero|necesito|puedo|por favor)\s+)*(?:(?:to\s+)?(?:talk|speak|hablar|comunicarme)\s+(?:to|with|con)\s+)?(?:a|an|un|una|el|la)?\s*(?:live agent|agent|representative|rep|human|operator|real person|a person|person|someone|somebody|customer service|agente|representante|humano|operador|operadora|persona|alguien|servicio al cliente|gerente|manager|supervisor)(?:\s+(?:please|por favor))?$/;
|
|
60
|
+
// Accents are not meaning here (owner 2026-10-16): text callers type "direccion" for "dirección", "si" for "sí". Both
|
|
61
|
+
// sides of every comparison pass through normalize, so a word list matches with or without them; ñ is kept.
|
|
62
|
+
const plainVowels = (s) => s.replace(/[áàâä]/g, 'a').replace(/[éèêë]/g, 'e').replace(/[íìîï]/g, 'i').replace(/[óòôö]/g, 'o').replace(/[úùûü]/g, 'u');
|
|
63
|
+
/** Unescapes what the engine escaped, lowercases, drops punctuation but keeps * @ ., and drops accents (not ñ). */
|
|
64
|
+
const normalize = (s) => plainVowels(String(s ?? '').replace(/\\(['"\\])/g, '$1').toLowerCase().replace(/[^\w\s*@.áéíóúüñàâäèêëìîïòôöùû]+/g, ''))
|
|
65
|
+
.replace(AGREEING, ' ').replace(/\s+/g, ' ').trim();
|
|
66
|
+
const NEGATION_SET = new Set(NEGATIONS.map(plainVowels));
|
|
67
|
+
const hasNegation = (s) => String(s).split(' ').some((t) => NEGATION_SET.has(t));
|
|
68
|
+
// Said around an answer without changing it: politeness, filler, a form of address.
|
|
69
|
+
const COURTESY = ['please', 'por favor', 'thanks', 'thank you', 'thank you so much', 'thanks a lot', 'gracias', 'muchas gracias', 'um', 'umm', 'uh', 'uhm',
|
|
70
|
+
'hmm', 'oh', 'ah', 'well', 'so', 'like', 'este', 'pues', "ma'am", 'maam', 'madam', 'sir', 'miss', 'señor', 'senor', 'señora',
|
|
71
|
+
'senora', 'señorita', 'senorita', 'mija', 'mijo'].map(normalize).map((w) => w.split(' '));
|
|
72
|
+
// A bare thanks at a yes/no question is a graceful acceptance (owner decision 2026-10-16): YES, but only at a choice keyed
|
|
73
|
+
// 'yes' that does not charge or cancel (pickChoice). "No, gracias" stays NO; isAnswer(…, 'yes') takes thanks only as a
|
|
74
|
+
// caller's extra.
|
|
75
|
+
const THANKS = ['thanks', 'thank you', 'thank you so much', 'thanks a lot', 'gracias', 'muchas gracias'];
|
|
76
|
+
const ACKNOWLEDGEMENTS = ['ok', 'okay', 'okey', 'está bien', 'esta bien', 'bueno', 'fine', "that's fine", 'alright', 'all right',
|
|
77
|
+
'perfect', 'perfecto', 'sounds good', 'great', 'excelente', 'muy bien'].map(normalize).map((w) => w.split(' '));
|
|
78
|
+
const CONFIRM_FILLER = CONFIRM_WORDS.map(normalize).map((w) => w.split(' '));
|
|
79
|
+
// articles and possessives in front of an exact choice's own words ("Yes, the full balance", "Sí, el saldo completo")
|
|
80
|
+
const ARTICLES = ['the', 'my', 'el', 'la', 'mi'].map((w) => [w]);
|
|
81
|
+
const YES_SET = new Set(ANSWER_WORDS.yes.map(normalize));
|
|
82
|
+
const collapse = (w) => w.replace(/([aeiouáéíóúü])\1+/g, '$1').replace(/(\p{L})\1{2,}/gu, '$1');
|
|
83
|
+
/**
|
|
84
|
+
* Whether the answer, with courtesy around it removed (please / thanks / gracias, um, uh, ma'am, señora…), holds NOTHING but
|
|
85
|
+
* words from `words` — at least one of them. A YES that acts must be the whole answer (owner rule, 2026-10-14): "Yeah, sure,
|
|
86
|
+
* thank you" and "Sí, claro, por favor" are YES; "yes, pay it" and "I made payments by 1" are not.
|
|
87
|
+
*/
|
|
88
|
+
function onlyAffirmative(b, words, needOne = true, filler = []) {
|
|
89
|
+
if (!words.length && needOne) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// words to match
|
|
94
|
+
}
|
|
95
|
+
const tk = String(b).replace(/\./g, ' ').split(' ').filter(Boolean);
|
|
96
|
+
const own = words.map(normalize).filter(Boolean).map((w) => w.split(' '));
|
|
97
|
+
const at = (i, phrase) => phrase.every((w, j) => tk[i + j] === w || (!/(\p{L})\1/u.test(w) && collapse(tk[i + j] || '') === w));
|
|
98
|
+
let i = 0;
|
|
99
|
+
let saw = false;
|
|
100
|
+
while (i < tk.length) {
|
|
101
|
+
const yes = own.filter((p) => at(i, p)).sort((x, y) => y.length - x.length)[0];
|
|
102
|
+
const courtesy = yes ? null : [...COURTESY, ...filler].filter((p) => at(i, p)).sort((x, y) => y.length - x.length)[0];
|
|
103
|
+
if (yes) {
|
|
104
|
+
i += yes.length;
|
|
105
|
+
saw = true;
|
|
106
|
+
}
|
|
107
|
+
else if (courtesy) {
|
|
108
|
+
i += courtesy.length;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return saw || !needOne;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Whether the answer is the keypad key `key` with nothing around it but courtesy ("1 please", "um 2", "2 gracias") and,
|
|
118
|
+
* when `yesOk`, a YES word ("yes 1", "sí, 1"). Owner decision 2026-10-15: "Yes 1" at "tell me the product number" was
|
|
119
|
+
* missed. Anything else around the key ("no 1", "1 or 2", "I paid 1 already") is not that key.
|
|
120
|
+
*/
|
|
121
|
+
function keyWithOnlyCourtesy(b, key, yesOk) {
|
|
122
|
+
const tk = String(b).replace(/\./g, ' ').split(' ').filter(Boolean);
|
|
123
|
+
const at = tk.reduce((acc, t, i) => (t === key ? [...acc, i] : acc), []);
|
|
124
|
+
if (at.length !== 1) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const rest = tk.filter((t, i) => i !== at[0]).join(' ');
|
|
129
|
+
return onlyAffirmative(rest, yesOk ? ANSWER_WORDS.yes : [], false);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Whether the answer is a `kind` answer ('yes' | 'no' | 'agent' | 'exit' | 'text'), with the caller's `extra` words.
|
|
134
|
+
* YES, AGENT and TEXT never match a negated answer ("not sure", "I don't want an agent"); 0 is AGENT, * is EXIT.
|
|
135
|
+
*/
|
|
136
|
+
export function isAnswer(input, kind, extra = []) {
|
|
137
|
+
const words = [...(ANSWER_WORDS[kind] || []), ...(Array.isArray(extra) ? extra : [])];
|
|
138
|
+
const b = normalize(input);
|
|
139
|
+
if (!b) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
else if (kind === 'agent' && b === '0') {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
else if (kind === 'exit' && b === '*') {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
else if ((kind === 'yes' || kind === 'agent' || kind === 'text') && hasNegation(b)) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
else if (kind === 'yes') {
|
|
152
|
+
return onlyAffirmative(b, words);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
return matchesChoice(b, words);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Whether the WHOLE answer asks for a person ("agent", "I want to talk to a representative", "0") — for free-text prompts. */
|
|
159
|
+
export function isAgentRequest(input) {
|
|
160
|
+
// the period normalize() keeps for emails is not part of the answer at its ends ("Operator." on a call, 2026-10-17)
|
|
161
|
+
const b = normalize(input).replace(/^[.\s]+|[.\s]+$/g, '');
|
|
162
|
+
return b === '0' || (!hasNegation(b) && AGENT_ONLY.test(b));
|
|
163
|
+
}
|
|
164
|
+
// A spoken or typed number: a digit, or a number word (ASR usually writes digits, not always).
|
|
165
|
+
const NUMBER_WORD = /\b(zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|hundred|thousand|cero|uno|una|dos|tres|cuatro|cinco|seis|siete|ocho|nueve|diez|once|doce|trece|catorce|quince|dieciséis|dieciseis|diecisiete|dieciocho|diecinueve|veinte|veinti\w+|treinta|cuarenta|cincuenta|sesenta|setenta|ochenta|noventa|cien|ciento|cientos|mil)\b/;
|
|
166
|
+
const EMAIL_SHAPE = /@|\b(at|arroba|dot|punto|gmail|hotmail|yahoo|outlook|icloud|aol)\b/;
|
|
167
|
+
// An address being spelled: a spelled symbol, or three or more single letters or digits of which at least two are not
|
|
168
|
+
// also words ("S o n. N y underscore.", "L e, o s"). The single-letter words a, e, i, o, u, y fill ordinary sentences
|
|
169
|
+
// ("quiero ver mi saldo y a la vez pagar o algo"), so they alone never make a spelling.
|
|
170
|
+
const SPELLING_WORD = /\b(underscore|guion|gui[oó]n bajo|dash|hyphen)\b/;
|
|
171
|
+
const isSpelling = (b) => {
|
|
172
|
+
const singles = b.split(' ').map((w) => w.replace(/[.,]$/, '')).filter((w) => /^[a-z0-9]$/.test(w));
|
|
173
|
+
return SPELLING_WORD.test(b) || (singles.length >= 3 && singles.filter((w) => !'aeiouy'.includes(w)).length >= 2);
|
|
174
|
+
};
|
|
175
|
+
/** Whether the answer is (part of) a spelled address — letters said one at a time, or a spelled symbol. */
|
|
176
|
+
export function isSpelledFragment(input) {
|
|
177
|
+
const b = normalize(input);
|
|
178
|
+
return !/@|\b(?:at|arroba)\b/.test(b) && isSpelling(b);
|
|
179
|
+
}
|
|
180
|
+
const SHAPES = {
|
|
181
|
+
number: (b) => /\d/.test(b) || NUMBER_WORD.test(b),
|
|
182
|
+
email: (b) => EMAIL_SHAPE.test(b) || isSpelling(b),
|
|
183
|
+
contact: (b) => /\d/.test(b) || NUMBER_WORD.test(b) || EMAIL_SHAPE.test(b) || isSpelling(b),
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Whether a free-text answer to a question that expects a `kind` ('number' | 'email' | 'contact') is the caller
|
|
187
|
+
* changing direction ("what's my balance?" to "how much would you like to pay?") rather than an attempt at the answer:
|
|
188
|
+
* three or more words, none of them the expected shape. Such an answer is DISPATCHed, never counted as a failed try.
|
|
189
|
+
* A short miss ("what?") is not: the asking flow re-asks it.
|
|
190
|
+
*/
|
|
191
|
+
export function isOtherRequest(input, kind) {
|
|
192
|
+
const b = normalize(input);
|
|
193
|
+
const shape = SHAPES[kind];
|
|
194
|
+
if (!shape) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
else if (shape(b)) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
return b.split(' ').filter((w) => /[a-záéíóúüñ]/.test(w)).length >= 3;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// An answer that names an amount: a currency sign or word, cents, or a number with decimals.
|
|
205
|
+
const AMOUNT_ANSWER = /\$|\b(dollars?|d[oó]lar(es)?|bucks?|cents?|centavos?)\b|\d[.,]\d{1,2}\b/i;
|
|
206
|
+
/**
|
|
207
|
+
* Whether the answer names an amount ("$1", "1 dollar", "un dólar", "50 cents", "12.50") rather than a keypad key.
|
|
208
|
+
* Such an answer never picks a digit choice (pickChoice): "$1" said at "press 1 to pay what is due" chose FULL and
|
|
209
|
+
* the caller was offered $235.90 (2026-10-14).
|
|
210
|
+
*/
|
|
211
|
+
export function isAmountAnswer(input) {
|
|
212
|
+
return AMOUNT_ANSWER.test(String(input ?? '').replace(/\\(['"\\])/g, '$1'));
|
|
213
|
+
}
|
|
214
|
+
// A number that names something other than an amount: an account or sub-account, a card's last digits, a code, a ZIP.
|
|
215
|
+
const IDENTIFIER_NUMBER = /\b(?:sub[\s-]?accounts?|accounts?|subcuentas?|cuentas?|cards?|tarjetas?|ending(?:\s+in)?|terminad[ao]s?\s+en|que\s+termina\s+en|termina\s+en|codes?|c[oó]digos?|zip(?:\s+code)?|postal|n[uú]mero|number)\s*(?:de\s+|is\s+|es\s+)?\d[\d\s-]*/gi;
|
|
216
|
+
// Digits spoken one at a time ("2 4 8 6"): a card ending or a code read back, never an amount.
|
|
217
|
+
const SPELLED_DIGITS = /\b\d(?:\s+\d\b){2,}/g;
|
|
218
|
+
// "10 con 18 centavos", "10 dollars and 18 cents", "$10 y 18 centavos"; speech-to-text also writes "76 con50 centavos", and
|
|
219
|
+
// callers say "171 con un 6 centavos" (owner 2026-10-18; a prod call went to the guru): an "un" before the cents.
|
|
220
|
+
const DOLLARS_AND_CENTS = /(\d+)\s*(?:dollars?|d[oó]lar(?:es)?|bucks?)?\s*(?:and|con|y)\s*(?:un\s+)?(\d{1,2})\s*(?:cents?|centavos?)\b/i;
|
|
221
|
+
// Dollars joined to two cent digits with no word for cents: "82 dollars and 50", "$82 y 50", "82 con 50". The joiner
|
|
222
|
+
// makes the second number cents when the first is marked as dollars ($ or a dollar word), or always for a joiner in
|
|
223
|
+
// CENTS_JOINERS (the languages that say amounts that way: Spanish "ochenta y dos con cincuenta") when two digits follow.
|
|
224
|
+
// "82 dollars and 5" is $82.05; "82 and 50" is $82.50; an unmarked "82 con 5", "82 and 5" or "82 y 50" stays two numbers. An "un" before the
|
|
225
|
+
// cents is allowed ("171 con un 06").
|
|
226
|
+
const CENTS_JOINERS = ['con', 'and']; // 'and' too (owner 2026-10-18: "87 and 21" = $87.21)
|
|
227
|
+
const DOLLARS_JOINED_CENTS = /(\$\s*)?(\d+)(\s*(?:dollars?|d[oó]lar(?:es)?|bucks?))?\s*(and|con|y)\s*(?:un\s+)?(\d{1,2})\b(?!\s*\d)/gi;
|
|
228
|
+
/**
|
|
229
|
+
* The amount an answer names, or NaN when it names none or is ambiguous. Prod replay 2026-10-14: the first number in the
|
|
230
|
+
* answer was taken, so "pagar cuenta 50, ciento treinta y cuatro…" paid $50, "$10 con 18 centavos" paid $10.00 and an
|
|
231
|
+
* offer read back with "terminada en 2 4 8 6" paid $2. Numbers that name an account, card, code or ZIP, and digits spoken
|
|
232
|
+
* one by one, are not amounts; two different remaining numbers are ambiguous unless exactly one is marked as money ($,
|
|
233
|
+
* dollars, cents or decimals). "50 cents" alone is $0.50; "82 dollars and 50" and "82 con 50" are $82.50. Amounts said only in words are NaN (not parsed yet).
|
|
234
|
+
*/
|
|
235
|
+
export function amountInAnswer(input) {
|
|
236
|
+
const raw = String(input ?? '').replace(/\\(['"\\])/g, '$1');
|
|
237
|
+
// 1.234,56 (European) → 1234.56; 1,234.56 → 1234.56; 12,50 → 12.50
|
|
238
|
+
let t = /\d{1,3}(\.\d{3})+,\d{1,2}\b/.test(raw) ? raw.replace(/\./g, '').replace(',', '.') : raw.replace(/,(?=\d{3}\b)/g, '').replace(/(\d),(\d{1,2})\b/g, '$1.$2');
|
|
239
|
+
const dc = t.match(DOLLARS_AND_CENTS);
|
|
240
|
+
if (dc) {
|
|
241
|
+
t = t.replace(DOLLARS_AND_CENTS, () => ` $${dc[1]}.${dc[2].padStart(2, '0')} `);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// no dollars-and-cents phrase
|
|
245
|
+
}
|
|
246
|
+
t = t.replace(IDENTIFIER_NUMBER, ' ').replace(SPELLED_DIGITS, ' ').replace(DOLLARS_JOINED_CENTS, (m, sign, d, word, joiner, c) => (sign || word || (CENTS_JOINERS.includes(joiner.toLowerCase()) && c.length === 2) ? ` $${d}.${c.padStart(2, '0')} ` : m));
|
|
247
|
+
const found = [...t.matchAll(/(\$\s*)?(\d+(?:\.\d{1,2})?)(\s*(?:dollars?|d[oó]lar(?:es)?|bucks?|cents?|centavos?)\b)?/gi)]
|
|
248
|
+
.map((m) => ({ v: parseFloat(m[2]), marked: !!(m[1] || m[3] || m[2].includes('.')), cents: !m[1] && /cent/i.test(m[3] || '') }));
|
|
249
|
+
const values = [...new Set(found.map((f) => f.v))];
|
|
250
|
+
let pick = null;
|
|
251
|
+
if (values.length === 1) {
|
|
252
|
+
pick = found[0];
|
|
253
|
+
}
|
|
254
|
+
else if (values.length > 1) {
|
|
255
|
+
const marked = found.filter((f) => f.marked);
|
|
256
|
+
pick = new Set(marked.map((f) => f.v)).size === 1 ? marked[0] : null;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// no number at all
|
|
260
|
+
}
|
|
261
|
+
if (!pick) {
|
|
262
|
+
return NaN;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
// Cents alone only when nothing else was said: "76 con50 centavos" read as $0.50 (prod, 10-15).
|
|
266
|
+
return pick.cents ? (values.length === 1 ? Math.round(pick.v) / 100 : NaN) : pick.v;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Number words a caller says for a keypad key (prod 2026-10-16 7:07 AM: "dos" at "presione 2 o diga NO" was re-asked).
|
|
270
|
+
const KEY_WORDS = { cero: '0', zero: '0', uno: '1', una: '1', one: '1', dos: '2', two: '2', tres: '3', three: '3', cuatro: '4',
|
|
271
|
+
four: '4', cinco: '5', five: '5', seis: '6', six: '6', siete: '7', seven: '7', ocho: '8', eight: '8', nueve: '9', nine: '9' };
|
|
272
|
+
/** The normalized answer with each number word replaced by its digit ("dos por favor" → "2 por favor"). */
|
|
273
|
+
function spokenKeys(b) {
|
|
274
|
+
return String(b).replace(/\./g, ' ').split(' ').filter(Boolean).map((t) => KEY_WORDS[t] ?? t).join(' ');
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* ask-choice's matcher: the key of the choice the answer picks, '__agent', '__exit', or '' (no choice).
|
|
278
|
+
* choices: [{ key, words, negative?, neutral?, exact? }]. A key (digit, * or #) and a YES word pick a choice only as the whole
|
|
279
|
+
* answer, courtesy aside (onlyAffirmative); an `exact` choice (one that charges or cancels) treats ALL its words that way. A choice keyed 'yes' also takes ANSWER_WORDS.yes (CONFIRM_WORDS when `exact`) and, when not `exact`, a bare thanks (THANKS), and the NO-type
|
|
280
|
+
* choice (key no / decline / done / cancel, or negative: true) also takes ANSWER_WORDS.no, so callers list only their
|
|
281
|
+
* extras. A word after a negation in its clause is negated: it picks the NO-type choice; a `neutral` choice still
|
|
282
|
+
* matches unless it follows its negation within two words; any other choice is blocked and NO answers. A deferral
|
|
283
|
+
* (later, mañana) answers NO; a reply led by a bare 'no' turns YES into NO. With acceptOther, AGENT only when the
|
|
284
|
+
* whole answer is an agent request and EXIT only on exit/quit/salir/abort (cancel is an answer there).
|
|
285
|
+
*/
|
|
286
|
+
export function pickChoice(answer, choices, acceptOther) {
|
|
287
|
+
const raw = String(answer ?? '').replace(/\\(['"\\])/g, '$1');
|
|
288
|
+
const b = normalize(raw);
|
|
289
|
+
const pos = [];
|
|
290
|
+
const negs = [];
|
|
291
|
+
raw.split(/[,;!?]|\.\s|\s(?:but|pero)\s/i).map(normalize).forEach((clause) => {
|
|
292
|
+
const tk = clause.split(' ');
|
|
293
|
+
const k = tk.findIndex((t) => NEGATION_SET.has(t));
|
|
294
|
+
if (k < 0) {
|
|
295
|
+
pos.push(clause);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
pos.push(tk.slice(0, k).join(' '));
|
|
299
|
+
negs.push(tk.slice(k).join(' '));
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
const P = pos.join(' ~ ');
|
|
303
|
+
const N = negs.join(' ~ ');
|
|
304
|
+
const refused = (w) => {
|
|
305
|
+
const wt = w.split(' ');
|
|
306
|
+
return negs.some((r) => {
|
|
307
|
+
const tk = r.split(' ');
|
|
308
|
+
for (let q = 1; q < tk.length && q <= 2; q++) {
|
|
309
|
+
if (tk.slice(q, q + wt.length).join(' ') === w) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
// not at this offset
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
const leadNo = /^(no|nope|nah|no thanks|no gracias)$/.test(normalize(raw.split(/[,;!?]|\.\s/)[0]));
|
|
320
|
+
const cs = Array.isArray(choices) ? choices.filter((c) => c && c.key) : [];
|
|
321
|
+
const isNoType = (c) => ['no', 'decline', 'done', 'cancel'].includes(c.key) || c.negative === true;
|
|
322
|
+
const noChoice = (cs.find(isNoType) || {}).key || '';
|
|
323
|
+
const deferral = DEFERRAL.test(b);
|
|
324
|
+
const amount = isAmountAnswer(raw); // an amount never picks a keypad key
|
|
325
|
+
const spoken = spokenKeys(b); // "dos", "two": a key said as a word picks what the digit would
|
|
326
|
+
let blocked = false;
|
|
327
|
+
let found = '';
|
|
328
|
+
for (const c of cs) {
|
|
329
|
+
const own = Array.isArray(c.words) ? c.words : [];
|
|
330
|
+
const all = c.key === 'yes' ? [...own, ...(c.exact === true ? CONFIRM_WORDS : ANSWER_WORDS.yes)] : (c.key === noChoice ? [...own, ...ANSWER_WORDS.no] : own);
|
|
331
|
+
// Strict words pick this choice only when they are the whole answer (onlyAffirmative): a key, a YES word, or — on an
|
|
332
|
+
// `exact` choice (one that charges or cancels) — every word. The rest (named actions: LINK, RECEIPT…) match inside it.
|
|
333
|
+
const strict = all.filter((w) => c.exact === true || /^[0-9*#]$/.test(String(w).trim()) || YES_SET.has(normalize(w)))
|
|
334
|
+
.filter((w) => !(amount && /^\d+$/.test(String(w).trim())));
|
|
335
|
+
const isKey = (w) => /^[0-9*#]$/.test(String(w).trim());
|
|
336
|
+
const keyed = (w) => isKey(w) && (matchesChoice(raw, [w]) || keyWithOnlyCourtesy(b, String(w).trim(), !isNoType(c)) || (spoken !== b && keyWithOnlyCourtesy(spoken, String(w).trim(), !isNoType(c))));
|
|
337
|
+
// A question is not a confirmation: "sure?" or "1?" never picks a choice that charges or cancels.
|
|
338
|
+
if (c.exact === true && /[?¿]/.test(raw)) {
|
|
339
|
+
// not this choice
|
|
340
|
+
}
|
|
341
|
+
else if (strict.some(keyed) || onlyAffirmative(b, [...strict.filter((w) => !isKey(w)), ...(c.key === 'yes' && c.exact !== true ? THANKS : [])], true, c.exact === true ? (c.key === 'yes' ? ACKNOWLEDGEMENTS : [...ACKNOWLEDGEMENTS, ...CONFIRM_FILLER, ...ARTICLES]) : [])) {
|
|
342
|
+
found = c.key;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
// not by a strict word
|
|
347
|
+
}
|
|
348
|
+
for (const w of all.map(normalize).filter((w) => !strict.map(normalize).includes(w) && !/^[0-9*#]$/.test(w) && !(c.exact === true))) {
|
|
349
|
+
if (!w || (amount && /^\d+$/.test(w))) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
else if (c.key === noChoice || c.key === 'no' || c.negative === true || hasNegation(w)) {
|
|
353
|
+
if (matchesChoice(b, [w])) {
|
|
354
|
+
found = c.key;
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
// a NO-type word matches the whole answer only
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (matchesChoice(P, [w])) {
|
|
361
|
+
found = c.key;
|
|
362
|
+
}
|
|
363
|
+
else if (matchesChoice(N, [w])) {
|
|
364
|
+
if (c.neutral === true && !refused(w)) {
|
|
365
|
+
found = c.key;
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
blocked = true;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
// not this word
|
|
373
|
+
}
|
|
374
|
+
if (found) {
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
// keep looking
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (found) {
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
// next choice
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (noChoice && deferral && found !== noChoice) {
|
|
389
|
+
return noChoice;
|
|
390
|
+
}
|
|
391
|
+
else if (found === 'yes' && leadNo && noChoice) {
|
|
392
|
+
return noChoice;
|
|
393
|
+
}
|
|
394
|
+
else if (found) {
|
|
395
|
+
return found;
|
|
396
|
+
}
|
|
397
|
+
else if (blocked && noChoice) {
|
|
398
|
+
return noChoice;
|
|
399
|
+
}
|
|
400
|
+
else if ((b === '0' && !amount) || (!hasNegation(b) && !b.includes('@') && (AGENT_ONLY.test(b) || (!acceptOther && matchesChoice(b, ANSWER_WORDS.agent.filter((w) => !AGENT_AMBIGUOUS.has(w))))))) {
|
|
401
|
+
return '__agent';
|
|
402
|
+
}
|
|
403
|
+
else if (b === '*' || (acceptOther ? /^(exit|quit|salir|abort)( please| por favor)?$/ : /^(exit|quit|salir|abort|cancel|cancelar)( please| por favor)?$/).test(b)) {
|
|
404
|
+
return '__exit';
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
return '';
|
|
408
|
+
}
|
|
409
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/helpers/capture.js"],"names":[],"mappings":"AAeA;;;SA8BC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* normalizeAndFindCapture (APPROVED_FUNCTIONS): the first capture pattern a free answer matches, as
|
|
3
|
+
* { variable, value } (value = the matched part) — the retry menu's "a contact in the answer retries with it" (generic-retry-with-options
|
|
4
|
+
* capture_patterns: [{ variable, regex, normalizer? }]). The normalizer is removed from the answer first
|
|
5
|
+
* (e.g. '[^0-9]' leaves the digits of "213-555-0100"). A pattern whose regex is invalid is skipped and reported
|
|
6
|
+
* through `warn`, so one bad tenant pattern cannot stop the others.
|
|
7
|
+
*
|
|
8
|
+
* A pattern may name a reader instead of a regex: { variable, reader: 'phone' | 'email' } reads the answer with
|
|
9
|
+
* readPhone / readEmail (spoken and spelled forms included).
|
|
10
|
+
*
|
|
11
|
+
* Part of jsfe/helpers; imports only its sibling contact.js.
|
|
12
|
+
*/
|
|
13
|
+
import { readEmail, readPhone } from './contact.js';
|
|
14
|
+
const READERS = { phone: readPhone, email: readEmail };
|
|
15
|
+
export function normalizeAndFindCapture(userInput, patterns, warn = () => { }) {
|
|
16
|
+
if (!Array.isArray(patterns)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
for (const pattern of patterns) {
|
|
21
|
+
if (pattern && READERS[pattern.reader]) {
|
|
22
|
+
const value = READERS[pattern.reader](userInput);
|
|
23
|
+
if (value) {
|
|
24
|
+
return { variable: pattern.variable, value };
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// a regex pattern
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const value = pattern.normalizer ? String(userInput ?? '').replace(new RegExp(pattern.normalizer, 'g'), '') : String(userInput ?? '');
|
|
35
|
+
const match = new RegExp(pattern.regex).exec(value);
|
|
36
|
+
if (match) {
|
|
37
|
+
// the matched part only: "my email is a@b.com" captures a@b.com
|
|
38
|
+
return { variable: pattern.variable, value: match[0].trim() };
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// not this pattern
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
warn(`Invalid regex in capture pattern: ${e.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card checks the payment flows call (validateCardNumber / validateExpiration). Pure; part of jsfe/helpers.
|
|
3
|
+
*/
|
|
4
|
+
/** 12–19 digits passing the Luhn checksum (catches nearly every typo, on every network). */
|
|
5
|
+
export function validateCardNumber(input: any): boolean;
|
|
6
|
+
/** MM and YY (two digits each), a real month, not expired, at most 20 years out. `now` is for tests. */
|
|
7
|
+
export function validateExpiration(month: any, year: any, now?: Date): boolean;
|
|
8
|
+
//# sourceMappingURL=card-checks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"card-checks.d.ts","sourceRoot":"","sources":["../../src/helpers/card-checks.js"],"names":[],"mappings":"AAAA;;GAEG;AAEH,4FAA4F;AAC5F,wDAoBC;AAED,wGAAwG;AACxG,+EAYC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card checks the payment flows call (validateCardNumber / validateExpiration). Pure; part of jsfe/helpers.
|
|
3
|
+
*/
|
|
4
|
+
/** 12–19 digits passing the Luhn checksum (catches nearly every typo, on every network). */
|
|
5
|
+
export function validateCardNumber(input) {
|
|
6
|
+
const cleaned = String(input || '').replace(/\D/g, '');
|
|
7
|
+
if (cleaned.length < 12 || cleaned.length > 19) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
let sum = 0;
|
|
12
|
+
let double = false;
|
|
13
|
+
for (let i = cleaned.length - 1; i >= 0; i--) {
|
|
14
|
+
let d = parseInt(cleaned[i], 10);
|
|
15
|
+
if (double) {
|
|
16
|
+
d *= 2;
|
|
17
|
+
if (d > 9)
|
|
18
|
+
d -= 9;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
// undoubled digit
|
|
22
|
+
}
|
|
23
|
+
sum += d;
|
|
24
|
+
double = !double;
|
|
25
|
+
}
|
|
26
|
+
return sum % 10 === 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** MM and YY (two digits each), a real month, not expired, at most 20 years out. `now` is for tests. */
|
|
30
|
+
export function validateExpiration(month, year, now = new Date()) {
|
|
31
|
+
const m = String(month || '').replace(/\D/g, '');
|
|
32
|
+
const y = String(year || '').replace(/\D/g, '');
|
|
33
|
+
if (m.length !== 2 || y.length !== 2)
|
|
34
|
+
return false;
|
|
35
|
+
const mNum = parseInt(m, 10);
|
|
36
|
+
const yNum = parseInt(y, 10);
|
|
37
|
+
if (mNum < 1 || mNum > 12)
|
|
38
|
+
return false;
|
|
39
|
+
const curYear = now.getFullYear() % 100;
|
|
40
|
+
const curMonth = now.getMonth() + 1;
|
|
41
|
+
if (yNum < curYear)
|
|
42
|
+
return false;
|
|
43
|
+
if (yNum === curYear && mNum < curMonth)
|
|
44
|
+
return false;
|
|
45
|
+
return yNum <= curYear + 20;
|
|
46
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The email address in the answer, lowercased, or ''. At / arroba and dot / punto may be spoken, and the name before
|
|
3
|
+
* the @ may come in several pieces ("J. A Garcia at example.com", "j u a n", "Maria jose@example.com"): every piece
|
|
4
|
+
* back to the first lead-in word ("my email is", "mi correo es") is kept and joined. On a call the address is read
|
|
5
|
+
* back for a YES / NO, so a misheard piece can be corrected; a dropped one could not. Accents are dropped from the
|
|
6
|
+
* name (an address cannot carry them), and only the last @ counts.
|
|
7
|
+
*/
|
|
8
|
+
export function readEmail(text: any): string;
|
|
9
|
+
/** The 10-digit North American number in the answer (a leading 1 dropped), or ''. */
|
|
10
|
+
export function readPhone(text: any): string;
|
|
11
|
+
/**
|
|
12
|
+
* A phone number from outside the US / Canada in the answer, as its digits with the country code, or ''. With an
|
|
13
|
+
* explicit prefix ("+", "plus", "más", "00") 8-15 digits; without one 11-13 digits not starting with 1 (a card or an
|
|
14
|
+
* account number is not mistaken for one). US / Canada numbers are readPhone's. The flows say what such a number
|
|
15
|
+
* cannot be used for instead of "I didn't get that" (owner, 2026-10-14).
|
|
16
|
+
*/
|
|
17
|
+
export function readInternationalPhone(text: any): string;
|
|
18
|
+
/** Whether the caller's own number (cargo.callerId) is from outside the US / Canada: on a call, a real caller ID. */
|
|
19
|
+
export function callerIsInternational(cargo: any): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* What the customer identified themselves with: { kind: 'account' | 'phone' | 'international_phone' | 'email', value } or
|
|
22
|
+
* null.
|
|
23
|
+
* accountPattern (a regex source, e.g. '^5\\d{6,7}$') is the tenant's account-number shape; its digits win first.
|
|
24
|
+
*/
|
|
25
|
+
export function readIdentifier(text: any, accountPattern?: string): {
|
|
26
|
+
kind: string;
|
|
27
|
+
value: string;
|
|
28
|
+
} | null;
|
|
29
|
+
/**
|
|
30
|
+
* The last four characters of a number (card, account, phone, tracking) as said to the customer: spaced one by one on a
|
|
31
|
+
* call ("5, 4, 7, 5"), as written otherwise ("5475"). Written ~25 times inline across the flow sets before it.
|
|
32
|
+
*/
|
|
33
|
+
export function last4(value: any, voice: any): string;
|
|
34
|
+
/**
|
|
35
|
+
* An email said back to the customer without reading all of it out. On a call: its first two characters spaced
|
|
36
|
+
* one by one and its domain ("starting with r, o, at gmail.com" / "que empieza con r, o, arroba gmail.com"): "ending
|
|
37
|
+
* in @gmail.com" alone is ambiguous on a call, and "ro***" makes the speech engine read the asterisks. Written:
|
|
38
|
+
* "ro***@gmail.com". '' for anything that is not an email.
|
|
39
|
+
*/
|
|
40
|
+
export function maskEmail(email: any, voice: any, lang?: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* The number we can text the caller at, 10 digits, or '': ONE rule for offering TEXT and for sending one. Only on a
|
|
43
|
+
* call with a sender (cargo.twilioNumber), a US / Canada caller ID, and not the number a code could not be texted to
|
|
44
|
+
* (cargo.otpUndeliverableTo). A number from another country used to be cut to its last 10 digits and offered TEXT,
|
|
45
|
+
* which then failed (dev calls from +52, 2026-10-13).
|
|
46
|
+
*/
|
|
47
|
+
export function callerTextNumber(cargo: any): string;
|
|
48
|
+
/** Where a text to the customer can go, 10 digits or '': the verified cell, else the caller (callerTextNumber). */
|
|
49
|
+
export function textDestination(cargo: any): string;
|
|
50
|
+
//# sourceMappingURL=contact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contact.d.ts","sourceRoot":"","sources":["../../src/helpers/contact.js"],"names":[],"mappings":"AAiBA;;;;;;GAMG;AACH,6CAwBC;AAED,qFAAqF;AACrF,6CASC;AAED;;;;;GAKG;AACH,0DAgBC;AAED,qHAAqH;AACrH,2DAIC;AAED;;;;GAIG;AACH;;;SAmBC;AAED;;;GAGG;AACH,sDAOC;AAED;;;;;GAKG;AACH,yEAUC;AAQD;;;;;GAKG;AACH,qDAIC;AAED,mHAAmH;AACnH,oDAIC"}
|