@qcobro/common 1.11.3 → 1.11.4

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 (173) hide show
  1. package/dist/config.d.ts +307 -0
  2. package/dist/config.d.ts.map +1 -0
  3. package/{src/config.ts → dist/config.js} +123 -154
  4. package/dist/config.js.map +1 -0
  5. package/dist/errors/ValidationError.d.ts +27 -0
  6. package/dist/errors/ValidationError.d.ts.map +1 -0
  7. package/dist/errors/ValidationError.js +52 -0
  8. package/dist/errors/ValidationError.js.map +1 -0
  9. package/{src/errors/index.ts → dist/errors/index.d.ts} +1 -0
  10. package/dist/errors/index.d.ts.map +1 -0
  11. package/dist/errors/index.js +2 -0
  12. package/dist/errors/index.js.map +1 -0
  13. package/dist/index.d.ts +19 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/{src/index.ts → dist/index.js} +2 -5
  16. package/dist/index.js.map +1 -0
  17. package/dist/schemas/agentTemplates.d.ts +73 -0
  18. package/dist/schemas/agentTemplates.d.ts.map +1 -0
  19. package/dist/schemas/agentTemplates.js +90 -0
  20. package/dist/schemas/agentTemplates.js.map +1 -0
  21. package/dist/schemas/apiKeys.d.ts +17 -0
  22. package/dist/schemas/apiKeys.d.ts.map +1 -0
  23. package/{src/schemas/apiKeys.ts → dist/schemas/apiKeys.js} +11 -16
  24. package/dist/schemas/apiKeys.js.map +1 -0
  25. package/dist/schemas/auth.d.ts +74 -0
  26. package/dist/schemas/auth.d.ts.map +1 -0
  27. package/dist/schemas/auth.js +53 -0
  28. package/dist/schemas/auth.js.map +1 -0
  29. package/dist/schemas/campaigns.d.ts +61 -0
  30. package/dist/schemas/campaigns.d.ts.map +1 -0
  31. package/{src/schemas/campaigns.ts → dist/schemas/campaigns.js} +20 -32
  32. package/dist/schemas/campaigns.js.map +1 -0
  33. package/dist/schemas/contactLog.d.ts +121 -0
  34. package/dist/schemas/contactLog.d.ts.map +1 -0
  35. package/dist/schemas/contactLog.js +83 -0
  36. package/dist/schemas/contactLog.js.map +1 -0
  37. package/dist/schemas/dispatch.d.ts +51 -0
  38. package/dist/schemas/dispatch.d.ts.map +1 -0
  39. package/{src/schemas/dispatch.ts → dist/schemas/dispatch.js} +55 -61
  40. package/dist/schemas/dispatch.js.map +1 -0
  41. package/dist/schemas/email.d.ts +38 -0
  42. package/dist/schemas/email.d.ts.map +1 -0
  43. package/dist/schemas/email.js +34 -0
  44. package/dist/schemas/email.js.map +1 -0
  45. package/dist/schemas/index.d.ts +16 -0
  46. package/dist/schemas/index.d.ts.map +1 -0
  47. package/{src/schemas/index.ts → dist/schemas/index.js} +1 -0
  48. package/dist/schemas/index.js.map +1 -0
  49. package/dist/schemas/insight.d.ts +25 -0
  50. package/dist/schemas/insight.d.ts.map +1 -0
  51. package/{src/schemas/insight.ts → dist/schemas/insight.js} +6 -9
  52. package/dist/schemas/insight.js.map +1 -0
  53. package/dist/schemas/portfolios.d.ts +65 -0
  54. package/dist/schemas/portfolios.d.ts.map +1 -0
  55. package/dist/schemas/portfolios.js +40 -0
  56. package/dist/schemas/portfolios.js.map +1 -0
  57. package/dist/schemas/userSettings.d.ts +25 -0
  58. package/dist/schemas/userSettings.d.ts.map +1 -0
  59. package/{src/schemas/userSettings.ts → dist/schemas/userSettings.js} +4 -9
  60. package/dist/schemas/userSettings.js.map +1 -0
  61. package/dist/schemas/users.d.ts +7 -0
  62. package/dist/schemas/users.d.ts.map +1 -0
  63. package/dist/schemas/users.js +6 -0
  64. package/dist/schemas/users.js.map +1 -0
  65. package/dist/schemas/voiceEvent.d.ts +37 -0
  66. package/dist/schemas/voiceEvent.d.ts.map +1 -0
  67. package/dist/schemas/voiceEvent.js +35 -0
  68. package/dist/schemas/voiceEvent.js.map +1 -0
  69. package/dist/schemas/whatsApp.d.ts +76 -0
  70. package/dist/schemas/whatsApp.d.ts.map +1 -0
  71. package/dist/schemas/whatsApp.js +85 -0
  72. package/dist/schemas/whatsApp.js.map +1 -0
  73. package/dist/schemas/workspaceSettings.d.ts +27 -0
  74. package/dist/schemas/workspaceSettings.d.ts.map +1 -0
  75. package/{src/schemas/workspaceSettings.ts → dist/schemas/workspaceSettings.js} +6 -11
  76. package/dist/schemas/workspaceSettings.js.map +1 -0
  77. package/dist/schemas/workspaces.d.ts +50 -0
  78. package/dist/schemas/workspaces.d.ts.map +1 -0
  79. package/dist/schemas/workspaces.js +36 -0
  80. package/dist/schemas/workspaces.js.map +1 -0
  81. package/dist/types/agentTemplates.d.ts +119 -0
  82. package/dist/types/agentTemplates.d.ts.map +1 -0
  83. package/dist/types/agentTemplates.js +2 -0
  84. package/dist/types/agentTemplates.js.map +1 -0
  85. package/dist/types/campaigns.d.ts +257 -0
  86. package/dist/types/campaigns.d.ts.map +1 -0
  87. package/dist/types/campaigns.js +2 -0
  88. package/dist/types/campaigns.js.map +1 -0
  89. package/dist/types/dispatch.d.ts +168 -0
  90. package/dist/types/dispatch.d.ts.map +1 -0
  91. package/dist/types/dispatch.js +8 -0
  92. package/dist/types/dispatch.js.map +1 -0
  93. package/dist/types/email.d.ts +65 -0
  94. package/dist/types/email.d.ts.map +1 -0
  95. package/dist/types/email.js +7 -0
  96. package/dist/types/email.js.map +1 -0
  97. package/{src/types/engine.ts → dist/types/engine.d.ts} +23 -46
  98. package/dist/types/engine.d.ts.map +1 -0
  99. package/dist/types/engine.js +10 -0
  100. package/dist/types/engine.js.map +1 -0
  101. package/dist/types/index.d.ts +12 -0
  102. package/dist/types/index.d.ts.map +1 -0
  103. package/{src/types/index.ts → dist/types/index.js} +1 -0
  104. package/dist/types/index.js.map +1 -0
  105. package/{src/types/insight.ts → dist/types/insight.d.ts} +10 -8
  106. package/dist/types/insight.d.ts.map +1 -0
  107. package/dist/types/insight.js +2 -0
  108. package/dist/types/insight.js.map +1 -0
  109. package/dist/types/portfolios.d.ts +153 -0
  110. package/dist/types/portfolios.d.ts.map +1 -0
  111. package/dist/types/portfolios.js +2 -0
  112. package/dist/types/portfolios.js.map +1 -0
  113. package/dist/types/userSettings.d.ts +25 -0
  114. package/dist/types/userSettings.d.ts.map +1 -0
  115. package/dist/types/userSettings.js +2 -0
  116. package/dist/types/userSettings.js.map +1 -0
  117. package/dist/types/voiceApplication.d.ts +32 -0
  118. package/dist/types/voiceApplication.d.ts.map +1 -0
  119. package/dist/types/voiceApplication.js +7 -0
  120. package/dist/types/voiceApplication.js.map +1 -0
  121. package/dist/types/whatsApp.d.ts +97 -0
  122. package/dist/types/whatsApp.d.ts.map +1 -0
  123. package/dist/types/whatsApp.js +2 -0
  124. package/dist/types/whatsApp.js.map +1 -0
  125. package/dist/types/workspaceSettings.d.ts +26 -0
  126. package/dist/types/workspaceSettings.d.ts.map +1 -0
  127. package/dist/types/workspaceSettings.js +2 -0
  128. package/dist/types/workspaceSettings.js.map +1 -0
  129. package/dist/utils/index.d.ts +4 -0
  130. package/dist/utils/index.d.ts.map +1 -0
  131. package/dist/utils/index.js +4 -0
  132. package/dist/utils/index.js.map +1 -0
  133. package/{src/utils/outreach.ts → dist/utils/outreach.d.ts} +7 -32
  134. package/dist/utils/outreach.d.ts.map +1 -0
  135. package/dist/utils/outreach.js +49 -0
  136. package/dist/utils/outreach.js.map +1 -0
  137. package/dist/utils/time.d.ts +25 -0
  138. package/dist/utils/time.d.ts.map +1 -0
  139. package/dist/utils/time.js +52 -0
  140. package/dist/utils/time.js.map +1 -0
  141. package/dist/utils/withErrorHandlingAndValidation.d.ts +18 -0
  142. package/dist/utils/withErrorHandlingAndValidation.d.ts.map +1 -0
  143. package/{src/utils/withErrorHandlingAndValidation.ts → dist/utils/withErrorHandlingAndValidation.js} +9 -15
  144. package/dist/utils/withErrorHandlingAndValidation.js.map +1 -0
  145. package/package.json +4 -1
  146. package/CHANGELOG.md +0 -115
  147. package/src/config.test.ts +0 -19
  148. package/src/errors/ValidationError.test.ts +0 -38
  149. package/src/errors/ValidationError.ts +0 -68
  150. package/src/schemas/agentTemplates.ts +0 -100
  151. package/src/schemas/apiKeys.test.ts +0 -38
  152. package/src/schemas/auth.ts +0 -76
  153. package/src/schemas/contactLog.ts +0 -96
  154. package/src/schemas/email.ts +0 -37
  155. package/src/schemas/portfolios.ts +0 -49
  156. package/src/schemas/users.ts +0 -7
  157. package/src/schemas/voiceEvent.ts +0 -45
  158. package/src/schemas/whatsApp.ts +0 -101
  159. package/src/schemas/workspaces.ts +0 -53
  160. package/src/types/agentTemplates.ts +0 -104
  161. package/src/types/campaigns.ts +0 -210
  162. package/src/types/dispatch.ts +0 -160
  163. package/src/types/email.ts +0 -66
  164. package/src/types/portfolios.ts +0 -128
  165. package/src/types/userSettings.ts +0 -21
  166. package/src/types/voiceApplication.ts +0 -29
  167. package/src/types/whatsApp.ts +0 -82
  168. package/src/types/workspaceSettings.ts +0 -22
  169. package/src/utils/index.ts +0 -14
  170. package/src/utils/outreach.test.ts +0 -83
  171. package/src/utils/time.ts +0 -66
  172. package/src/utils/withErrorHandlingAndValidation.test.ts +0 -33
  173. package/tsconfig.json +0 -9
