@qcobro/common 1.11.3
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/CHANGELOG.md +115 -0
- package/package.json +25 -0
- package/src/config.test.ts +19 -0
- package/src/config.ts +358 -0
- package/src/errors/ValidationError.test.ts +38 -0
- package/src/errors/ValidationError.ts +68 -0
- package/src/errors/index.ts +1 -0
- package/src/index.ts +21 -0
- package/src/schemas/agentTemplates.ts +100 -0
- package/src/schemas/apiKeys.test.ts +38 -0
- package/src/schemas/apiKeys.ts +28 -0
- package/src/schemas/auth.ts +76 -0
- package/src/schemas/campaigns.ts +88 -0
- package/src/schemas/contactLog.ts +96 -0
- package/src/schemas/dispatch.ts +115 -0
- package/src/schemas/email.ts +37 -0
- package/src/schemas/index.ts +15 -0
- package/src/schemas/insight.ts +20 -0
- package/src/schemas/portfolios.ts +49 -0
- package/src/schemas/userSettings.ts +18 -0
- package/src/schemas/users.ts +7 -0
- package/src/schemas/voiceEvent.ts +45 -0
- package/src/schemas/whatsApp.ts +101 -0
- package/src/schemas/workspaceSettings.ts +20 -0
- package/src/schemas/workspaces.ts +53 -0
- package/src/types/agentTemplates.ts +104 -0
- package/src/types/campaigns.ts +210 -0
- package/src/types/dispatch.ts +160 -0
- package/src/types/email.ts +66 -0
- package/src/types/engine.ts +73 -0
- package/src/types/index.ts +11 -0
- package/src/types/insight.ts +20 -0
- package/src/types/portfolios.ts +128 -0
- package/src/types/userSettings.ts +21 -0
- package/src/types/voiceApplication.ts +29 -0
- package/src/types/whatsApp.ts +82 -0
- package/src/types/workspaceSettings.ts +22 -0
- package/src/utils/index.ts +14 -0
- package/src/utils/outreach.test.ts +83 -0
- package/src/utils/outreach.ts +57 -0
- package/src/utils/time.ts +66 -0
- package/src/utils/withErrorHandlingAndValidation.test.ts +33 -0
- package/src/utils/withErrorHandlingAndValidation.ts +32 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,100 @@
|
|
|
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>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createApiKeySchema, apiKeyRefSchema } from "./apiKeys.js";
|
|
4
|
+
|
|
5
|
+
describe("createApiKeySchema", () => {
|
|
6
|
+
it("defaults the role to admin with no expiry", () => {
|
|
7
|
+
const parsed = createApiKeySchema.parse({});
|
|
8
|
+
assert.equal(parsed.role, "WORKSPACE_ADMIN");
|
|
9
|
+
assert.equal(parsed.expiresAt, undefined);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("accepts an admin role with a future expiry", () => {
|
|
13
|
+
const future = Date.now() + 86_400_000;
|
|
14
|
+
const parsed = createApiKeySchema.parse({ role: "WORKSPACE_ADMIN", expiresAt: future });
|
|
15
|
+
assert.equal(parsed.role, "WORKSPACE_ADMIN");
|
|
16
|
+
assert.equal(parsed.expiresAt, future);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("rejects member/owner roles — Identity only issues admin keys", () => {
|
|
20
|
+
assert.equal(createApiKeySchema.safeParse({ role: "WORKSPACE_MEMBER" }).success, false);
|
|
21
|
+
assert.equal(createApiKeySchema.safeParse({ role: "WORKSPACE_OWNER" }).success, false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("rejects an expiry in the past", () => {
|
|
25
|
+
const result = createApiKeySchema.safeParse({ expiresAt: Date.now() - 1000 });
|
|
26
|
+
assert.equal(result.success, false);
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
assert.match(result.error.issues[0].message, /future/);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("apiKeyRefSchema", () => {
|
|
34
|
+
it("requires a non-empty ref", () => {
|
|
35
|
+
assert.equal(apiKeyRefSchema.safeParse({ ref: "" }).success, false);
|
|
36
|
+
assert.equal(apiKeyRefSchema.safeParse({ ref: "ak_1" }).success, true);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// API keys are workspace-scoped credentials for unattended, server-to-server
|
|
4
|
+
// integrations (e.g. @qcobro/sdk's loginWithApiKey). Unlike member invites,
|
|
5
|
+
// Fonoster Identity only permits WORKSPACE_ADMIN for an API key (its
|
|
6
|
+
// createApiKeyRequestSchema is `z.enum([WORKSPACE_ADMIN])`), so there is no role
|
|
7
|
+
// choice — every key is an admin-scoped key.
|
|
8
|
+
export const apiKeyRoleEnum = z.enum(["WORKSPACE_ADMIN"]);
|
|
9
|
+
export type ApiKeyRole = z.infer<typeof apiKeyRoleEnum>;
|
|
10
|
+
|
|
11
|
+
export const createApiKeySchema = z.object({
|
|
12
|
+
role: apiKeyRoleEnum.default("WORKSPACE_ADMIN"),
|
|
13
|
+
// Optional expiry as epoch milliseconds; must be in the future when present.
|
|
14
|
+
// Identity stores no expiry when this is omitted (the key never expires).
|
|
15
|
+
expiresAt: z
|
|
16
|
+
.number()
|
|
17
|
+
.int()
|
|
18
|
+
.positive()
|
|
19
|
+
.refine((ms) => ms > Date.now(), { message: "expiresAt must be in the future" })
|
|
20
|
+
.optional()
|
|
21
|
+
});
|
|
22
|
+
export type CreateApiKeyInput = z.infer<typeof createApiKeySchema>;
|
|
23
|
+
|
|
24
|
+
// Identifies a single key for regenerate/delete.
|
|
25
|
+
export const apiKeyRefSchema = z.object({
|
|
26
|
+
ref: z.string().min(1)
|
|
27
|
+
});
|
|
28
|
+
export type ApiKeyRefInput = z.infer<typeof apiKeyRefSchema>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const signUpSchema = z.object({
|
|
4
|
+
name: z.string().min(1).max(60),
|
|
5
|
+
email: z.email(),
|
|
6
|
+
password: z.string().min(8).max(128),
|
|
7
|
+
phone: z.string().max(20).optional(),
|
|
8
|
+
avatar: z.string().max(255).optional()
|
|
9
|
+
});
|
|
10
|
+
export type SignUpInput = z.infer<typeof signUpSchema>;
|
|
11
|
+
|
|
12
|
+
export const loginSchema = z.object({
|
|
13
|
+
email: z.email(),
|
|
14
|
+
password: z.string().min(1),
|
|
15
|
+
twoFactorCode: z.string().optional()
|
|
16
|
+
});
|
|
17
|
+
export type LoginInput = z.infer<typeof loginSchema>;
|
|
18
|
+
|
|
19
|
+
export const refreshTokenSchema = z.object({
|
|
20
|
+
refreshToken: z.string().min(1)
|
|
21
|
+
});
|
|
22
|
+
export type RefreshTokenInput = z.infer<typeof refreshTokenSchema>;
|
|
23
|
+
|
|
24
|
+
// Exchange a workspace API key (accessKeyId + accessKeySecret) for tokens.
|
|
25
|
+
// Used by unattended, server-to-server integrations (e.g. the SDK's
|
|
26
|
+
// loginWithApiKey) that cannot perform an interactive credentials login.
|
|
27
|
+
export const apiKeyLoginSchema = z.object({
|
|
28
|
+
accessKeyId: z.string().min(1),
|
|
29
|
+
accessKeySecret: z.string().min(1)
|
|
30
|
+
});
|
|
31
|
+
export type ApiKeyLoginInput = z.infer<typeof apiKeyLoginSchema>;
|
|
32
|
+
|
|
33
|
+
export const sendResetPasswordCodeSchema = z.object({
|
|
34
|
+
username: z.email(),
|
|
35
|
+
resetPasswordUrl: z.string().url()
|
|
36
|
+
});
|
|
37
|
+
export type SendResetPasswordCodeInput = z.infer<typeof sendResetPasswordCodeSchema>;
|
|
38
|
+
|
|
39
|
+
export const resetPasswordSchema = z.object({
|
|
40
|
+
username: z.email(),
|
|
41
|
+
password: z.string().min(8).max(128),
|
|
42
|
+
verificationCode: z.string().min(1)
|
|
43
|
+
});
|
|
44
|
+
export type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;
|
|
45
|
+
|
|
46
|
+
export const contactTypeEnum = z.enum(["EMAIL", "PHONE"]);
|
|
47
|
+
export type ContactType = z.infer<typeof contactTypeEnum>;
|
|
48
|
+
|
|
49
|
+
export const sendVerificationCodeSchema = z.object({
|
|
50
|
+
contactType: contactTypeEnum,
|
|
51
|
+
value: z.string().min(1)
|
|
52
|
+
});
|
|
53
|
+
export type SendVerificationCodeInput = z.infer<typeof sendVerificationCodeSchema>;
|
|
54
|
+
|
|
55
|
+
export const verifyCodeSchema = z.object({
|
|
56
|
+
username: z.email(),
|
|
57
|
+
contactType: contactTypeEnum,
|
|
58
|
+
value: z.string().min(1),
|
|
59
|
+
verificationCode: z.string().min(1)
|
|
60
|
+
});
|
|
61
|
+
export type VerifyCodeInput = z.infer<typeof verifyCodeSchema>;
|
|
62
|
+
|
|
63
|
+
// Identity currently supports a single OAuth2 provider.
|
|
64
|
+
export const oauthProviderEnum = z.enum(["GITHUB"]);
|
|
65
|
+
export type OauthProvider = z.infer<typeof oauthProviderEnum>;
|
|
66
|
+
|
|
67
|
+
export const oauthSignInSchema = z.object({
|
|
68
|
+
provider: oauthProviderEnum,
|
|
69
|
+
code: z.string().min(1)
|
|
70
|
+
});
|
|
71
|
+
export type OauthSignInInput = z.infer<typeof oauthSignInSchema>;
|
|
72
|
+
|
|
73
|
+
export const oauthSignUpSchema = z.object({
|
|
74
|
+
code: z.string().min(1)
|
|
75
|
+
});
|
|
76
|
+
export type OauthSignUpInput = z.infer<typeof oauthSignUpSchema>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const campaignStatusSchema = z.enum(["PAUSED", "ACTIVE", "COMPLETED", "ARCHIVED"]);
|
|
4
|
+
export type CampaignStatus = z.infer<typeof campaignStatusSchema>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Valid status transitions. A new campaign starts ACTIVE (dispatching immediately).
|
|
8
|
+
* COMPLETED is read-only. An ARCHIVED campaign can be restored to PAUSED — it never
|
|
9
|
+
* resumes dispatch without an explicit later activation. The UI offers only the
|
|
10
|
+
* transitions valid for the current status; the API enforces the same map.
|
|
11
|
+
*/
|
|
12
|
+
export const campaignStatusTransitions: Record<CampaignStatus, CampaignStatus[]> = {
|
|
13
|
+
PAUSED: ["ACTIVE", "ARCHIVED"],
|
|
14
|
+
ACTIVE: ["PAUSED", "COMPLETED", "ARCHIVED"],
|
|
15
|
+
COMPLETED: ["ARCHIVED"],
|
|
16
|
+
ARCHIVED: ["PAUSED"]
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const timeOfDay = z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "must be HH:MM 24h time");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Days of the week the campaign runs, as ISO weekday numbers (1 = Monday … 7 = Sunday).
|
|
23
|
+
* Any non-empty combination is allowed (e.g. Monday + Friday only).
|
|
24
|
+
*/
|
|
25
|
+
const daysOfWeek = z
|
|
26
|
+
.array(z.number().int().min(1, "weekday must be 1–7").max(7, "weekday must be 1–7"))
|
|
27
|
+
.min(1, "select at least one day")
|
|
28
|
+
.refine((days) => new Set(days).size === days.length, {
|
|
29
|
+
message: "days of week must be unique"
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const createCampaignSchema = z
|
|
33
|
+
.object({
|
|
34
|
+
name: z.string().min(1).max(120),
|
|
35
|
+
agentTemplateId: z.string().min(1),
|
|
36
|
+
portfolioIds: z.array(z.string().min(1)).min(1),
|
|
37
|
+
/**
|
|
38
|
+
* The WhatsApp sender number to send from. Required when the agent template is
|
|
39
|
+
* `WHATSAPP` (quality rating and conversation continuity are per-number, so the
|
|
40
|
+
* sender identity is chosen per campaign, not pooled like voice/SMS) and must be
|
|
41
|
+
* omitted otherwise. The cross-field rule is enforced in `createCampaign` against the
|
|
42
|
+
* template's resolved type.
|
|
43
|
+
*/
|
|
44
|
+
whatsAppSenderNumberId: z.string().min(1).optional(),
|
|
45
|
+
startDate: z.string().min(1),
|
|
46
|
+
endDate: z.string().min(1).optional(),
|
|
47
|
+
daysOfWeek,
|
|
48
|
+
startTime: timeOfDay,
|
|
49
|
+
endTime: timeOfDay,
|
|
50
|
+
maxAttemptsPerAccount: z.number().int().positive(),
|
|
51
|
+
maxAttemptsPerDay: z.number().int().positive()
|
|
52
|
+
})
|
|
53
|
+
.refine((c) => !c.endDate || new Date(c.endDate) > new Date(c.startDate), {
|
|
54
|
+
message: "endDate must be after startDate",
|
|
55
|
+
path: ["endDate"]
|
|
56
|
+
});
|
|
57
|
+
export type CreateCampaignInput = z.infer<typeof createCampaignSchema>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Updating a campaign: mutable configuration only. `agentTemplateId` is immutable and
|
|
61
|
+
* `status` is changed through {@link updateCampaignStatusSchema} (guarded transitions),
|
|
62
|
+
* so `.strict()` rejects either being passed here, along with any unknown keys.
|
|
63
|
+
*/
|
|
64
|
+
export const updateCampaignSchema = z
|
|
65
|
+
.object({
|
|
66
|
+
id: z.string().min(1),
|
|
67
|
+
name: z.string().min(1).max(120).optional(),
|
|
68
|
+
startDate: z.string().min(1).optional(),
|
|
69
|
+
endDate: z.string().min(1).optional(),
|
|
70
|
+
daysOfWeek: daysOfWeek.optional(),
|
|
71
|
+
startTime: timeOfDay.optional(),
|
|
72
|
+
endTime: timeOfDay.optional(),
|
|
73
|
+
maxAttemptsPerAccount: z.number().int().positive().optional(),
|
|
74
|
+
maxAttemptsPerDay: z.number().int().positive().optional()
|
|
75
|
+
})
|
|
76
|
+
.strict();
|
|
77
|
+
export type UpdateCampaignInput = z.infer<typeof updateCampaignSchema>;
|
|
78
|
+
|
|
79
|
+
export const updateCampaignStatusSchema = z.object({
|
|
80
|
+
id: z.string().min(1),
|
|
81
|
+
status: campaignStatusSchema
|
|
82
|
+
});
|
|
83
|
+
export type UpdateCampaignStatusInput = z.infer<typeof updateCampaignStatusSchema>;
|
|
84
|
+
|
|
85
|
+
export const deleteCampaignSchema = z.object({
|
|
86
|
+
id: z.string().min(1)
|
|
87
|
+
});
|
|
88
|
+
export type DeleteCampaignInput = z.infer<typeof deleteCampaignSchema>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { agentTypeSchema } from "./agentTemplates.js";
|
|
3
|
+
|
|
4
|
+
export const contactOutcomeSchema = z.enum([
|
|
5
|
+
"DELIVERED",
|
|
6
|
+
"NOT_DELIVERED",
|
|
7
|
+
"NO_ANSWER",
|
|
8
|
+
"PAYMENT_PROMISE",
|
|
9
|
+
"PARTIAL_PAYMENT_AGREED",
|
|
10
|
+
"NEW_TERMS",
|
|
11
|
+
"CALLBACK_REQUESTED",
|
|
12
|
+
"DISPUTE_RAISED",
|
|
13
|
+
"INFORMATION_REQUEST",
|
|
14
|
+
"RESOLVED",
|
|
15
|
+
"PAID",
|
|
16
|
+
"WRONG_NUMBER",
|
|
17
|
+
"OPT_OUT",
|
|
18
|
+
"REFUSED",
|
|
19
|
+
"OTHER"
|
|
20
|
+
]);
|
|
21
|
+
export type ContactOutcome = z.infer<typeof contactOutcomeSchema>;
|
|
22
|
+
|
|
23
|
+
export const aiSentimentSchema = z.enum(["POSITIVE", "NEUTRAL", "NEGATIVE", "HOSTILE"]);
|
|
24
|
+
export type AiSentiment = z.infer<typeof aiSentimentSchema>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* PaymentPromise is the only outcome QCobro tracks with a lifecycle, because a payment
|
|
28
|
+
* is the only commitment it can verify. DUE is derived (PENDING past its dueDate), not a
|
|
29
|
+
* stored status. There is intentionally no "broken" status — an unpaid promise stays on
|
|
30
|
+
* the worklist until an operator resolves it. EXPIRED is set when the account leaves its
|
|
31
|
+
* portfolio.
|
|
32
|
+
*/
|
|
33
|
+
export const paymentPromiseStatusSchema = z.enum(["PENDING", "MET", "EXPIRED", "CANCELLED"]);
|
|
34
|
+
export type PaymentPromiseStatus = z.infer<typeof paymentPromiseStatusSchema>;
|
|
35
|
+
|
|
36
|
+
export const createContactLogSchema = z.object({
|
|
37
|
+
portfolioAccountId: z.string().min(1),
|
|
38
|
+
campaignId: z.string().min(1).optional(),
|
|
39
|
+
/** Agent template used (campaign dispatch or ad-hoc follow-up). */
|
|
40
|
+
agentTemplateId: z.string().min(1).optional(),
|
|
41
|
+
/** Set when this gestión is an ad-hoc follow-up on a specific PaymentPromise. */
|
|
42
|
+
paymentPromiseId: z.string().min(1).optional(),
|
|
43
|
+
agentType: agentTypeSchema,
|
|
44
|
+
contactedAt: z.string().min(1),
|
|
45
|
+
durationSeconds: z.number().int().nonnegative().optional(),
|
|
46
|
+
outcome: contactOutcomeSchema,
|
|
47
|
+
notes: z.string().optional(),
|
|
48
|
+
debtAmountSnapshot: z.number().nonnegative().optional(),
|
|
49
|
+
aiSummary: z.string().optional(),
|
|
50
|
+
aiSentiment: aiSentimentSchema.optional(),
|
|
51
|
+
aiDebtReason: z.string().optional(),
|
|
52
|
+
aiResult: z.string().optional(),
|
|
53
|
+
aiNextStep: z.string().optional(),
|
|
54
|
+
intentMetadata: z.record(z.string(), z.unknown()).optional(),
|
|
55
|
+
channelData: z.record(z.string(), z.unknown()).optional(),
|
|
56
|
+
/**
|
|
57
|
+
* Provider call ref (voice) / message sid (sms) for the dispatch-time attempt.
|
|
58
|
+
* When present, `recordOutcome` upserts the gestión keyed by it (one row per
|
|
59
|
+
* attempt, enriched by the async callback) instead of inserting a duplicate.
|
|
60
|
+
*/
|
|
61
|
+
providerRef: z.string().min(1).optional()
|
|
62
|
+
});
|
|
63
|
+
export type CreateContactLogInput = z.infer<typeof createContactLogSchema>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Input to reserve a campaign attempt before the provider call (the engine's
|
|
67
|
+
* at-most-once step). Increments the attempt counters; writes no gestión.
|
|
68
|
+
*/
|
|
69
|
+
export const reserveAttemptSchema = z.object({
|
|
70
|
+
campaignId: z.string().min(1).optional(),
|
|
71
|
+
portfolioAccountId: z.string().min(1),
|
|
72
|
+
/** When the attempt is being made (ISO). */
|
|
73
|
+
at: z.string().min(1)
|
|
74
|
+
});
|
|
75
|
+
export type ReserveAttemptInput = z.infer<typeof reserveAttemptSchema>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Operator resolution of a payment promise. A promise leaves PENDING only by explicit
|
|
79
|
+
* action: `MET` (paid — v1 is manual-only, no trusted payment signal) or `CANCELLED`.
|
|
80
|
+
* `EXPIRED` is set by the system when the account leaves its portfolio, not here.
|
|
81
|
+
*/
|
|
82
|
+
export const updatePaymentPromiseSchema = z.object({
|
|
83
|
+
id: z.string().min(1),
|
|
84
|
+
status: z.enum(["MET", "CANCELLED"])
|
|
85
|
+
});
|
|
86
|
+
export type UpdatePaymentPromiseInput = z.infer<typeof updatePaymentPromiseSchema>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Follow up on a payment promise with an ad-hoc agent dispatch (no campaign). Writes a
|
|
90
|
+
* gestión with `campaignId` null, the chosen `agentTemplateId`, and a link to the promise.
|
|
91
|
+
*/
|
|
92
|
+
export const followUpPaymentPromiseSchema = z.object({
|
|
93
|
+
paymentPromiseId: z.string().min(1),
|
|
94
|
+
agentTemplateId: z.string().min(1)
|
|
95
|
+
});
|
|
96
|
+
export type FollowUpPaymentPromiseInput = z.infer<typeof followUpPaymentPromiseSchema>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** The channels the dispatch layer triggers (subset of AgentType). */
|
|
4
|
+
export const dispatchChannelSchema = z.enum([
|
|
5
|
+
"VOICE_AI",
|
|
6
|
+
"VOICE_PRERECORDED",
|
|
7
|
+
"SMS",
|
|
8
|
+
"EMAIL",
|
|
9
|
+
"WHATSAPP"
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A normalized dispatch request: a channel, a destination, the render context
|
|
14
|
+
* (the customer's account fields), and the raw template body fields for that
|
|
15
|
+
* channel. The dispatch function renders the bodies against `context` before
|
|
16
|
+
* sending, so callers pass raw templates — not pre-rendered strings.
|
|
17
|
+
*/
|
|
18
|
+
export const dispatchOutreachSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
channel: dispatchChannelSchema,
|
|
21
|
+
/** Destination number (E.164). */
|
|
22
|
+
to: z.string().min(1),
|
|
23
|
+
/** Render context — the customer's account fields plus derived values. */
|
|
24
|
+
context: z.record(z.string(), z.unknown()).default({}),
|
|
25
|
+
/** Optional explicit caller-ID/sender; otherwise picked from the pool. */
|
|
26
|
+
from: z.string().min(1).optional(),
|
|
27
|
+
/** Voice: the provider application ref to drive the call. */
|
|
28
|
+
appRef: z.string().min(1).optional(),
|
|
29
|
+
/** Voz IA: opening line template (the autopilot may also open silently). */
|
|
30
|
+
firstMessage: z.string().optional(),
|
|
31
|
+
/** Voz pregrabada: the whole spoken script template (locuted via TTS). */
|
|
32
|
+
script: z.string().optional(),
|
|
33
|
+
/** SMS / EMAIL / WHATSAPP: message body template. For WHATSAPP this is the fetched
|
|
34
|
+
* template body whose `{{vars}}` are extracted and sent as named parameters. */
|
|
35
|
+
body: z.string().optional(),
|
|
36
|
+
/** EMAIL: subject line template. */
|
|
37
|
+
subject: z.string().optional(),
|
|
38
|
+
/** WHATSAPP: Meta-approved template name to send. */
|
|
39
|
+
templateName: z.string().optional(),
|
|
40
|
+
/** WHATSAPP: Meta template-send language code (sourced from the workspace, e.g. `es_DO`). */
|
|
41
|
+
languageCode: z.string().optional()
|
|
42
|
+
})
|
|
43
|
+
.superRefine((value, ctx) => {
|
|
44
|
+
if (value.channel === "SMS" && (value.body ?? "").length === 0) {
|
|
45
|
+
ctx.addIssue({ code: "custom", path: ["body"], message: "SMS requires a message body" });
|
|
46
|
+
}
|
|
47
|
+
if (value.channel === "EMAIL") {
|
|
48
|
+
if ((value.subject ?? "").length === 0) {
|
|
49
|
+
ctx.addIssue({ code: "custom", path: ["subject"], message: "Email requires a subject" });
|
|
50
|
+
}
|
|
51
|
+
if ((value.body ?? "").length === 0) {
|
|
52
|
+
ctx.addIssue({ code: "custom", path: ["body"], message: "Email requires a body" });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (value.channel === "WHATSAPP") {
|
|
56
|
+
if ((value.templateName ?? "").length === 0) {
|
|
57
|
+
ctx.addIssue({
|
|
58
|
+
code: "custom",
|
|
59
|
+
path: ["templateName"],
|
|
60
|
+
message: "WhatsApp requires a templateName"
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if ((value.languageCode ?? "").length === 0) {
|
|
64
|
+
ctx.addIssue({
|
|
65
|
+
code: "custom",
|
|
66
|
+
path: ["languageCode"],
|
|
67
|
+
message: "WhatsApp requires a languageCode"
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if ((value.body ?? "").length === 0) {
|
|
71
|
+
ctx.addIssue({
|
|
72
|
+
code: "custom",
|
|
73
|
+
path: ["body"],
|
|
74
|
+
message: "WhatsApp requires a template body for parameter extraction"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Voice dispatch needs the synced application ref. Neither voice channel requires a
|
|
79
|
+
// `firstMessage`: VOICE_AI may open silently (the autopilot places the call and waits
|
|
80
|
+
// for the customer to speak first), and pre-recorded has no first message at all.
|
|
81
|
+
// EMAIL and SMS are not voice and need no appRef.
|
|
82
|
+
const isVoice = value.channel === "VOICE_AI" || value.channel === "VOICE_PRERECORDED";
|
|
83
|
+
if (isVoice && !value.appRef) {
|
|
84
|
+
ctx.addIssue({
|
|
85
|
+
code: "custom",
|
|
86
|
+
path: ["appRef"],
|
|
87
|
+
message: "Voice dispatch requires appRef"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export type DispatchOutreachInput = z.infer<typeof dispatchOutreachSchema>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Input for the manual "Contactar manualmente" procedure: which customer and which
|
|
96
|
+
* campaign. A manual contact runs the campaign's agent against this one customer, so
|
|
97
|
+
* the campaign (required) determines the agent/channel; the server resolves these to
|
|
98
|
+
* a {@link dispatchOutreachSchema} request.
|
|
99
|
+
*/
|
|
100
|
+
export const manualOutreachSchema = z.object({
|
|
101
|
+
portfolioAccountId: z.string().min(1),
|
|
102
|
+
// Manual outreach is agent-based, not campaign-based: it dispatches the chosen agent
|
|
103
|
+
// template ad-hoc and records a campaign-less gestión (no CampaignAccountState).
|
|
104
|
+
agentTemplateId: z.string().min(1),
|
|
105
|
+
/** Operator override for the email subject (rendered, replaces template value). */
|
|
106
|
+
subject: z.string().optional(),
|
|
107
|
+
/** Operator override for the message body — SMS or EMAIL (rendered). */
|
|
108
|
+
body: z.string().optional(),
|
|
109
|
+
/** Operator override for the Voz IA opening line (rendered). */
|
|
110
|
+
firstMessage: z.string().optional(),
|
|
111
|
+
/** Operator override for the Voz pregrabada spoken script (rendered). */
|
|
112
|
+
script: z.string().optional()
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export type ManualOutreachInput = z.infer<typeof manualOutreachSchema>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The structured decision the EMAIL autopilot returns for an inbound reply. Validated so a
|
|
5
|
+
* model can't drive the loop with malformed output. `outcome` mirrors the contact-log
|
|
6
|
+
* outcomes; `objective` carries promise details when the reply implies one.
|
|
7
|
+
*/
|
|
8
|
+
export const emailAutopilotDecisionSchema = z.object({
|
|
9
|
+
action: z.enum(["reply", "ignore", "resolve", "escalate"]),
|
|
10
|
+
replyBody: z.string().optional(),
|
|
11
|
+
outcome: z.string().optional(),
|
|
12
|
+
objective: z
|
|
13
|
+
.object({
|
|
14
|
+
type: z.string(),
|
|
15
|
+
amount: z.number().optional(),
|
|
16
|
+
dueDate: z.string().optional(),
|
|
17
|
+
note: z.string().optional()
|
|
18
|
+
})
|
|
19
|
+
.nullish()
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Normalized inbound email from the provider webhook. The provider payload is mapped to
|
|
24
|
+
* this before ingestion so the function stays provider-agnostic.
|
|
25
|
+
*/
|
|
26
|
+
export const inboundEmailSchema = z.object({
|
|
27
|
+
from: z.string().min(1),
|
|
28
|
+
to: z.array(z.string()).default([]),
|
|
29
|
+
subject: z.string().optional(),
|
|
30
|
+
text: z.string().default(""),
|
|
31
|
+
messageId: z.string().optional(),
|
|
32
|
+
inReplyTo: z.string().optional(),
|
|
33
|
+
references: z.array(z.string()).optional(),
|
|
34
|
+
headers: z.record(z.string(), z.string()).optional()
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export type InboundEmailInput = z.infer<typeof inboundEmailSchema>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from "./auth.js";
|
|
2
|
+
export * from "./workspaces.js";
|
|
3
|
+
export * from "./apiKeys.js";
|
|
4
|
+
export * from "./users.js";
|
|
5
|
+
export * from "./portfolios.js";
|
|
6
|
+
export * from "./workspaceSettings.js";
|
|
7
|
+
export * from "./userSettings.js";
|
|
8
|
+
export * from "./agentTemplates.js";
|
|
9
|
+
export * from "./campaigns.js";
|
|
10
|
+
export * from "./contactLog.js";
|
|
11
|
+
export * from "./dispatch.js";
|
|
12
|
+
export * from "./email.js";
|
|
13
|
+
export * from "./whatsApp.js";
|
|
14
|
+
export * from "./voiceEvent.js";
|
|
15
|
+
export * from "./insight.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { aiSentimentSchema } from "./contactLog.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The structured analysis an LLM must return for a gestión transcript. Mirrors the
|
|
6
|
+
* `ai*` fields on `AccountContactLog`; text fields are written in the call's language,
|
|
7
|
+
* `aiSentiment` is always one of the fixed enum values.
|
|
8
|
+
*/
|
|
9
|
+
export const gestionInsightSchema = z.object({
|
|
10
|
+
aiSummary: z.string().min(1),
|
|
11
|
+
aiSentiment: aiSentimentSchema,
|
|
12
|
+
aiDebtReason: z.string().min(1),
|
|
13
|
+
aiResult: z.string().min(1),
|
|
14
|
+
aiNextStep: z.string().min(1)
|
|
15
|
+
});
|
|
16
|
+
export type GestionInsight = z.infer<typeof gestionInsightSchema>;
|
|
17
|
+
|
|
18
|
+
/** Input to the generate-insight operation — the gestión (contact-log) id. */
|
|
19
|
+
export const generateInsightInputSchema = z.object({ id: z.string().min(1) });
|
|
20
|
+
export type GenerateInsightInput = z.infer<typeof generateInsightInputSchema>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const createPortfolioSchema = z.object({
|
|
4
|
+
name: z.string().min(1).max(120),
|
|
5
|
+
clientId: z.string().min(1).max(120)
|
|
6
|
+
});
|
|
7
|
+
export type CreatePortfolioInput = z.infer<typeof createPortfolioSchema>;
|
|
8
|
+
|
|
9
|
+
export const updatePortfolioSchema = z.object({
|
|
10
|
+
id: z.string().min(1),
|
|
11
|
+
name: z.string().min(1).max(120).optional(),
|
|
12
|
+
// `archived` toggles the portfolio's archived state: true sets `archivedAt` to
|
|
13
|
+
// now, false clears it (restore). There is no separate status concept.
|
|
14
|
+
archived: z.boolean().optional()
|
|
15
|
+
});
|
|
16
|
+
export type UpdatePortfolioInput = z.infer<typeof updatePortfolioSchema>;
|
|
17
|
+
|
|
18
|
+
export const deletePortfolioSchema = z.object({
|
|
19
|
+
id: z.string().min(1)
|
|
20
|
+
});
|
|
21
|
+
export type DeletePortfolioInput = z.infer<typeof deletePortfolioSchema>;
|
|
22
|
+
|
|
23
|
+
export const accountRowSchema = z.object({
|
|
24
|
+
externalId: z.string().min(1),
|
|
25
|
+
fullName: z.string().min(1),
|
|
26
|
+
phone: z.string().optional(),
|
|
27
|
+
email: z.string().email().optional(),
|
|
28
|
+
preferredLanguage: z.string().optional(),
|
|
29
|
+
bestTimeToCall: z.string().optional(),
|
|
30
|
+
customerSegment: z.string().optional(),
|
|
31
|
+
principalAmount: z.number().nonnegative().default(0),
|
|
32
|
+
termsAmount: z.number().nonnegative().default(0),
|
|
33
|
+
termsFrequency: z.string().optional(),
|
|
34
|
+
termsLength: z.number().int().nonnegative().default(0),
|
|
35
|
+
outstandingBalance: z.number().nonnegative(),
|
|
36
|
+
daysPastDue: z.number().int().nonnegative().default(0),
|
|
37
|
+
missedInstallments: z.number().int().nonnegative().default(0),
|
|
38
|
+
lastPaymentDate: z.string().optional(),
|
|
39
|
+
lastPaymentAmount: z.number().nonnegative().optional(),
|
|
40
|
+
negotiationOptions: z.string().optional()
|
|
41
|
+
});
|
|
42
|
+
export type AccountRowInput = z.infer<typeof accountRowSchema>;
|
|
43
|
+
|
|
44
|
+
export const syncAccountsInputSchema = z.object({
|
|
45
|
+
portfolioId: z.string().min(1),
|
|
46
|
+
mode: z.enum(["APPEND_ONLY", "UPDATE_EXISTING", "REPLACE"]),
|
|
47
|
+
rows: z.array(accountRowSchema).min(1)
|
|
48
|
+
});
|
|
49
|
+
export type SyncAccountsInput = z.infer<typeof syncAccountsInputSchema>;
|