@absolutejs/voice 0.0.22-beta.509 → 0.0.22-beta.510
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/dist/callDisposition.d.ts +38 -0
- package/dist/callingWindow.d.ts +26 -0
- package/dist/campaignTemplate.d.ts +16 -0
- package/dist/dncRegistry.d.ts +38 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +620 -0
- package/dist/retryPolicy.d.ts +38 -0
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type VoiceCallDispositionTaxonomy = "sales" | "support" | "collections" | "survey" | "custom";
|
|
2
|
+
export type VoiceCallDispositionDefinition = {
|
|
3
|
+
code: string;
|
|
4
|
+
label: string;
|
|
5
|
+
outcome: "positive" | "neutral" | "negative" | "no-contact";
|
|
6
|
+
retryable: boolean;
|
|
7
|
+
taxonomy?: VoiceCallDispositionTaxonomy;
|
|
8
|
+
};
|
|
9
|
+
export declare const DEFAULT_VOICE_CALL_DISPOSITIONS: VoiceCallDispositionDefinition[];
|
|
10
|
+
export type VoiceCallDispositionTag = {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
code: string;
|
|
13
|
+
taggedAt: number;
|
|
14
|
+
note?: string;
|
|
15
|
+
};
|
|
16
|
+
export type CreateVoiceCallDispositionTaggerOptions = {
|
|
17
|
+
taxonomy?: VoiceCallDispositionDefinition[];
|
|
18
|
+
allowMultiple?: boolean;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
};
|
|
21
|
+
export declare const createVoiceCallDispositionTagger: (options?: CreateVoiceCallDispositionTaggerOptions) => {
|
|
22
|
+
definitionFor: (code: string) => VoiceCallDispositionDefinition | null;
|
|
23
|
+
definitions: VoiceCallDispositionDefinition[];
|
|
24
|
+
listAll: () => VoiceCallDispositionTag[];
|
|
25
|
+
listForSession: (sessionId: string) => VoiceCallDispositionTag[];
|
|
26
|
+
summarize: () => {
|
|
27
|
+
byCode: {
|
|
28
|
+
[k: string]: number;
|
|
29
|
+
};
|
|
30
|
+
byOutcome: Record<"negative" | "neutral" | "positive" | "no-contact", number>;
|
|
31
|
+
retryablePct: number;
|
|
32
|
+
totalSessions: number;
|
|
33
|
+
totalTagged: number;
|
|
34
|
+
};
|
|
35
|
+
tag: (sessionId: string, code: string, note?: string) => VoiceCallDispositionTag;
|
|
36
|
+
untag: (sessionId: string, code?: string) => number;
|
|
37
|
+
};
|
|
38
|
+
export type VoiceCallDispositionTagger = ReturnType<typeof createVoiceCallDispositionTagger>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type VoiceCallingDayKey = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";
|
|
2
|
+
export type VoiceCallingTimeRange = {
|
|
3
|
+
start: string;
|
|
4
|
+
end: string;
|
|
5
|
+
};
|
|
6
|
+
export type VoiceCallingWindowOptions = {
|
|
7
|
+
timezone?: string;
|
|
8
|
+
allowedDays?: VoiceCallingDayKey[];
|
|
9
|
+
allowedHours?: VoiceCallingTimeRange | VoiceCallingTimeRange[];
|
|
10
|
+
blockedDates?: string[];
|
|
11
|
+
perDayHours?: Partial<Record<VoiceCallingDayKey, VoiceCallingTimeRange[]>>;
|
|
12
|
+
now?: () => Date;
|
|
13
|
+
};
|
|
14
|
+
export type VoiceCallingWindowVerdict = {
|
|
15
|
+
allowed: boolean;
|
|
16
|
+
reason?: "outside-hours" | "blocked-date" | "outside-day";
|
|
17
|
+
nextWindowAt?: number;
|
|
18
|
+
};
|
|
19
|
+
export declare const createVoiceCallingWindow: (options?: VoiceCallingWindowOptions) => {
|
|
20
|
+
allowedDays: VoiceCallingDayKey[];
|
|
21
|
+
canCallNow(at?: Date): VoiceCallingWindowVerdict;
|
|
22
|
+
nextWindowOpensAt(at?: Date): number;
|
|
23
|
+
timezone: string | undefined;
|
|
24
|
+
};
|
|
25
|
+
export type VoiceCallingWindow = ReturnType<typeof createVoiceCallingWindow>;
|
|
26
|
+
export declare const VOICE_TCPA_DEFAULT_WINDOW: VoiceCallingWindowOptions;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type VoiceCampaignTemplateValue = string | number | boolean | null | undefined;
|
|
2
|
+
export type VoiceCampaignTemplateScope = Record<string, VoiceCampaignTemplateValue | Record<string, VoiceCampaignTemplateValue>>;
|
|
3
|
+
export type VoiceCampaignTemplateFilter = (value: VoiceCampaignTemplateValue, ...args: string[]) => VoiceCampaignTemplateValue;
|
|
4
|
+
export type VoiceCampaignTemplateResolveResult = {
|
|
5
|
+
output: string;
|
|
6
|
+
missingVariables: string[];
|
|
7
|
+
};
|
|
8
|
+
export type ResolveVoiceCampaignTemplateOptions = {
|
|
9
|
+
scope: VoiceCampaignTemplateScope;
|
|
10
|
+
filters?: Record<string, VoiceCampaignTemplateFilter>;
|
|
11
|
+
fallback?: string;
|
|
12
|
+
strict?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare const DEFAULT_VOICE_CAMPAIGN_TEMPLATE_FILTERS: Record<string, VoiceCampaignTemplateFilter>;
|
|
15
|
+
export declare const resolveVoiceCampaignTemplate: (template: string, options: ResolveVoiceCampaignTemplateOptions) => VoiceCampaignTemplateResolveResult;
|
|
16
|
+
export declare const collectVoiceCampaignTemplateVariables: (template: string) => string[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type VoiceDNCSource = "internal" | "regulatory" | "imported";
|
|
2
|
+
export type VoiceDNCEntry = {
|
|
3
|
+
phoneNumber: string;
|
|
4
|
+
source: VoiceDNCSource;
|
|
5
|
+
reason?: string;
|
|
6
|
+
addedAt: number;
|
|
7
|
+
expiresAt?: number;
|
|
8
|
+
};
|
|
9
|
+
export type VoiceDNCLookupVerdict = {
|
|
10
|
+
blocked: boolean;
|
|
11
|
+
entry: VoiceDNCEntry | null;
|
|
12
|
+
matchedFromExternal?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type VoiceDNCExternalLookup = (phoneNumber: string) => Promise<VoiceDNCEntry | null> | VoiceDNCEntry | null;
|
|
15
|
+
export type CreateVoiceDNCRegistryOptions = {
|
|
16
|
+
entries?: VoiceDNCEntry[];
|
|
17
|
+
externalLookup?: VoiceDNCExternalLookup;
|
|
18
|
+
now?: () => number;
|
|
19
|
+
};
|
|
20
|
+
export declare const createVoiceDNCRegistry: (options?: CreateVoiceDNCRegistryOptions) => {
|
|
21
|
+
block: (phoneNumber: string, options?: {
|
|
22
|
+
source?: VoiceDNCSource;
|
|
23
|
+
reason?: string;
|
|
24
|
+
expiresAt?: number;
|
|
25
|
+
}) => VoiceDNCEntry;
|
|
26
|
+
check(phoneNumber: string): Promise<VoiceDNCLookupVerdict>;
|
|
27
|
+
checkSync(phoneNumber: string): VoiceDNCLookupVerdict;
|
|
28
|
+
has(phoneNumber: string): boolean;
|
|
29
|
+
snapshot(): VoiceDNCEntry[];
|
|
30
|
+
unblock: (phoneNumber: string) => boolean;
|
|
31
|
+
};
|
|
32
|
+
export type VoiceDNCRegistry = ReturnType<typeof createVoiceDNCRegistry>;
|
|
33
|
+
export declare const importVoiceDNCFromCSV: (csv: string, options?: {
|
|
34
|
+
phoneColumn?: string;
|
|
35
|
+
reasonColumn?: string;
|
|
36
|
+
source?: VoiceDNCSource;
|
|
37
|
+
now?: () => number;
|
|
38
|
+
}) => VoiceDNCEntry[];
|
package/dist/index.d.ts
CHANGED
|
@@ -301,4 +301,14 @@ export { createVoicePostCallSurvey, DEFAULT_VOICE_POST_CALL_SURVEY_QUESTIONS, su
|
|
|
301
301
|
export type { VoicePostCallSurvey, VoicePostCallSurveyAnswer, VoicePostCallSurveyQuestion, VoicePostCallSurveyResponse, CreateVoicePostCallSurveyOptions, } from "./postCallSurvey";
|
|
302
302
|
export { collectVoiceDTMFInput, validateVoiceDTMFLuhn, VOICE_DTMF_DIGITS, } from "./dtmfCollector";
|
|
303
303
|
export type { VoiceDTMFCollector, VoiceDTMFCollectorState, VoiceDTMFDigit, CreateVoiceDTMFCollectorOptions, } from "./dtmfCollector";
|
|
304
|
+
export { createVoiceDNCRegistry, importVoiceDNCFromCSV, } from "./dncRegistry";
|
|
305
|
+
export type { VoiceDNCEntry, VoiceDNCExternalLookup, VoiceDNCLookupVerdict, VoiceDNCRegistry, VoiceDNCSource, CreateVoiceDNCRegistryOptions, } from "./dncRegistry";
|
|
306
|
+
export { createVoiceCallingWindow, VOICE_TCPA_DEFAULT_WINDOW, } from "./callingWindow";
|
|
307
|
+
export type { VoiceCallingDayKey, VoiceCallingTimeRange, VoiceCallingWindow, VoiceCallingWindowOptions, VoiceCallingWindowVerdict, } from "./callingWindow";
|
|
308
|
+
export { createVoiceCallDispositionTagger, DEFAULT_VOICE_CALL_DISPOSITIONS, } from "./callDisposition";
|
|
309
|
+
export type { VoiceCallDispositionDefinition, VoiceCallDispositionTag, VoiceCallDispositionTagger, VoiceCallDispositionTaxonomy, CreateVoiceCallDispositionTaggerOptions, } from "./callDisposition";
|
|
310
|
+
export { createVoiceRetryPolicy } from "./retryPolicy";
|
|
311
|
+
export type { VoiceRetryAttempt, VoiceRetryDecision, VoiceRetryDispositionAction, VoiceRetryDispositionRule, VoiceRetryPolicy, CreateVoiceRetryPolicyOptions, } from "./retryPolicy";
|
|
312
|
+
export { collectVoiceCampaignTemplateVariables, DEFAULT_VOICE_CAMPAIGN_TEMPLATE_FILTERS, resolveVoiceCampaignTemplate, } from "./campaignTemplate";
|
|
313
|
+
export type { ResolveVoiceCampaignTemplateOptions, VoiceCampaignTemplateFilter, VoiceCampaignTemplateResolveResult, VoiceCampaignTemplateScope, VoiceCampaignTemplateValue, } from "./campaignTemplate";
|
|
304
314
|
export * from "./types";
|
package/dist/index.js
CHANGED
|
@@ -48561,6 +48561,616 @@ var validateVoiceDTMFLuhn = (digits) => {
|
|
|
48561
48561
|
}
|
|
48562
48562
|
return sum % 10 === 0;
|
|
48563
48563
|
};
|
|
48564
|
+
// src/dncRegistry.ts
|
|
48565
|
+
var normalizePhone = (phone) => {
|
|
48566
|
+
const trimmed = phone.trim();
|
|
48567
|
+
if (!trimmed)
|
|
48568
|
+
throw new Error("Phone number is required");
|
|
48569
|
+
const digitsOnly = trimmed.replace(/[\s().-]/gu, "");
|
|
48570
|
+
if (!/^\+?\d+$/u.test(digitsOnly)) {
|
|
48571
|
+
throw new Error(`Invalid phone number: ${phone}`);
|
|
48572
|
+
}
|
|
48573
|
+
return digitsOnly.startsWith("+") ? digitsOnly : `+${digitsOnly}`;
|
|
48574
|
+
};
|
|
48575
|
+
var createVoiceDNCRegistry = (options = {}) => {
|
|
48576
|
+
const now = options.now ?? (() => Date.now());
|
|
48577
|
+
const store = new Map;
|
|
48578
|
+
for (const entry of options.entries ?? []) {
|
|
48579
|
+
store.set(normalizePhone(entry.phoneNumber), {
|
|
48580
|
+
...entry,
|
|
48581
|
+
phoneNumber: normalizePhone(entry.phoneNumber)
|
|
48582
|
+
});
|
|
48583
|
+
}
|
|
48584
|
+
const isExpired = (entry, at) => entry.expiresAt !== undefined && entry.expiresAt <= at;
|
|
48585
|
+
const block = (phoneNumber, options2 = {}) => {
|
|
48586
|
+
const normalized = normalizePhone(phoneNumber);
|
|
48587
|
+
const entry = {
|
|
48588
|
+
addedAt: now(),
|
|
48589
|
+
phoneNumber: normalized,
|
|
48590
|
+
source: options2.source ?? "internal",
|
|
48591
|
+
...options2.reason !== undefined ? { reason: options2.reason } : {},
|
|
48592
|
+
...options2.expiresAt !== undefined ? { expiresAt: options2.expiresAt } : {}
|
|
48593
|
+
};
|
|
48594
|
+
store.set(normalized, entry);
|
|
48595
|
+
return entry;
|
|
48596
|
+
};
|
|
48597
|
+
const unblock = (phoneNumber) => {
|
|
48598
|
+
const normalized = normalizePhone(phoneNumber);
|
|
48599
|
+
return store.delete(normalized);
|
|
48600
|
+
};
|
|
48601
|
+
const localLookup = (phoneNumber) => {
|
|
48602
|
+
const normalized = normalizePhone(phoneNumber);
|
|
48603
|
+
const entry = store.get(normalized);
|
|
48604
|
+
if (!entry)
|
|
48605
|
+
return null;
|
|
48606
|
+
if (isExpired(entry, now())) {
|
|
48607
|
+
store.delete(normalized);
|
|
48608
|
+
return null;
|
|
48609
|
+
}
|
|
48610
|
+
return entry;
|
|
48611
|
+
};
|
|
48612
|
+
return {
|
|
48613
|
+
block,
|
|
48614
|
+
async check(phoneNumber) {
|
|
48615
|
+
const local = localLookup(phoneNumber);
|
|
48616
|
+
if (local)
|
|
48617
|
+
return { blocked: true, entry: local };
|
|
48618
|
+
if (!options.externalLookup) {
|
|
48619
|
+
return { blocked: false, entry: null };
|
|
48620
|
+
}
|
|
48621
|
+
const remote = await options.externalLookup(normalizePhone(phoneNumber));
|
|
48622
|
+
if (!remote)
|
|
48623
|
+
return { blocked: false, entry: null };
|
|
48624
|
+
return { blocked: true, entry: remote, matchedFromExternal: true };
|
|
48625
|
+
},
|
|
48626
|
+
checkSync(phoneNumber) {
|
|
48627
|
+
const local = localLookup(phoneNumber);
|
|
48628
|
+
return local ? { blocked: true, entry: local } : { blocked: false, entry: null };
|
|
48629
|
+
},
|
|
48630
|
+
has(phoneNumber) {
|
|
48631
|
+
return localLookup(phoneNumber) !== null;
|
|
48632
|
+
},
|
|
48633
|
+
snapshot() {
|
|
48634
|
+
const at = now();
|
|
48635
|
+
const live = [];
|
|
48636
|
+
for (const entry of store.values()) {
|
|
48637
|
+
if (!isExpired(entry, at))
|
|
48638
|
+
live.push(entry);
|
|
48639
|
+
}
|
|
48640
|
+
return live;
|
|
48641
|
+
},
|
|
48642
|
+
unblock
|
|
48643
|
+
};
|
|
48644
|
+
};
|
|
48645
|
+
var importVoiceDNCFromCSV = (csv, options = {}) => {
|
|
48646
|
+
const lines = csv.split(/\r?\n/u).filter((line) => line.trim().length > 0);
|
|
48647
|
+
if (lines.length === 0)
|
|
48648
|
+
return [];
|
|
48649
|
+
const header = (lines[0] ?? "").split(",").map((h) => h.trim().toLowerCase());
|
|
48650
|
+
const phoneCol = options.phoneColumn?.toLowerCase() ?? "phone";
|
|
48651
|
+
const reasonCol = options.reasonColumn?.toLowerCase() ?? "reason";
|
|
48652
|
+
const phoneIdx = header.indexOf(phoneCol);
|
|
48653
|
+
const reasonIdx = header.indexOf(reasonCol);
|
|
48654
|
+
if (phoneIdx === -1) {
|
|
48655
|
+
throw new Error(`Phone column not found in CSV header: ${phoneCol}`);
|
|
48656
|
+
}
|
|
48657
|
+
const now = options.now ?? (() => Date.now());
|
|
48658
|
+
const at = now();
|
|
48659
|
+
const entries = [];
|
|
48660
|
+
for (let i = 1;i < lines.length; i++) {
|
|
48661
|
+
const row = (lines[i] ?? "").split(",").map((cell) => cell.trim());
|
|
48662
|
+
const phone = row[phoneIdx];
|
|
48663
|
+
if (!phone)
|
|
48664
|
+
continue;
|
|
48665
|
+
entries.push({
|
|
48666
|
+
addedAt: at,
|
|
48667
|
+
phoneNumber: normalizePhone(phone),
|
|
48668
|
+
source: options.source ?? "imported",
|
|
48669
|
+
...reasonIdx >= 0 && row[reasonIdx] ? { reason: row[reasonIdx] } : {}
|
|
48670
|
+
});
|
|
48671
|
+
}
|
|
48672
|
+
return entries;
|
|
48673
|
+
};
|
|
48674
|
+
// src/callingWindow.ts
|
|
48675
|
+
var DAY_INDEX = {
|
|
48676
|
+
friday: 5,
|
|
48677
|
+
monday: 1,
|
|
48678
|
+
saturday: 6,
|
|
48679
|
+
sunday: 0,
|
|
48680
|
+
thursday: 4,
|
|
48681
|
+
tuesday: 2,
|
|
48682
|
+
wednesday: 3
|
|
48683
|
+
};
|
|
48684
|
+
var DAY_KEYS = [
|
|
48685
|
+
"sunday",
|
|
48686
|
+
"monday",
|
|
48687
|
+
"tuesday",
|
|
48688
|
+
"wednesday",
|
|
48689
|
+
"thursday",
|
|
48690
|
+
"friday",
|
|
48691
|
+
"saturday"
|
|
48692
|
+
];
|
|
48693
|
+
var parseTime = (value) => {
|
|
48694
|
+
const match = /^([0-9]{1,2}):([0-9]{2})$/u.exec(value);
|
|
48695
|
+
if (!match)
|
|
48696
|
+
throw new Error(`Invalid time string (expected HH:MM): ${value}`);
|
|
48697
|
+
const hour = Number(match[1]);
|
|
48698
|
+
const minute = Number(match[2]);
|
|
48699
|
+
if (hour < 0 || hour > 23) {
|
|
48700
|
+
throw new RangeError(`Hour out of range: ${value}`);
|
|
48701
|
+
}
|
|
48702
|
+
if (minute < 0 || minute > 59) {
|
|
48703
|
+
throw new RangeError(`Minute out of range: ${value}`);
|
|
48704
|
+
}
|
|
48705
|
+
return { hour, minute };
|
|
48706
|
+
};
|
|
48707
|
+
var minutesOf = (range) => {
|
|
48708
|
+
const start = parseTime(range.start);
|
|
48709
|
+
const end = parseTime(range.end);
|
|
48710
|
+
return {
|
|
48711
|
+
end: end.hour * 60 + end.minute,
|
|
48712
|
+
start: start.hour * 60 + start.minute
|
|
48713
|
+
};
|
|
48714
|
+
};
|
|
48715
|
+
var parts = (date, timezone) => {
|
|
48716
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
48717
|
+
day: "2-digit",
|
|
48718
|
+
hour: "2-digit",
|
|
48719
|
+
hour12: false,
|
|
48720
|
+
minute: "2-digit",
|
|
48721
|
+
month: "2-digit",
|
|
48722
|
+
timeZone: timezone,
|
|
48723
|
+
weekday: "short",
|
|
48724
|
+
year: "numeric"
|
|
48725
|
+
});
|
|
48726
|
+
const map = {};
|
|
48727
|
+
for (const part of formatter.formatToParts(date)) {
|
|
48728
|
+
if (part.type !== "literal")
|
|
48729
|
+
map[part.type] = part.value;
|
|
48730
|
+
}
|
|
48731
|
+
const weekdayMap = {
|
|
48732
|
+
Fri: 5,
|
|
48733
|
+
Mon: 1,
|
|
48734
|
+
Sat: 6,
|
|
48735
|
+
Sun: 0,
|
|
48736
|
+
Thu: 4,
|
|
48737
|
+
Tue: 2,
|
|
48738
|
+
Wed: 3
|
|
48739
|
+
};
|
|
48740
|
+
const hourValue = map.hour === "24" ? "00" : map.hour ?? "0";
|
|
48741
|
+
return {
|
|
48742
|
+
day: map.day ?? "00",
|
|
48743
|
+
minutes: Number(hourValue) * 60 + Number(map.minute ?? "0"),
|
|
48744
|
+
month: map.month ?? "00",
|
|
48745
|
+
weekday: weekdayMap[map.weekday ?? ""] ?? 0,
|
|
48746
|
+
year: map.year ?? "0000"
|
|
48747
|
+
};
|
|
48748
|
+
};
|
|
48749
|
+
var createVoiceCallingWindow = (options = {}) => {
|
|
48750
|
+
const now = options.now ?? (() => new Date);
|
|
48751
|
+
const allowedDayIndexes = new Set((options.allowedDays ?? [
|
|
48752
|
+
"monday",
|
|
48753
|
+
"tuesday",
|
|
48754
|
+
"wednesday",
|
|
48755
|
+
"thursday",
|
|
48756
|
+
"friday"
|
|
48757
|
+
]).map((d) => DAY_INDEX[d]));
|
|
48758
|
+
const blockedDates = new Set(options.blockedDates ?? []);
|
|
48759
|
+
const baseRanges = !options.allowedHours ? [{ end: "21:00", start: "08:00" }] : Array.isArray(options.allowedHours) ? options.allowedHours : [options.allowedHours];
|
|
48760
|
+
const baseMinutes = baseRanges.map(minutesOf);
|
|
48761
|
+
const perDayMinutes = new Map;
|
|
48762
|
+
for (const [key, ranges] of Object.entries(options.perDayHours ?? {})) {
|
|
48763
|
+
if (!ranges)
|
|
48764
|
+
continue;
|
|
48765
|
+
perDayMinutes.set(DAY_INDEX[key], ranges.map(minutesOf));
|
|
48766
|
+
}
|
|
48767
|
+
const rangesFor = (weekday) => perDayMinutes.get(weekday) ?? baseMinutes;
|
|
48768
|
+
const isAllowedAt = (date) => {
|
|
48769
|
+
const p = parts(date, options.timezone);
|
|
48770
|
+
const isoDate = `${p.year}-${p.month}-${p.day}`;
|
|
48771
|
+
if (blockedDates.has(isoDate)) {
|
|
48772
|
+
return { allowed: false, reason: "blocked-date" };
|
|
48773
|
+
}
|
|
48774
|
+
if (!allowedDayIndexes.has(p.weekday)) {
|
|
48775
|
+
return { allowed: false, reason: "outside-day" };
|
|
48776
|
+
}
|
|
48777
|
+
const ranges = rangesFor(p.weekday);
|
|
48778
|
+
for (const range of ranges) {
|
|
48779
|
+
if (p.minutes >= range.start && p.minutes < range.end) {
|
|
48780
|
+
return { allowed: true };
|
|
48781
|
+
}
|
|
48782
|
+
}
|
|
48783
|
+
return { allowed: false, reason: "outside-hours" };
|
|
48784
|
+
};
|
|
48785
|
+
const findNextOpening = (from) => {
|
|
48786
|
+
const cursor = new Date(from.getTime());
|
|
48787
|
+
for (let step = 0;step < 14 * 24 * 60; step++) {
|
|
48788
|
+
const verdict = isAllowedAt(cursor);
|
|
48789
|
+
if (verdict.allowed)
|
|
48790
|
+
return cursor.getTime();
|
|
48791
|
+
cursor.setTime(cursor.getTime() + 60000);
|
|
48792
|
+
}
|
|
48793
|
+
return cursor.getTime();
|
|
48794
|
+
};
|
|
48795
|
+
return {
|
|
48796
|
+
allowedDays: [...allowedDayIndexes].map((idx) => DAY_KEYS[idx]),
|
|
48797
|
+
canCallNow(at) {
|
|
48798
|
+
const date = at ?? now();
|
|
48799
|
+
const verdict = isAllowedAt(date);
|
|
48800
|
+
if (verdict.allowed)
|
|
48801
|
+
return verdict;
|
|
48802
|
+
return { ...verdict, nextWindowAt: findNextOpening(date) };
|
|
48803
|
+
},
|
|
48804
|
+
nextWindowOpensAt(at) {
|
|
48805
|
+
const date = at ?? now();
|
|
48806
|
+
const verdict = isAllowedAt(date);
|
|
48807
|
+
if (verdict.allowed)
|
|
48808
|
+
return date.getTime();
|
|
48809
|
+
return findNextOpening(date);
|
|
48810
|
+
},
|
|
48811
|
+
timezone: options.timezone
|
|
48812
|
+
};
|
|
48813
|
+
};
|
|
48814
|
+
var VOICE_TCPA_DEFAULT_WINDOW = {
|
|
48815
|
+
allowedDays: ["monday", "tuesday", "wednesday", "thursday", "friday"],
|
|
48816
|
+
allowedHours: { end: "21:00", start: "08:00" }
|
|
48817
|
+
};
|
|
48818
|
+
// src/callDisposition.ts
|
|
48819
|
+
var DEFAULT_VOICE_CALL_DISPOSITIONS = [
|
|
48820
|
+
{
|
|
48821
|
+
code: "sale",
|
|
48822
|
+
label: "Sale closed",
|
|
48823
|
+
outcome: "positive",
|
|
48824
|
+
retryable: false,
|
|
48825
|
+
taxonomy: "sales"
|
|
48826
|
+
},
|
|
48827
|
+
{
|
|
48828
|
+
code: "qualified-lead",
|
|
48829
|
+
label: "Qualified lead",
|
|
48830
|
+
outcome: "positive",
|
|
48831
|
+
retryable: false,
|
|
48832
|
+
taxonomy: "sales"
|
|
48833
|
+
},
|
|
48834
|
+
{
|
|
48835
|
+
code: "callback-requested",
|
|
48836
|
+
label: "Callback requested",
|
|
48837
|
+
outcome: "neutral",
|
|
48838
|
+
retryable: true,
|
|
48839
|
+
taxonomy: "sales"
|
|
48840
|
+
},
|
|
48841
|
+
{
|
|
48842
|
+
code: "not-interested",
|
|
48843
|
+
label: "Not interested",
|
|
48844
|
+
outcome: "negative",
|
|
48845
|
+
retryable: false,
|
|
48846
|
+
taxonomy: "sales"
|
|
48847
|
+
},
|
|
48848
|
+
{
|
|
48849
|
+
code: "do-not-call",
|
|
48850
|
+
label: "Do not call",
|
|
48851
|
+
outcome: "negative",
|
|
48852
|
+
retryable: false,
|
|
48853
|
+
taxonomy: "sales"
|
|
48854
|
+
},
|
|
48855
|
+
{
|
|
48856
|
+
code: "voicemail-left",
|
|
48857
|
+
label: "Voicemail left",
|
|
48858
|
+
outcome: "no-contact",
|
|
48859
|
+
retryable: true,
|
|
48860
|
+
taxonomy: "sales"
|
|
48861
|
+
},
|
|
48862
|
+
{
|
|
48863
|
+
code: "no-answer",
|
|
48864
|
+
label: "No answer",
|
|
48865
|
+
outcome: "no-contact",
|
|
48866
|
+
retryable: true,
|
|
48867
|
+
taxonomy: "sales"
|
|
48868
|
+
},
|
|
48869
|
+
{
|
|
48870
|
+
code: "busy",
|
|
48871
|
+
label: "Line busy",
|
|
48872
|
+
outcome: "no-contact",
|
|
48873
|
+
retryable: true,
|
|
48874
|
+
taxonomy: "sales"
|
|
48875
|
+
},
|
|
48876
|
+
{
|
|
48877
|
+
code: "wrong-number",
|
|
48878
|
+
label: "Wrong number",
|
|
48879
|
+
outcome: "negative",
|
|
48880
|
+
retryable: false,
|
|
48881
|
+
taxonomy: "sales"
|
|
48882
|
+
},
|
|
48883
|
+
{
|
|
48884
|
+
code: "resolved",
|
|
48885
|
+
label: "Issue resolved",
|
|
48886
|
+
outcome: "positive",
|
|
48887
|
+
retryable: false,
|
|
48888
|
+
taxonomy: "support"
|
|
48889
|
+
},
|
|
48890
|
+
{
|
|
48891
|
+
code: "escalated",
|
|
48892
|
+
label: "Escalated to human",
|
|
48893
|
+
outcome: "neutral",
|
|
48894
|
+
retryable: false,
|
|
48895
|
+
taxonomy: "support"
|
|
48896
|
+
},
|
|
48897
|
+
{
|
|
48898
|
+
code: "payment-promised",
|
|
48899
|
+
label: "Payment promised",
|
|
48900
|
+
outcome: "positive",
|
|
48901
|
+
retryable: false,
|
|
48902
|
+
taxonomy: "collections"
|
|
48903
|
+
},
|
|
48904
|
+
{
|
|
48905
|
+
code: "dispute-raised",
|
|
48906
|
+
label: "Dispute raised",
|
|
48907
|
+
outcome: "negative",
|
|
48908
|
+
retryable: false,
|
|
48909
|
+
taxonomy: "collections"
|
|
48910
|
+
}
|
|
48911
|
+
];
|
|
48912
|
+
var createVoiceCallDispositionTagger = (options = {}) => {
|
|
48913
|
+
const now = options.now ?? (() => Date.now());
|
|
48914
|
+
const definitions = options.taxonomy ?? DEFAULT_VOICE_CALL_DISPOSITIONS;
|
|
48915
|
+
const byCode = new Map(definitions.map((d) => [d.code, d]));
|
|
48916
|
+
const allowMultiple = options.allowMultiple ?? false;
|
|
48917
|
+
const tags = new Map;
|
|
48918
|
+
const tag = (sessionId, code, note) => {
|
|
48919
|
+
if (!byCode.has(code)) {
|
|
48920
|
+
throw new Error(`Unknown disposition code: ${code}`);
|
|
48921
|
+
}
|
|
48922
|
+
const entry = {
|
|
48923
|
+
code,
|
|
48924
|
+
sessionId,
|
|
48925
|
+
taggedAt: now(),
|
|
48926
|
+
...note !== undefined ? { note } : {}
|
|
48927
|
+
};
|
|
48928
|
+
const existing = tags.get(sessionId) ?? [];
|
|
48929
|
+
if (!allowMultiple) {
|
|
48930
|
+
tags.set(sessionId, [entry]);
|
|
48931
|
+
} else {
|
|
48932
|
+
tags.set(sessionId, [...existing, entry]);
|
|
48933
|
+
}
|
|
48934
|
+
return entry;
|
|
48935
|
+
};
|
|
48936
|
+
const untag = (sessionId, code) => {
|
|
48937
|
+
const existing = tags.get(sessionId);
|
|
48938
|
+
if (!existing)
|
|
48939
|
+
return 0;
|
|
48940
|
+
if (code === undefined) {
|
|
48941
|
+
tags.delete(sessionId);
|
|
48942
|
+
return existing.length;
|
|
48943
|
+
}
|
|
48944
|
+
const filtered = existing.filter((t) => t.code !== code);
|
|
48945
|
+
const removed = existing.length - filtered.length;
|
|
48946
|
+
if (filtered.length === 0)
|
|
48947
|
+
tags.delete(sessionId);
|
|
48948
|
+
else
|
|
48949
|
+
tags.set(sessionId, filtered);
|
|
48950
|
+
return removed;
|
|
48951
|
+
};
|
|
48952
|
+
const listForSession = (sessionId) => tags.get(sessionId)?.slice() ?? [];
|
|
48953
|
+
const listAll = () => {
|
|
48954
|
+
const flat = [];
|
|
48955
|
+
for (const list of tags.values())
|
|
48956
|
+
flat.push(...list);
|
|
48957
|
+
return flat;
|
|
48958
|
+
};
|
|
48959
|
+
const summarize = () => {
|
|
48960
|
+
const byCodeCount = new Map;
|
|
48961
|
+
const byOutcomeCount = { negative: 0, neutral: 0, "no-contact": 0, positive: 0 };
|
|
48962
|
+
let retryableTagged = 0;
|
|
48963
|
+
let totalTagged = 0;
|
|
48964
|
+
for (const list of tags.values()) {
|
|
48965
|
+
for (const entry of list) {
|
|
48966
|
+
totalTagged += 1;
|
|
48967
|
+
byCodeCount.set(entry.code, (byCodeCount.get(entry.code) ?? 0) + 1);
|
|
48968
|
+
const def = byCode.get(entry.code);
|
|
48969
|
+
if (!def)
|
|
48970
|
+
continue;
|
|
48971
|
+
byOutcomeCount[def.outcome] += 1;
|
|
48972
|
+
if (def.retryable)
|
|
48973
|
+
retryableTagged += 1;
|
|
48974
|
+
}
|
|
48975
|
+
}
|
|
48976
|
+
return {
|
|
48977
|
+
byCode: Object.fromEntries(byCodeCount),
|
|
48978
|
+
byOutcome: byOutcomeCount,
|
|
48979
|
+
retryablePct: totalTagged === 0 ? 0 : retryableTagged / totalTagged,
|
|
48980
|
+
totalSessions: tags.size,
|
|
48981
|
+
totalTagged
|
|
48982
|
+
};
|
|
48983
|
+
};
|
|
48984
|
+
return {
|
|
48985
|
+
definitionFor: (code) => byCode.get(code) ?? null,
|
|
48986
|
+
definitions,
|
|
48987
|
+
listAll,
|
|
48988
|
+
listForSession,
|
|
48989
|
+
summarize,
|
|
48990
|
+
tag,
|
|
48991
|
+
untag
|
|
48992
|
+
};
|
|
48993
|
+
};
|
|
48994
|
+
// src/retryPolicy.ts
|
|
48995
|
+
var DEFAULT_RULES = [
|
|
48996
|
+
{ action: "retry", cooldownMs: 4 * 60 * 60 * 1000, disposition: "voicemail-left" },
|
|
48997
|
+
{ action: "retry", cooldownMs: 30 * 60 * 1000, disposition: "no-answer" },
|
|
48998
|
+
{ action: "retry", cooldownMs: 10 * 60 * 1000, disposition: "busy" },
|
|
48999
|
+
{ action: "retry", cooldownMs: 24 * 60 * 60 * 1000, disposition: "callback-requested" },
|
|
49000
|
+
{ action: "abandon", disposition: "do-not-call" },
|
|
49001
|
+
{ action: "abandon", disposition: "not-interested" },
|
|
49002
|
+
{ action: "abandon", disposition: "wrong-number" },
|
|
49003
|
+
{ action: "abandon", disposition: "sale" }
|
|
49004
|
+
];
|
|
49005
|
+
var createVoiceRetryPolicy = (options = {}) => {
|
|
49006
|
+
const now = options.now ?? (() => Date.now());
|
|
49007
|
+
const maxAttempts = options.maxAttempts ?? 3;
|
|
49008
|
+
const baseCooldown = options.defaultCooldownMs ?? 30 * 60 * 1000;
|
|
49009
|
+
const jitter = options.jitterMs ?? 60 * 1000;
|
|
49010
|
+
const backoff = options.backoffMultiplier ?? 1.5;
|
|
49011
|
+
const escalateAfter = options.escalateAfterAttempts ?? Infinity;
|
|
49012
|
+
const rulesByDisposition = new Map;
|
|
49013
|
+
for (const rule of options.rules ?? DEFAULT_RULES) {
|
|
49014
|
+
rulesByDisposition.set(rule.disposition, rule);
|
|
49015
|
+
}
|
|
49016
|
+
const decide = (history, lastDisposition) => {
|
|
49017
|
+
const rule = rulesByDisposition.get(lastDisposition);
|
|
49018
|
+
const attemptCount = history.length;
|
|
49019
|
+
const max = rule?.maxAttemptsOverride ?? maxAttempts;
|
|
49020
|
+
if (attemptCount >= escalateAfter) {
|
|
49021
|
+
return {
|
|
49022
|
+
action: "escalate",
|
|
49023
|
+
reason: `${attemptCount} attempts without contact`
|
|
49024
|
+
};
|
|
49025
|
+
}
|
|
49026
|
+
if (!rule || rule.action === "abandon") {
|
|
49027
|
+
return { action: "abandon", reason: "non-retryable" };
|
|
49028
|
+
}
|
|
49029
|
+
if (rule.action === "escalate") {
|
|
49030
|
+
return { action: "escalate", reason: lastDisposition };
|
|
49031
|
+
}
|
|
49032
|
+
if (attemptCount >= max) {
|
|
49033
|
+
return { action: "abandon", reason: "max-attempts" };
|
|
49034
|
+
}
|
|
49035
|
+
const cooldown = rule.cooldownMs ?? baseCooldown;
|
|
49036
|
+
const exponent = Math.pow(backoff, Math.max(0, attemptCount - 1));
|
|
49037
|
+
const jitterDelta = jitter > 0 ? Math.floor(Math.random() * jitter) : 0;
|
|
49038
|
+
const retryAt = now() + Math.round(cooldown * exponent) + jitterDelta;
|
|
49039
|
+
return {
|
|
49040
|
+
action: "retry",
|
|
49041
|
+
attemptNumber: attemptCount + 1,
|
|
49042
|
+
retryAt
|
|
49043
|
+
};
|
|
49044
|
+
};
|
|
49045
|
+
return {
|
|
49046
|
+
decide,
|
|
49047
|
+
maxAttempts,
|
|
49048
|
+
rules: () => Array.from(rulesByDisposition.values()),
|
|
49049
|
+
updateRule(rule) {
|
|
49050
|
+
rulesByDisposition.set(rule.disposition, rule);
|
|
49051
|
+
}
|
|
49052
|
+
};
|
|
49053
|
+
};
|
|
49054
|
+
// src/campaignTemplate.ts
|
|
49055
|
+
var lookupPath = (scope, path) => {
|
|
49056
|
+
const parts2 = path.split(".");
|
|
49057
|
+
let cursor = scope;
|
|
49058
|
+
for (const part of parts2) {
|
|
49059
|
+
if (cursor === null || cursor === undefined)
|
|
49060
|
+
return;
|
|
49061
|
+
if (typeof cursor !== "object")
|
|
49062
|
+
return;
|
|
49063
|
+
cursor = cursor[part];
|
|
49064
|
+
}
|
|
49065
|
+
return cursor;
|
|
49066
|
+
};
|
|
49067
|
+
var escapeSpeech = (text) => text.replace(/[<&>"]/gu, (char) => {
|
|
49068
|
+
switch (char) {
|
|
49069
|
+
case "&":
|
|
49070
|
+
return "&";
|
|
49071
|
+
case "<":
|
|
49072
|
+
return "<";
|
|
49073
|
+
case ">":
|
|
49074
|
+
return ">";
|
|
49075
|
+
case '"':
|
|
49076
|
+
return """;
|
|
49077
|
+
default:
|
|
49078
|
+
return char;
|
|
49079
|
+
}
|
|
49080
|
+
});
|
|
49081
|
+
var formatPhone = (phone) => {
|
|
49082
|
+
const digits = phone.replace(/\D/gu, "");
|
|
49083
|
+
if (digits.length === 10) {
|
|
49084
|
+
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
|
|
49085
|
+
}
|
|
49086
|
+
if (digits.length === 11 && digits.startsWith("1")) {
|
|
49087
|
+
return `+1 (${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7)}`;
|
|
49088
|
+
}
|
|
49089
|
+
return phone;
|
|
49090
|
+
};
|
|
49091
|
+
var formatDate = (value, locale) => {
|
|
49092
|
+
if (value === null || value === undefined)
|
|
49093
|
+
return "";
|
|
49094
|
+
const date = new Date(typeof value === "number" ? value : String(value));
|
|
49095
|
+
if (Number.isNaN(date.getTime()))
|
|
49096
|
+
return String(value);
|
|
49097
|
+
return date.toLocaleDateString(locale ?? "en-US", {
|
|
49098
|
+
day: "numeric",
|
|
49099
|
+
month: "long",
|
|
49100
|
+
year: "numeric"
|
|
49101
|
+
});
|
|
49102
|
+
};
|
|
49103
|
+
var DEFAULT_VOICE_CAMPAIGN_TEMPLATE_FILTERS = {
|
|
49104
|
+
capitalize: (value) => {
|
|
49105
|
+
if (value === null || value === undefined || value === "")
|
|
49106
|
+
return value ?? "";
|
|
49107
|
+
const text = String(value);
|
|
49108
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
49109
|
+
},
|
|
49110
|
+
currency: (value, currency = "USD") => {
|
|
49111
|
+
if (typeof value !== "number")
|
|
49112
|
+
return String(value ?? "");
|
|
49113
|
+
return new Intl.NumberFormat("en-US", {
|
|
49114
|
+
currency,
|
|
49115
|
+
style: "currency"
|
|
49116
|
+
}).format(value);
|
|
49117
|
+
},
|
|
49118
|
+
date: (value, locale) => formatDate(value, locale),
|
|
49119
|
+
default: (value, fallback) => value === null || value === undefined || value === "" ? fallback ?? "" : value,
|
|
49120
|
+
lower: (value) => String(value ?? "").toLowerCase(),
|
|
49121
|
+
phone: (value) => formatPhone(String(value ?? "")),
|
|
49122
|
+
ssml: (value) => escapeSpeech(String(value ?? "")),
|
|
49123
|
+
upper: (value) => String(value ?? "").toUpperCase()
|
|
49124
|
+
};
|
|
49125
|
+
var renderValue = (value, filters, filterChain) => {
|
|
49126
|
+
let cursor = value;
|
|
49127
|
+
for (const segment of filterChain) {
|
|
49128
|
+
const parts2 = segment.split(":");
|
|
49129
|
+
const name = (parts2[0] ?? "").trim();
|
|
49130
|
+
if (!name)
|
|
49131
|
+
continue;
|
|
49132
|
+
const args = parts2.slice(1).join(":").split(",").map((a) => a.trim()).filter((a) => a.length > 0);
|
|
49133
|
+
const filter = filters[name];
|
|
49134
|
+
if (!filter)
|
|
49135
|
+
throw new Error(`Unknown template filter: ${name}`);
|
|
49136
|
+
cursor = filter(cursor, ...args);
|
|
49137
|
+
}
|
|
49138
|
+
if (cursor === null || cursor === undefined)
|
|
49139
|
+
return "";
|
|
49140
|
+
return String(cursor);
|
|
49141
|
+
};
|
|
49142
|
+
var resolveVoiceCampaignTemplate = (template, options) => {
|
|
49143
|
+
const filters = {
|
|
49144
|
+
...DEFAULT_VOICE_CAMPAIGN_TEMPLATE_FILTERS,
|
|
49145
|
+
...options.filters ?? {}
|
|
49146
|
+
};
|
|
49147
|
+
const missing = new Set;
|
|
49148
|
+
const output = template.replace(/\{\{([^{}]+)\}\}/gu, (match, expression) => {
|
|
49149
|
+
const segments = expression.split("|").map((part) => part.trim());
|
|
49150
|
+
const path = segments[0] ?? "";
|
|
49151
|
+
const value = lookupPath(options.scope, path);
|
|
49152
|
+
if (value === undefined) {
|
|
49153
|
+
missing.add(path);
|
|
49154
|
+
if (options.strict) {
|
|
49155
|
+
throw new Error(`Missing template variable: ${path}`);
|
|
49156
|
+
}
|
|
49157
|
+
return options.fallback ?? "";
|
|
49158
|
+
}
|
|
49159
|
+
return renderValue(value, filters, segments.slice(1));
|
|
49160
|
+
});
|
|
49161
|
+
return { missingVariables: Array.from(missing), output };
|
|
49162
|
+
};
|
|
49163
|
+
var collectVoiceCampaignTemplateVariables = (template) => {
|
|
49164
|
+
const set = new Set;
|
|
49165
|
+
const matches = template.matchAll(/\{\{([^{}]+)\}\}/gu);
|
|
49166
|
+
for (const match of matches) {
|
|
49167
|
+
const expression = match[1] ?? "";
|
|
49168
|
+
const path = (expression.split("|")[0] ?? "").trim();
|
|
49169
|
+
if (path)
|
|
49170
|
+
set.add(path);
|
|
49171
|
+
}
|
|
49172
|
+
return Array.from(set);
|
|
49173
|
+
};
|
|
48564
49174
|
export {
|
|
48565
49175
|
writeVoiceProofPack,
|
|
48566
49176
|
writeVoiceMediaPipelineArtifacts,
|
|
@@ -48668,6 +49278,7 @@ export {
|
|
|
48668
49278
|
resolveVoiceMonitorIssue,
|
|
48669
49279
|
resolveVoiceDiagnosticsTraceFilter,
|
|
48670
49280
|
resolveVoiceDashboardRenderers,
|
|
49281
|
+
resolveVoiceCampaignTemplate,
|
|
48671
49282
|
resolveVoiceAuditTrailFilter,
|
|
48672
49283
|
resolveVoiceAuditDeliveryFilter,
|
|
48673
49284
|
resolveVoiceAssistantMode,
|
|
@@ -48825,6 +49436,7 @@ export {
|
|
|
48825
49436
|
isVoiceOpsTaskOverdue,
|
|
48826
49437
|
isPhoneOnDNC,
|
|
48827
49438
|
interleaveStereoPcm,
|
|
49439
|
+
importVoiceDNCFromCSV,
|
|
48828
49440
|
importVoiceCampaignRecipients,
|
|
48829
49441
|
heartbeatVoiceOpsTask,
|
|
48830
49442
|
hasVoiceOpsTaskSLABreach,
|
|
@@ -49002,6 +49614,7 @@ export {
|
|
|
49002
49614
|
createVoiceRoutingDecisionSummary,
|
|
49003
49615
|
createVoiceRouteAuth,
|
|
49004
49616
|
createVoiceReviewSavedEvent,
|
|
49617
|
+
createVoiceRetryPolicy,
|
|
49005
49618
|
createVoiceRetentionScheduler,
|
|
49006
49619
|
createVoiceResilienceRoutes,
|
|
49007
49620
|
createVoiceReplayTimelineHTMXRoute,
|
|
@@ -49196,6 +49809,7 @@ export {
|
|
|
49196
49809
|
createVoiceDeliveryRuntime,
|
|
49197
49810
|
createVoiceDataControlRoutes,
|
|
49198
49811
|
createVoiceDTMFTool,
|
|
49812
|
+
createVoiceDNCRegistry,
|
|
49199
49813
|
createVoiceCostDashboardHTMXRoute,
|
|
49200
49814
|
createVoiceCostAccountant,
|
|
49201
49815
|
createVoiceCompetitiveCoverageRoutes,
|
|
@@ -49205,11 +49819,13 @@ export {
|
|
|
49205
49819
|
createVoiceCampaignTelephonyOutcomeHandler,
|
|
49206
49820
|
createVoiceCampaignRoutes,
|
|
49207
49821
|
createVoiceCampaign,
|
|
49822
|
+
createVoiceCallingWindow,
|
|
49208
49823
|
createVoiceCallerMemoryNamespace,
|
|
49209
49824
|
createVoiceCallReviewRecorder,
|
|
49210
49825
|
createVoiceCallReviewFromSession,
|
|
49211
49826
|
createVoiceCallReviewFromLiveTelephonyReport,
|
|
49212
49827
|
createVoiceCallPlayer,
|
|
49828
|
+
createVoiceCallDispositionTagger,
|
|
49213
49829
|
createVoiceCallDebuggerRoutes,
|
|
49214
49830
|
createVoiceCallCompletedEvent,
|
|
49215
49831
|
createVoiceCRMActivitySink,
|
|
@@ -49285,6 +49901,7 @@ export {
|
|
|
49285
49901
|
compareVoiceEvalBaseline,
|
|
49286
49902
|
compareVoiceCostScenarios,
|
|
49287
49903
|
collectVoiceDTMFInput,
|
|
49904
|
+
collectVoiceCampaignTemplateVariables,
|
|
49288
49905
|
claimVoiceOpsTask,
|
|
49289
49906
|
buildVoiceTraceReplay,
|
|
49290
49907
|
buildVoiceTraceDeliveryReport,
|
|
@@ -49435,6 +50052,7 @@ export {
|
|
|
49435
50052
|
acknowledgeVoiceMonitorIssue,
|
|
49436
50053
|
VOICE_WEBHOOK_TIMESTAMP_HEADER,
|
|
49437
50054
|
VOICE_WEBHOOK_SIGNATURE_HEADER,
|
|
50055
|
+
VOICE_TCPA_DEFAULT_WINDOW,
|
|
49438
50056
|
VOICE_LIVE_OPS_ACTIONS,
|
|
49439
50057
|
VOICE_DTMF_DIGITS,
|
|
49440
50058
|
VOICE_CALLER_MEMORY_KEY,
|
|
@@ -49445,5 +50063,7 @@ export {
|
|
|
49445
50063
|
DEFAULT_VOICE_PROMPT_INJECTION_RULES,
|
|
49446
50064
|
DEFAULT_VOICE_PRICE_BOOK,
|
|
49447
50065
|
DEFAULT_VOICE_POST_CALL_SURVEY_QUESTIONS,
|
|
50066
|
+
DEFAULT_VOICE_CAMPAIGN_TEMPLATE_FILTERS,
|
|
50067
|
+
DEFAULT_VOICE_CALL_DISPOSITIONS,
|
|
49448
50068
|
BROWSER_NOISE_SUPPRESSOR_PRESETS
|
|
49449
50069
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type VoiceRetryDispositionAction = "retry" | "abandon" | "escalate";
|
|
2
|
+
export type VoiceRetryDispositionRule = {
|
|
3
|
+
disposition: string;
|
|
4
|
+
action: VoiceRetryDispositionAction;
|
|
5
|
+
cooldownMs?: number;
|
|
6
|
+
maxAttemptsOverride?: number;
|
|
7
|
+
};
|
|
8
|
+
export type VoiceRetryAttempt = {
|
|
9
|
+
disposition: string;
|
|
10
|
+
at: number;
|
|
11
|
+
};
|
|
12
|
+
export type VoiceRetryDecision = {
|
|
13
|
+
action: "retry";
|
|
14
|
+
attemptNumber: number;
|
|
15
|
+
retryAt: number;
|
|
16
|
+
} | {
|
|
17
|
+
action: "abandon";
|
|
18
|
+
reason: "max-attempts" | "non-retryable" | "explicit";
|
|
19
|
+
} | {
|
|
20
|
+
action: "escalate";
|
|
21
|
+
reason: string;
|
|
22
|
+
};
|
|
23
|
+
export type CreateVoiceRetryPolicyOptions = {
|
|
24
|
+
maxAttempts?: number;
|
|
25
|
+
defaultCooldownMs?: number;
|
|
26
|
+
jitterMs?: number;
|
|
27
|
+
backoffMultiplier?: number;
|
|
28
|
+
rules?: VoiceRetryDispositionRule[];
|
|
29
|
+
escalateAfterAttempts?: number;
|
|
30
|
+
now?: () => number;
|
|
31
|
+
};
|
|
32
|
+
export declare const createVoiceRetryPolicy: (options?: CreateVoiceRetryPolicyOptions) => {
|
|
33
|
+
decide: (history: VoiceRetryAttempt[], lastDisposition: string) => VoiceRetryDecision;
|
|
34
|
+
maxAttempts: number;
|
|
35
|
+
rules: () => VoiceRetryDispositionRule[];
|
|
36
|
+
updateRule(rule: VoiceRetryDispositionRule): void;
|
|
37
|
+
};
|
|
38
|
+
export type VoiceRetryPolicy = ReturnType<typeof createVoiceRetryPolicy>;
|