@@ -1,18 +1,12 @@
1
- import Handlebars from "handlebars";
2
1
  import type { PortfolioAccountRecord } from "../types/portfolios.js";
3
2
  import type { NumberSelector } from "../types/dispatch.js";
4
-
5
3
  /**
6
4
  * Renders a Handlebars template against a context. Bodies are plain text (voice
7
5
  * script / SMS), never HTML, so escaping is disabled. A missing `{{field}}`
8
6
  * renders as empty rather than throwing, so a sparse account never aborts a
9
7
  * dispatch mid-flight.
10
8
  */
11
- export function renderTemplate(template: string, context: Record<string, unknown>): string {
12
- const compiled = Handlebars.compile(template, { noEscape: true });
13
- return compiled(context);
14
- }
15
-
9
+ export declare function renderTemplate(template: string, context: Record<string, unknown>): string;
16
10
  /**
17
11
  * Extracts the distinct simple placeholder names from a template, in first-seen order
18
12
  * (e.g. `"Hola {{firstName}}, saldo {{outstandingBalance}}"` → `["firstName",
@@ -20,16 +14,7 @@ export function renderTemplate(template: string, context: Record<string, unknown
20
14
  * turn a WhatsApp template body into Meta **named** parameters: each token becomes a
21
15
  * `{ parameter_name, text }` pair rendered against the customer context.
22
16
  */
23
- export function extractTemplateTokens(template: string): string[] {
24
- const tokens: string[] = [];
25
- const re = /\{\{\s*([A-Za-z_][\w.]*)\s*\}\}/g;
26
- let match: RegExpExecArray | null;
27
- while ((match = re.exec(template)) !== null) {
28
- if (!tokens.includes(match[1])) tokens.push(match[1]);
29
- }
30
- return tokens;
31
- }
32
-
17
+ export declare function extractTemplateTokens(template: string): string[];
33
18
  /**
34
19
  * Builds the render context exposed to outreach templates: every account field
35
20
  * plus derived `firstName` (first token of `fullName`), `currency` (the
@@ -39,19 +24,9 @@ export function extractTemplateTokens(template: string): string[] {
39
24
  * `{{#if isDue}}su pago está vencido{{else}}gracias por estar al día{{/if}}`.
40
25
  * These are the variables documented in the agent console.
41
26
  */
42
- export function buildOutreachContext(
43
- account: PortfolioAccountRecord,
44
- opts: { currency: string }
45
- ): Record<string, unknown> {
46
- const firstName = account.fullName.trim().split(/\s+/)[0] ?? "";
47
- return {
48
- ...account,
49
- firstName,
50
- currency: opts.currency,
51
- isDue: account.daysPastDue > 0
52
- };
53
- }
54
-
27
+ export declare function buildOutreachContext(account: PortfolioAccountRecord, opts: {
28
+ currency: string;
29
+ }): Record<string, unknown>;
55
30
  /** Default number selector: a uniform random pick from the pool. */
56
- export const pickRandomNumber: NumberSelector = (numbers) =>
57
- numbers[Math.floor(Math.random() * numbers.length)];
31
+ export declare const pickRandomNumber: NumberSelector;
32
+ //# sourceMappingURL=outreach.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outreach.d.ts","sourceRoot":"","sources":["../../src/utils/outreach.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAGzF;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAQhE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,sBAAsB,EAC/B,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GACzB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQzB;AAED,oEAAoE;AACpE,eAAO,MAAM,gBAAgB,EAAE,cACsB,CAAC"}
@@ -0,0 +1,49 @@
1
+ import Handlebars from "handlebars";
2
+ /**
3
+ * Renders a Handlebars template against a context. Bodies are plain text (voice
4
+ * script / SMS), never HTML, so escaping is disabled. A missing `{{field}}`
5
+ * renders as empty rather than throwing, so a sparse account never aborts a
6
+ * dispatch mid-flight.
7
+ */
8
+ export function renderTemplate(template, context) {
9
+ const compiled = Handlebars.compile(template, { noEscape: true });
10
+ return compiled(context);
11
+ }
12
+ /**
13
+ * Extracts the distinct simple placeholder names from a template, in first-seen order
14
+ * (e.g. `"Hola {{firstName}}, saldo {{outstandingBalance}}"` → `["firstName",
15
+ * "outstandingBalance"]`). Block helpers like `{{#if}}`/`{{/if}}` are ignored. Used to
16
+ * turn a WhatsApp template body into Meta **named** parameters: each token becomes a
17
+ * `{ parameter_name, text }` pair rendered against the customer context.
18
+ */
19
+ export function extractTemplateTokens(template) {
20
+ const tokens = [];
21
+ const re = /\{\{\s*([A-Za-z_][\w.]*)\s*\}\}/g;
22
+ let match;
23
+ while ((match = re.exec(template)) !== null) {
24
+ if (!tokens.includes(match[1]))
25
+ tokens.push(match[1]);
26
+ }
27
+ return tokens;
28
+ }
29
+ /**
30
+ * Builds the render context exposed to outreach templates: every account field
31
+ * plus derived `firstName` (first token of `fullName`), `currency` (the
32
+ * workspace's currency from WorkspaceSettings), and `isDue` (whether the account
33
+ * is past due, i.e. `daysPastDue > 0`). `isDue` is a boolean so templates can
34
+ * branch on it with Handlebars conditionals, e.g.
35
+ * `{{#if isDue}}su pago está vencido{{else}}gracias por estar al día{{/if}}`.
36
+ * These are the variables documented in the agent console.
37
+ */
38
+ export function buildOutreachContext(account, opts) {
39
+ const firstName = account.fullName.trim().split(/\s+/)[0] ?? "";
40
+ return {
41
+ ...account,
42
+ firstName,
43
+ currency: opts.currency,
44
+ isDue: account.daysPastDue > 0
45
+ };
46
+ }
47
+ /** Default number selector: a uniform random pick from the pool. */
48
+ export const pickRandomNumber = (numbers) => numbers[Math.floor(Math.random() * numbers.length)];
49
+ //# sourceMappingURL=outreach.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outreach.js","sourceRoot":"","sources":["../../src/utils/outreach.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,YAAY,CAAC;AAIpC;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,OAAgC;IAC/E,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAClE,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAgB;IACpD,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,EAAE,GAAG,kCAAkC,CAAC;IAC9C,IAAI,KAA6B,CAAC;IAClC,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC5C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAA+B,EAC/B,IAA0B;IAE1B,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAChE,OAAO;QACL,GAAG,OAAO;QACV,SAAS;QACT,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,KAAK,EAAE,OAAO,CAAC,WAAW,GAAG,CAAC;KAC/B,CAAC;AACJ,CAAC;AAED,oEAAoE;AACpE,MAAM,CAAC,MAAM,gBAAgB,GAAmB,CAAC,OAAO,EAAE,EAAE,CAC1D,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Timezone-aware wall-clock helpers. The campaigns engine evaluates schedule
3
+ * windows and daily caps in the deployment timezone (from `qcobro.json`), not UTC.
4
+ * All functions take an IANA timezone string and use `Intl` so DST is handled.
5
+ */
6
+ interface LocalParts {
7
+ /** Local calendar date as `YYYY-MM-DD`. */
8
+ date: string;
9
+ /** ISO weekday: 1 = Monday … 7 = Sunday. */
10
+ weekday: number;
11
+ /** Local time as `HH:MM` (24h, zero-padded). */
12
+ time: string;
13
+ }
14
+ /** Decompose an instant into local date/weekday/time parts for the given timezone. */
15
+ export declare function localParts(date: Date, timeZone: string): LocalParts;
16
+ /** Local calendar date (`YYYY-MM-DD`) for an instant in the given timezone. */
17
+ export declare function localDateString(date: Date, timeZone: string): string;
18
+ /** True when two instants fall on the same local calendar day in the timezone. */
19
+ export declare function isSameLocalDay(a: Date, b: Date, timeZone: string): boolean;
20
+ /** ISO weekday (1=Mon … 7=Sun) for an instant in the timezone. */
21
+ export declare function localWeekdayISO(date: Date, timeZone: string): number;
22
+ /** Local `HH:MM` (24h) for an instant in the timezone. */
23
+ export declare function localTimeHHMM(date: Date, timeZone: string): string;
24
+ export {};
25
+ //# sourceMappingURL=time.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time.d.ts","sourceRoot":"","sources":["../../src/utils/time.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,UAAU,UAAU;IAClB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;CACd;AAYD,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,CAmBnE;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1E;AAED,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAElE"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Timezone-aware wall-clock helpers. The campaigns engine evaluates schedule
3
+ * windows and daily caps in the deployment timezone (from `qcobro.json`), not UTC.
4
+ * All functions take an IANA timezone string and use `Intl` so DST is handled.
5
+ */
6
+ const WEEKDAY_TO_ISO = {
7
+ Mon: 1,
8
+ Tue: 2,
9
+ Wed: 3,
10
+ Thu: 4,
11
+ Fri: 5,
12
+ Sat: 6,
13
+ Sun: 7
14
+ };
15
+ /** Decompose an instant into local date/weekday/time parts for the given timezone. */
16
+ export function localParts(date, timeZone) {
17
+ const fmt = new Intl.DateTimeFormat("en-US", {
18
+ timeZone,
19
+ weekday: "short",
20
+ year: "numeric",
21
+ month: "2-digit",
22
+ day: "2-digit",
23
+ hour: "2-digit",
24
+ minute: "2-digit",
25
+ hour12: false
26
+ });
27
+ const parts = Object.fromEntries(fmt.formatToParts(date).map((p) => [p.type, p.value]));
28
+ // `hour` can come back as "24" at midnight under hour12:false; normalize to "00".
29
+ const hour = parts.hour === "24" ? "00" : parts.hour;
30
+ return {
31
+ date: `${parts.year}-${parts.month}-${parts.day}`,
32
+ weekday: WEEKDAY_TO_ISO[parts.weekday] ?? 0,
33
+ time: `${hour}:${parts.minute}`
34
+ };
35
+ }
36
+ /** Local calendar date (`YYYY-MM-DD`) for an instant in the given timezone. */
37
+ export function localDateString(date, timeZone) {
38
+ return localParts(date, timeZone).date;
39
+ }
40
+ /** True when two instants fall on the same local calendar day in the timezone. */
41
+ export function isSameLocalDay(a, b, timeZone) {
42
+ return localDateString(a, timeZone) === localDateString(b, timeZone);
43
+ }
44
+ /** ISO weekday (1=Mon … 7=Sun) for an instant in the timezone. */
45
+ export function localWeekdayISO(date, timeZone) {
46
+ return localParts(date, timeZone).weekday;
47
+ }
48
+ /** Local `HH:MM` (24h) for an instant in the timezone. */
49
+ export function localTimeHHMM(date, timeZone) {
50
+ return localParts(date, timeZone).time;
51
+ }
52
+ //# sourceMappingURL=time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time.js","sourceRoot":"","sources":["../../src/utils/time.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH,MAAM,cAAc,GAA2B;IAC7C,GAAG,EAAE,CAAC;IACN,GAAG,EAAE,CAAC;IACN,GAAG,EAAE,CAAC;IACN,GAAG,EAAE,CAAC;IACN,GAAG,EAAE,CAAC;IACN,GAAG,EAAE,CAAC;IACN,GAAG,EAAE,CAAC;CACP,CAAC;AAEF,sFAAsF;AACtF,MAAM,UAAU,UAAU,CAAC,IAAU,EAAE,QAAgB;IACrD,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;QAC3C,QAAQ;QACR,OAAO,EAAE,OAAO;QAChB,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,SAAS;QAChB,GAAG,EAAE,SAAS;QACd,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,SAAS;QACjB,MAAM,EAAE,KAAK;KACd,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACxF,kFAAkF;IAClF,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;IACrD,OAAO;QACL,IAAI,EAAE,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,GAAG,EAAE;QACjD,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAC3C,IAAI,EAAE,GAAG,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE;KAChC,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,eAAe,CAAC,IAAU,EAAE,QAAgB;IAC1D,OAAO,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC;AACzC,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,cAAc,CAAC,CAAO,EAAE,CAAO,EAAE,QAAgB;IAC/D,OAAO,eAAe,CAAC,CAAC,EAAE,QAAQ,CAAC,KAAK,eAAe,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AACvE,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,eAAe,CAAC,IAAU,EAAE,QAAgB;IAC1D,OAAO,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC;AAC5C,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,aAAa,CAAC,IAAU,EAAE,QAAgB;IACxD,OAAO,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC;AACzC,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Wraps an async function with Zod schema validation.
4
+ *
5
+ * The returned function validates its input against `schema` before calling
6
+ * `fn`. Valid input is passed through as parsed, typed data; invalid input
7
+ * throws a {@link ValidationError} and `fn` is never invoked.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const createCustomer = withErrorHandlingAndValidation(
12
+ * async (params: CreateCustomerInput) => client.customer.create({ data: params }),
13
+ * createCustomerSchema
14
+ * );
15
+ * ```
16
+ */
17
+ export declare function withErrorHandlingAndValidation<TSchema extends z.ZodType, TResult>(fn: (params: z.infer<TSchema>) => Promise<TResult>, schema: TSchema): (params: unknown) => Promise<TResult>;
18
+ //# sourceMappingURL=withErrorHandlingAndValidation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withErrorHandlingAndValidation.d.ts","sourceRoot":"","sources":["../../src/utils/withErrorHandlingAndValidation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,SAAS,CAAC,CAAC,OAAO,EAAE,OAAO,EAC/E,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,EAClD,MAAM,EAAE,OAAO,GACd,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAUvC"}
@@ -1,6 +1,4 @@
1
- import { z } from "zod";
2
1
  import { ValidationError } from "../errors/ValidationError.js";
3
-
4
2
  /**
5
3
  * Wraps an async function with Zod schema validation.
6
4
  *
@@ -16,17 +14,13 @@ import { ValidationError } from "../errors/ValidationError.js";
16
14
  * );
17
15
  * ```
18
16
  */
19
- export function withErrorHandlingAndValidation<TSchema extends z.ZodType, TResult>(
20
- fn: (params: z.infer<TSchema>) => Promise<TResult>,
21
- schema: TSchema
22
- ): (params: unknown) => Promise<TResult> {
23
- return async (params: unknown) => {
24
- const result = schema.safeParse(params);
25
-
26
- if (!result.success) {
27
- throw new ValidationError(result.error);
28
- }
29
-
30
- return fn(result.data);
31
- };
17
+ export function withErrorHandlingAndValidation(fn, schema) {
18
+ return async (params) => {
19
+ const result = schema.safeParse(params);
20
+ if (!result.success) {
21
+ throw new ValidationError(result.error);
22
+ }
23
+ return fn(result.data);
24
+ };
32
25
  }
26
+ //# sourceMappingURL=withErrorHandlingAndValidation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withErrorHandlingAndValidation.js","sourceRoot":"","sources":["../../src/utils/withErrorHandlingAndValidation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAE/D;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,8BAA8B,CAC5C,EAAkD,EAClD,MAAe;IAEf,OAAO,KAAK,EAAE,MAAe,EAAE,EAAE;QAC/B,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAExC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,IAAI,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC1C,CAAC;QAED,OAAO,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@qcobro/common",
3
- "version": "1.11.3",
3
+ "version": "1.11.4",
4
4
  "type": "module",
5
+ "files": [
6
+ "dist"
7
+ ],
5
8
  "publishConfig": {
6
9
  "access": "public"
7
10
  },
package/CHANGELOG.md DELETED
@@ -1,115 +0,0 @@
1
- # Change Log
2
-
3
- All notable changes to this project will be documented in this file.
4
- See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
-
6
- # [1.11.0](https://github.com/fonoster/qcobro/compare/v1.10.0...v1.11.0) (2026-06-30)
7
-
8
- ### Features
9
-
10
- - **whatsapp:** inbound autopilot — reply, opt-out, payment promise (§7.3, §7.4, §9.4) ([ef37a91](https://github.com/fonoster/qcobro/commit/ef37a9109909cb9e86ff3a787b5af06ed199fd78))
11
- - **whatsapp:** inbound event processing — opt-out suppression + quality-rating (§7.2, §9.3) ([974a9e3](https://github.com/fonoster/qcobro/commit/974a9e35d549e1baf6386ac6bcec40297c8ae703))
12
- - **whatsapp:** wire engine tick, webhook handshake, and unit tests (§6.2, §7.1, §9.1–9.2) ([a46bb1f](https://github.com/fonoster/qcobro/commit/a46bb1fd0f786e5856281da48d07f2f3249081e7))
13
-
14
- # [1.10.0](https://github.com/fonoster/qcobro/compare/v1.9.0...v1.10.0) (2026-06-30)
15
-
16
- ### Features
17
-
18
- - **whatsapp:** add WhatsApp channel server foundation (§2–§6.1) ([51211f3](https://github.com/fonoster/qcobro/commit/51211f3b8af1cd7fb45e0b2ded40262915606548)), closes [#5](https://github.com/fonoster/qcobro/issues/5)
19
-
20
- # [1.9.0](https://github.com/fonoster/qcobro/compare/v1.8.0...v1.9.0) (2026-06-29)
21
-
22
- ### Bug Fixes
23
-
24
- - **api:** stop resending Voz IA system prompt as call metadata ([0d66d18](https://github.com/fonoster/qcobro/commit/0d66d18b34d0f353f02d96599d8784f0f372a547))
25
-
26
- ### Features
27
-
28
- - **common:** add isDue outreach variable and document template variables ([ed7a511](https://github.com/fonoster/qcobro/commit/ed7a51161b758d2337055028f7adf52f22f7ae56))
29
-
30
- ## [1.5.4](https://github.com/fonoster/qcobro/compare/v1.5.3...v1.5.4) (2026-06-28)
31
-
32
- ### Bug Fixes
33
-
34
- - **e2e:** supply fonoster voices in CI config; enable campaigns + console specs ([2ff90ab](https://github.com/fonoster/qcobro/commit/2ff90aba4da71f7e6d902aacff753ea5a9411950))
35
-
36
- # [1.5.0](https://github.com/fonoster/qcobro/compare/v1.4.1...v1.5.0) (2026-06-28)
37
-
38
- ### Features
39
-
40
- - **profile-language:** per-user language preference + i18n hygiene sweep ([dbddb9c](https://github.com/fonoster/qcobro/commit/dbddb9c7915103afc339ec9d695b27c97d7634ce))
41
-
42
- # [1.4.0](https://github.com/fonoster/qcobro/compare/v1.3.2...v1.4.0) (2026-06-28)
43
-
44
- ### Features
45
-
46
- - **workspace-settings:** collect currency + timezone at workspace creation ([682ae8f](https://github.com/fonoster/qcobro/commit/682ae8f5fcf2045564c0dd5ac0e8f38fa736ed4a))
47
-
48
- ## [1.3.2](https://github.com/fonoster/qcobro/compare/v1.3.1...v1.3.2) (2026-06-28)
49
-
50
- **Note:** Version bump only for package @qcobro/common
51
-
52
- ## [1.3.1](https://github.com/fonoster/qcobro/compare/v1.3.0...v1.3.1) (2026-06-28)
53
-
54
- ### Bug Fixes
55
-
56
- - **timezone:** contact-log REST uses workspace tz; default is a constant ([1b86419](https://github.com/fonoster/qcobro/commit/1b86419fc98722d0c1174ce9f1bdf19848b35dd5))
57
-
58
- # [1.3.0](https://github.com/fonoster/qcobro/compare/v1.2.3...v1.3.0) (2026-06-28)
59
-
60
- ### Features
61
-
62
- - **workspace-settings:** per-workspace currency + timezone (off Identity) ([c1516a3](https://github.com/fonoster/qcobro/commit/c1516a3d306e2b2a906d9ae476fb27f23887d5ae))
63
-
64
- # [1.2.0](https://github.com/fonoster/qcobro/compare/v1.1.4...v1.2.0) (2026-06-28)
65
-
66
- ### Features
67
-
68
- - **payment-promises:** outcomes + PaymentPromise worklist, agent-based outreach ([6c620f8](https://github.com/fonoster/qcobro/commit/6c620f8a80c65a7178b0716b825a7d4ebb4077f7))
69
-
70
- # 1.1.0 (2026-06-28)
71
-
72
- ### Bug Fixes
73
-
74
- - **channel-dispatch:** allow empty firstMessage for VOICE_AI; non-destructive engine test ([71e17a6](https://github.com/fonoster/qcobro/commit/71e17a6a32d8353e9d428e5763e937408df066df))
75
- - **channel-dispatch:** pre-recorded voice has no firstMessage either ([ebc8e90](https://github.com/fonoster/qcobro/commit/ebc8e9049b765388e8e0e7505dccef22d4dde875))
76
- - **email:** hydrate inbound body from received-emails api; strip quoted history ([ddcbdb1](https://github.com/fonoster/qcobro/commit/ddcbdb170953cb9428d92a0cc385d5b83eb43cfd))
77
- - **voice:** provision AUTOPILOT apps with required conversation settings ([c92a8aa](https://github.com/fonoster/qcobro/commit/c92a8aa52c30e2c8f3f78145dec17a99b72282cd))
78
- - **webapp:** drop unused fromName/fromEmail from EMAIL agent form; add Resend status badge ([625c3c8](https://github.com/fonoster/qcobro/commit/625c3c8300d275f9f1982353bdf87112a9f03fa2))
79
- - **workspaces:** wire invite acceptance to Identity HTTP bridge ([5b9fc40](https://github.com/fonoster/qcobro/commit/5b9fc40c57cd8d692d2711c1483bbd158e01aa54))
80
-
81
- ### Features
82
-
83
- - **agent-templates:** per-channel agents, voices-from-config, Fonoster Voz IA sync ([6a8065d](https://github.com/fonoster/qcobro/commit/6a8065d27f8954aa5c5faf7ab34553dccefda5fc))
84
- - **ai-insights:** transcript-based AI analysis + Voz IA wiring ([4ed7d2e](https://github.com/fonoster/qcobro/commit/4ed7d2e0faf2af9d8ff7966c687c346183b05184))
85
- - **api-keys:** workspace API key management ([30dd25d](https://github.com/fonoster/qcobro/commit/30dd25d52e1083afb66c7bd323b10d0ac193425a))
86
- - **api,webapp:** delete-workspace — ownerProcedure and WorkspaceSettings UI ([2542443](https://github.com/fonoster/qcobro/commit/2542443d6cdf4c9a6b2587e3380de9ea3e9f8263))
87
- - **api,webapp:** profile-management — profile router and Profile page ([5850ec6](https://github.com/fonoster/qcobro/commit/5850ec6e777a987c559f6ab94a15725bc998820f))
88
- - **api:** add contact-verification and OAuth auth procedures ([b6b70c3](https://github.com/fonoster/qcobro/commit/b6b70c35dd8063ccbdc6e429ded21aa96154928d))
89
- - **api:** complete auth-and-workspaces change — password reset, resend invite, accept-invite UI ([09c557b](https://github.com/fonoster/qcobro/commit/09c557b00dfee7de725e45cfc5f6e5f61e91f44d))
90
- - **apiserver:** add auth router (signup, login, refresh, logout) ([9fd50e0](https://github.com/fonoster/qcobro/commit/9fd50e0a32c92ca6bcdf0c053f7050f16921b42b))
91
- - **apiserver:** add workspace create/list/get (Group 5 core) ([99de39c](https://github.com/fonoster/qcobro/commit/99de39c29c692fd2760053c3bb7f196e11c1a05b))
92
- - **campaigns-engine:** propose change + config/contracts (group 1) ([6ffd70d](https://github.com/fonoster/qcobro/commit/6ffd70df34cf0a3f33060243eadb1b7460e440eb))
93
- - **campaigns-engine:** wiring + cleanup (groups 7/9) ([828056d](https://github.com/fonoster/qcobro/commit/828056d091f419bafc5d675a6f0bd92e67bf9eb9))
94
- - **campaigns:** campaigns-core — lifecycle, days-of-week, edit modal, specs synced ([d1e75cd](https://github.com/fonoster/qcobro/commit/d1e75cd9e065a1556811ee0abf94c4e2ab569e20))
95
- - **campaigns:** checkpoint campaigns-core WIP before refinement ([2b3e339](https://github.com/fonoster/qcobro/commit/2b3e339913ab4de37152ed04e5aaf0d90fb247c4))
96
- - **channel-dispatch:** outreach trigger layer (Fonoster voice + Twilio SMS) ([56a4b9e](https://github.com/fonoster/qcobro/commit/56a4b9e4c7c9267f0bfbad42b4b37fff74b6b8fb))
97
- - **common:** add validated-function utilities and conventions guide ([a60bab9](https://github.com/fonoster/qcobro/commit/a60bab99affd9290602512e5921632c4a1f9f70f))
98
- - **console:** config-driven announcement banner; flag unimplemented data ([01d8977](https://github.com/fonoster/qcobro/commit/01d89775fa547766521c5be5c15ba429bf5a655c))
99
- - **console:** refinement + cleanup pass ([78dc3e5](https://github.com/fonoster/qcobro/commit/78dc3e58f2a4eaede4bf10a9d2a551b3c426d9ee))
100
- - **email-channel:** inbound autopilot — webhook, decision loop, reply cap ([67b6a85](https://github.com/fonoster/qcobro/commit/67b6a8515e75f8ca178293755bffa540a2a3d139))
101
- - **email-channel:** outbound email + engine integration (Resend) ([391d3d0](https://github.com/fonoster/qcobro/commit/391d3d0caf3440d4f41568d0c48ec37c7ec76d36))
102
- - **email-channel:** spec + contracts for bidirectional email (Resend autopilot) ([6c2461c](https://github.com/fonoster/qcobro/commit/6c2461ced535cc1b1eb4a7f9eb2a3dcd989dbbe5))
103
- - **email:** bidirectional email channel end-to-end ([b49a442](https://github.com/fonoster/qcobro/commit/b49a442104841023f3507a6c33cf85dfe689bf12))
104
- - **gestiones:** add voz IA channel webhook and rich detail panel ([824671f](https://github.com/fonoster/qcobro/commit/824671f0da22dcc9ec16a610618925f96e28d2c8))
105
- - identity now from the published fonoster identity mod ([9a6eaea](https://github.com/fonoster/qcobro/commit/9a6eaeaed20f51ea7a4846fe116735aeecdcc6e5))
106
- - **manual-outreach:** carteras reach-out modal + campaign-derived dispatch ([367db2d](https://github.com/fonoster/qcobro/commit/367db2d370c893317042b76239acb7d39c3e69f7))
107
- - **portfolios:** portfolio management, status enums, currency, and row actions ([6b6bac9](https://github.com/fonoster/qcobro/commit/6b6bac914d85f5140da0abfaa84bd682686364e3))
108
- - scaffold Qcobro app monorepo ([d5a7507](https://github.com/fonoster/qcobro/commit/d5a7507016d27cc2f76ece0c6aaeff33186d8da0))
109
- - scaffold spec-driven monorepo foundation ([1a17d89](https://github.com/fonoster/qcobro/commit/1a17d89dffe686032caafe1c09be50053286e48b))
110
- - **sdk:** add @qcobro/sdk with portfolios, API-key auth, and auto-refresh ([324405e](https://github.com/fonoster/qcobro/commit/324405e9922bf70ccd57088122098c6c2d8de2e7))
111
- - **voice:** embedded Fonoster VoiceServer for pre-recorded (external) agents ([00e581e](https://github.com/fonoster/qcobro/commit/00e581e78c503509ad7af4cade0b29b226a78a60))
112
- - **voice:** make pre-recorded audio permanent and spec the events-hook ([8b03fdb](https://github.com/fonoster/qcobro/commit/8b03fdbbbd84cc4233578ae94e37a9a364171584))
113
- - **voice:** pre-recorded via shared external app ref + Say playback ([e156292](https://github.com/fonoster/qcobro/commit/e15629223914a1141025a1804aa184222c3f244e))
114
- - **webapp:** implement Pencil UI — login brand panel, workspace picker, sidebar redesign ([e34ef12](https://github.com/fonoster/qcobro/commit/e34ef123b68941a007304316f3c0135f50a69cbd))
115
- - **workspaces:** rename + console navigation (workspace-management) ([46acf86](https://github.com/fonoster/qcobro/commit/46acf86d32a09d3cf4474818f4950e35776bef01))
@@ -1,19 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { ttsProductRefForVoice, type VoiceCatalogEntry } from "./config.js";
4
-
5
- const voices: VoiceCatalogEntry[] = [
6
- { id: "v-eleven", name: "Sofía", language: "es", gender: "female", provider: "elevenlabs" },
7
- { id: "v-google", name: "Andrés", language: "es", gender: "male", provider: "google" }
8
- ];
9
-
10
- describe("ttsProductRefForVoice", () => {
11
- it("derives the TTS product ref from the voice's provider", () => {
12
- assert.equal(ttsProductRefForVoice("v-eleven", voices), "tts.elevenlabs");
13
- assert.equal(ttsProductRefForVoice("v-google", voices), "tts.google");
14
- });
15
-
16
- it("falls back to tts.elevenlabs for an unknown voice", () => {
17
- assert.equal(ttsProductRefForVoice("missing", voices), "tts.elevenlabs");
18
- });
19
- });
@@ -1,38 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { z } from "zod";
4
- import { ValidationError } from "./ValidationError.js";
5
-
6
- const schema = z.object({ name: z.string().min(1), age: z.number() });
7
-
8
- function zodErrorFor(input: unknown): z.ZodError {
9
- const result = schema.safeParse(input);
10
- assert.equal(result.success, false);
11
- return (result as { success: false; error: z.ZodError }).error;
12
- }
13
-
14
- describe("ValidationError", () => {
15
- it("exposes a stable code and field-level errors", () => {
16
- const error = new ValidationError(zodErrorFor({ name: "", age: "nope" }));
17
-
18
- assert.equal(error.code, "VALIDATION_ERROR");
19
- assert.equal(error.name, "ValidationError");
20
- assert.ok(error instanceof Error);
21
-
22
- const fields = error.fieldErrors.map((f) => f.field);
23
- assert.ok(fields.includes("name"));
24
- assert.ok(fields.includes("age"));
25
- for (const fieldError of error.fieldErrors) {
26
- assert.equal(typeof fieldError.message, "string");
27
- assert.equal(typeof fieldError.code, "string");
28
- }
29
- });
30
-
31
- it("serializes to a JSON shape for API responses", () => {
32
- const error = new ValidationError(zodErrorFor({ name: "", age: 1 }));
33
- const json = error.toJSON();
34
-
35
- assert.deepEqual(Object.keys(json).sort(), ["code", "fieldErrors", "message"]);
36
- assert.equal(json.code, "VALIDATION_ERROR");
37
- });
38
- });
@@ -1,68 +0,0 @@
1
- import type { z } from "zod";
2
-
3
- export interface FieldError {
4
- field: string;
5
- message: string;
6
- code: string;
7
- }
8
-
9
- /**
10
- * Wraps a Zod validation error with structured, field-level details suitable
11
- * for API responses.
12
- */
13
- export class ValidationError extends Error {
14
- public readonly code = "VALIDATION_ERROR";
15
- public readonly fieldErrors: FieldError[];
16
- public readonly zodError: z.ZodError;
17
-
18
- constructor(zodError: z.ZodError) {
19
- const fieldErrors = ValidationError.extractFieldErrors(zodError);
20
- const message = ValidationError.formatMessage(fieldErrors);
21
-
22
- super(message);
23
- this.name = "ValidationError";
24
- this.zodError = zodError;
25
- this.fieldErrors = fieldErrors;
26
-
27
- // Maintains proper stack trace for where the error was thrown (V8 engines).
28
- if (Error.captureStackTrace) {
29
- Error.captureStackTrace(this, ValidationError);
30
- }
31
- }
32
-
33
- private static extractFieldErrors(zodError: z.ZodError): FieldError[] {
34
- return zodError.issues.map((issue) => ({
35
- field: issue.path.join(".") || "root",
36
- message: issue.message,
37
- code: issue.code
38
- }));
39
- }
40
-
41
- private static formatMessage(fieldErrors: FieldError[]): string {
42
- if (fieldErrors.length === 0) {
43
- return "Validation failed";
44
- }
45
-
46
- if (fieldErrors.length === 1) {
47
- const { field, message } = fieldErrors[0];
48
- return field === "root" ? message : `${field}: ${message}`;
49
- }
50
-
51
- const details = fieldErrors
52
- .map(({ field, message }) => (field === "root" ? message : `${field}: ${message}`))
53
- .join("; ");
54
-
55
- return `Validation failed: ${details}`;
56
- }
57
-
58
- /**
59
- * Returns a JSON-serializable representation for API responses.
60
- */
61
- toJSON() {
62
- return {
63
- code: this.code,
64
- message: this.message,
65
- fieldErrors: this.fieldErrors
66
- };
67
- }
68
- }
@@ -1,100 +0,0 @@
1
- import { z } from "zod";
2
-
3
- export const agentTypeSchema = z.enum([
4
- "SMS",
5
- "VOICE_PRERECORDED",
6
- "VOICE_AI",
7
- "EMAIL",
8
- "WHATSAPP"
9
- ]);
10
- export type AgentType = z.infer<typeof agentTypeSchema>;
11
-
12
- const baseFields = {
13
- name: z.string().min(1).max(120)
14
- };
15
-
16
- /**
17
- * Creating an agent template is a discriminated union on `type`: each channel
18
- * carries its own config fields, never mixed across types. `fonosterAppName` is
19
- * optional on voice types — the create function defaults it to the template name.
20
- */
21
- export const createAgentTemplateSchema = z.discriminatedUnion("type", [
22
- z.object({
23
- ...baseFields,
24
- type: z.literal("VOICE_AI"),
25
- voice: z.string().min(1),
26
- systemPrompt: z.string().min(1),
27
- // Optional: a VOICE_AI agent may rely on its system prompt with no scripted opening line.
28
- firstMessage: z.string().optional(),
29
- language: z.string().min(1),
30
- fonosterAppName: z.string().min(1).optional()
31
- }),
32
- z.object({
33
- ...baseFields,
34
- type: z.literal("VOICE_PRERECORDED"),
35
- voice: z.string().min(1),
36
- script: z.string().min(1),
37
- language: z.string().min(1),
38
- fonosterAppName: z.string().min(1).optional()
39
- }),
40
- z.object({
41
- ...baseFields,
42
- type: z.literal("SMS"),
43
- messageBody: z.string().min(1),
44
- senderId: z.string().min(1).optional()
45
- }),
46
- z.object({
47
- ...baseFields,
48
- type: z.literal("EMAIL"),
49
- subject: z.string().min(1),
50
- messageBody: z.string().min(1),
51
- /** Autopilot decision brain: governs reply/ignore/resolve/escalate on each inbound reply. */
52
- systemPrompt: z.string().min(1),
53
- /** Per-agent cap on autopilot replies per collection attempt; falls back to the
54
- * `resend.maxRepliesDefault` deployment default when omitted. */
55
- maxReplies: z.number().int().nonnegative().optional()
56
- }),
57
- z.object({
58
- ...baseFields,
59
- type: z.literal("WHATSAPP"),
60
- /** Meta template id the operator enters; QCobro resolves + previews the template from the WABA. */
61
- templateId: z.string().min(1),
62
- /** Resolved from `templateId` (read-only in the UI); the approved Meta template name to send. */
63
- templateName: z.string().min(1),
64
- /** Fetched template body (read-only preview); its `{{vars}}` are sent as named parameters. */
65
- messageBody: z.string().min(1),
66
- /** Smart-agent decision brain for replies after the customer responds (mirrors EMAIL). */
67
- systemPrompt: z.string().min(1),
68
- /** Per-agent cap on automated replies per gestión; falls back to the deployment default when omitted. */
69
- maxReplies: z.number().int().nonnegative().optional()
70
- })
71
- ]);
72
- export type CreateAgentTemplateInput = z.infer<typeof createAgentTemplateSchema>;
73
-
74
- /**
75
- * Updating an agent template: mutable base fields plus a loose `config` bag of
76
- * type-specific fields applied to the stored child table. `type` is immutable —
77
- * `.strict()` rejects any attempt to pass it (or other unknown keys).
78
- */
79
- export const updateAgentTemplateSchema = z
80
- .object({
81
- id: z.string().min(1),
82
- name: z.string().min(1).max(120).optional(),
83
- // `archived` toggles the template's archived state: true sets `archivedAt` to
84
- // now, false clears it (restore). Templates have no status concept.
85
- archived: z.boolean().optional(),
86
- config: z.record(z.string(), z.unknown()).optional()
87
- })
88
- .strict();
89
- export type UpdateAgentTemplateInput = z.infer<typeof updateAgentTemplateSchema>;
90
-
91
- export const deleteAgentTemplateSchema = z.object({
92
- id: z.string().min(1)
93
- });
94
- export type DeleteAgentTemplateInput = z.infer<typeof deleteAgentTemplateSchema>;
95
-
96
- /** Manually re-attempt the Fonoster sync for a voice template. */
97
- export const syncAgentTemplateSchema = z.object({
98
- id: z.string().min(1)
99
- });
100
- export type SyncAgentTemplateInput = z.infer<typeof syncAgentTemplateSchema>;