@avakado.ai/schemas 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,8 +22,18 @@ configureSchemas({ sendKafkaMessage });
22
22
  await mongoose.connect(process.env.MONGO_URI);
23
23
  ```
24
24
 
25
+ SaaS and Chat also pass the OAuth host map used by `ApiAuthenticators.refreshToken`:
26
+
27
+ ```js
28
+ configureSchemas({
29
+ sendKafkaMessage,
30
+ getAuthProvider: (name) => PROVIDER_MAP[name],
31
+ providerSupportsRefresh,
32
+ });
33
+ ```
34
+
25
35
  ```js
26
- import { Business, Payment, Subscription, UsageLog, Conversation, Campaign, Task } from "@avakado.ai/schemas";
36
+ import { Business, User, AgentModel, Channel, Workflow } from "@avakado.ai/schemas";
27
37
  import { SUBSCRIPTION_STATUS, CURRENT_SUBSCRIPTION_STATUSES } from "@avakado.ai/schemas/enums";
28
38
  import { Payment } from "@avakado.ai/schemas/models/Payments.js"; // narrower import, same registration
29
39
  ```
@@ -32,7 +42,10 @@ Every entry point returns the **same** registered model. Importing a model twice
32
42
  via a subpath, or from two places in a dependency tree — reuses the existing registration instead of
33
43
  throwing `OverwriteModelError`.
34
44
 
35
- ## What is in v1
45
+ ## What is in v1.1
46
+
47
+ Every mongoose model the four services used. Where copies differed, the package keeps the **union**
48
+ of fields (and a superset of enums) so a save in any service still has a path to write.
36
49
 
37
50
  | Model | Collection | Notes |
38
51
  | --- | --- | --- |
@@ -43,15 +56,30 @@ throwing `OverwriteModelError`.
43
56
  | `Conversation` | `Conversations` | includes `updateStatus`, which needs `sendKafkaMessage` |
44
57
  | `Campaign` / `Task` | `Campaign` / `Tasks` | includes `updateTimeLine`; the task executor stays in socketio-server |
45
58
  | `Trigger` | `triggers` | read by `Conversation.updateStatus` |
46
-
47
- Only models that have been through a cross-service alignment pass are here. Extracting an unaligned
48
- model would freeze whichever copy happened to be picked that is exactly how production ended up
49
- running a partial index whose filter matched zero documents.
50
-
51
- Still per-service, pending alignment: `Agent`, `Channels`, `Workflow`, `Messages`, `Leads`,
52
- `CallSessions`, `Plans`, `User`, `Collection`, `Action`, `Log`, `apiAuthenticator`, `market`,
53
- `InbuiltNodes`, `ExternalServiceProviders`, and the already-identical-but-not-yet-moved `BillingLogs`,
54
- `Data`, `Document`, `Integrations`, `Job`, `Tickets`, `notifications`.
59
+ | `User` | `Users` | exports `ScopesEnum` / `RoleScopes` |
60
+ | `AgentModel` | `Agent` | runtime discriminators `TURN_BASED` / `REALTIME` / `BACKGROUND` |
61
+ | `Channel` | `Channel` | type enum includes both `website` and `web` |
62
+ | `CallSession` | `CallSession` | includes AvaPhone's `usage` |
63
+ | `Plan` | `Plans` | type enum includes `TEST` |
64
+ | `Api` / `Providers` | `Api` / `Providers` | |
65
+ | `Template` | `StandardProduct` | marketplace discriminator `agent` / `action` |
66
+ | `Message` / `MessageSession` | `Messages` / `MessageSessions` | |
67
+ | `Lead` / `LeadTemplate` | `Leads` / `LeadTemplates` | |
68
+ | `Log` | `Logs` | includes Lambda `error` / `response` |
69
+ | `BillingLog` | `BillingLogs` | |
70
+ | `ApiAuthenticators` | `ApiAuthenticators` | token refresh needs `getAuthProvider` |
71
+ | `DemonstrationModel` | `Demonstrations` | |
72
+ | `Action` | `Action` | |
73
+ | `Collection` | `Collection` | Lambda stage helpers are statics |
74
+ | `Node` / `NodeModel` | `Nodes` | same registration, two export names |
75
+ | `Integration` | `Integration` | |
76
+ | `Job` | `Job` | discriminator `outboundCall` |
77
+ | `Notification` | `Notification` | |
78
+ | `Ticket` | `Ticket` | |
79
+ | `Document` | `documents` | mongoose model name is `document` |
80
+ | `Data` | `Data` | |
81
+ | `Workflow` | `Workflow` | `updateStatus` / `getNextNode` live here; `execute` stays in socketio-server |
82
+ | `WorkflowLogs` | `WorkflowLogs` | |
55
83
 
56
84
  ## Behaviour that needs the host service
57
85
 
@@ -61,9 +89,11 @@ A package cannot import a service's `utils/`, so anything a shared model needs i
61
89
  | Option | Used by | If missing |
62
90
  | --- | --- | --- |
63
91
  | `sendKafkaMessage` | `Conversation.updateStatus` | throws a named error telling you to call `configureSchemas` |
92
+ | `getAuthProvider` | `ApiAuthenticators` token refresh | throws if `refreshToken` / `ensureValidToken` / `_resolveProvider` runs |
93
+ | `providerSupportsRefresh` | `ApiAuthenticators` token refresh | same |
64
94
 
65
- Methods that need more than a function — socketio-server's `Task.trigger` needs its `RealtimeServer`
66
- plus the `Message` and `CallSession` models — stay in the service and attach to the shared model:
95
+ Methods that need more than a function — socketio-server's `Task.trigger` needs its `RealtimeServer`,
96
+ and `Workflow.execute` writes `WorkflowLogs` — stay in the service and attach to the shared model:
67
97
 
68
98
  ```js
69
99
  import { Task } from "@avakado.ai/schemas";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avakado.ai/schemas",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Canonical Mongo schemas, models, enums and index declarations shared by the Avakado services.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import "./models/index.js";
1
2
  export * from "./models/index.js";
2
3
  export * from "./enums/index.js";
3
4
  export { configureSchemas, defineModel, getSchemaConfig } from "./runtime.js";
@@ -0,0 +1,19 @@
1
+ import { Schema } from "mongoose";
2
+ import { defineModel } from "../runtime.js";
3
+ const ActionSchema = new Schema({
4
+ name: String,
5
+ business: { type: Schema.Types.ObjectId, ref: 'Businesses' },
6
+ async: { type: Boolean, default: true },
7
+ description: String,
8
+ needsApproval: Boolean, // Knowledge fetching doesn't need approval
9
+ parameters: Schema.Types.Mixed,
10
+ config: Schema.Types.Mixed,
11
+ functionString: String,
12
+ errorFunction: String,
13
+ UI: Schema.Types.Mixed,
14
+ isPublic: { type: Boolean, default: false },
15
+ }, {
16
+ timestamps: true
17
+ });
18
+ export const Action = defineModel('Action', ActionSchema, "Action");
19
+
@@ -0,0 +1,219 @@
1
+ import { Schema } from "mongoose";
2
+ import { defineModel } from "../runtime.js";
3
+
4
+ const ProviderConfig = {
5
+ openai: {
6
+ TURN_BASED: {
7
+ models: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna'],
8
+ },
9
+ REALTIME: {
10
+ models: ['gpt-realtime', 'gpt-realtime-mini', 'gpt-realtime-2.1', 'gpt-realtime-2.1-mini', 'gpt-realtime-2.0', 'gpt-realtime-1.5'],
11
+ voices: ['alloy', 'ash', 'ballad', 'coral', 'echo', 'sage', 'shimmer', 'verse', 'marin', 'cedar'],
12
+ modalities: ['audio', 'text'],
13
+ wssUrl: 'wss://api.openai.com/v1/realtime',
14
+ },
15
+ },
16
+ gemini: {
17
+ TURN_BASED: {
18
+ models: ['gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-3.5-flash', 'gemini-3.1-flash-lite', 'gemini-2.5-pro'],
19
+ },
20
+ REALTIME: {
21
+ models: ['gemini-2.5-flash-native-audio-latest'],
22
+ voices: ['Zephyr', 'Puck', 'Charon', 'Kore', 'Fenrir', 'Leda', 'Orus', 'Aoede', 'Callirrhoe', 'Autonoe', 'Enceladus', 'Iapetus', 'Umbriel', 'Algieba', 'Despina', 'Erinome', 'Algenib', 'Rasalgethi', 'Laomedeia', 'Achernar', 'Alnilam', 'Schedar', 'Gacrux', 'Pulcherrima', 'Achird', 'Zubenelgenubi', 'Vindemiatrix', 'Sadachbia', 'Sadaltager', 'Sulafat'],
23
+ modalities: ['AUDIO', 'TEXT'],
24
+ wssUrl: 'wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent',
25
+ },
26
+ },
27
+ anthropic: {
28
+ TURN_BASED: { models: ['claude-opus-4-8', 'claude-sonnet-5', 'claude-haiku-4-5-20251001'] },
29
+ REALTIME: null
30
+ }
31
+ };
32
+
33
+
34
+ // --- AGENT SCHEMA ---
35
+ const ModelConfigSchema = new Schema({
36
+ provider: { type: String, enum: [...Object.keys(ProviderConfig), 'custom'], required: true, default: 'openai' },
37
+ customProviderRef: { type: String, required: function () { return this.provider === 'custom' } },
38
+ providerData: Schema.Types.Mixed,
39
+ model: {
40
+ type: String, required: true, validate: {
41
+ validator: function (value) {
42
+ if (this.provider === 'custom') return true;
43
+ return ProviderConfig[this.provider]?.[this._doc?.runtime]?.models.includes(value);
44
+ }, message: props => `Model "${props.value}" is not valid for provider "${this.provider}"`
45
+ }
46
+ },
47
+ modelSettings: {
48
+ temperature: { type: Number, default: 0.3 },
49
+ toolChoice: { type: String, default: 'auto' },
50
+ topP: Number,
51
+ frequencyPenalty: Number,
52
+ presencePenalty: Number,
53
+ parallelToolCalls: Boolean,
54
+ truncation: { type: String, enum: ['auto', 'disabled'] },
55
+ maxTokens: Number,
56
+ store: Boolean,
57
+ reasoning: {
58
+ effort: { type: String, enum: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'] },
59
+ mode: String,
60
+ context: { type: String, enum: ['auto', 'current_turn', 'all_turns', null] },
61
+ summary: { type: String, enum: ['auto', 'concise', 'detailed'] },
62
+ },
63
+ text: { verbosity: { type: String, enum: ['low', 'medium', 'high'] } },
64
+ promptCacheOptions: {
65
+ mode: { type: String, enum: ['implicit', 'explicit'] },
66
+ ttl: { type: String, enum: ['30m'] },
67
+ },
68
+ promptCacheRetention: { type: String, enum: ['in-memory', '24h', null] },
69
+ // retry: {
70
+ // maxRetries: Number,
71
+ // backoff: {
72
+ // initialDelayMs: Number,
73
+ // maxDelayMs: Number,
74
+ // multiplier: Number,
75
+ // jitter: Boolean,
76
+ // },
77
+ // policy: { type: String, enum: ['providerSuggested', 'networkError', 'retryAfter', 'never'] },
78
+ // },
79
+ },
80
+ },
81
+ { _id: false }
82
+ );
83
+ const ResponseConfigSchema = new Schema({
84
+ provider: { type: String, enum: [...Object.keys(ProviderConfig), 'elevenlabs', 'custom'], required: true },
85
+ }, { _id: false, discriminatorKey: 'provider' });
86
+ const RealtimeConfigSchema = new Schema({
87
+ modality: {
88
+ type: String, required: true,
89
+ validate: {
90
+ validator(value) {
91
+ const provider = this._doc?.modelConfig?.provider;
92
+ const runtime = this._doc?.runtime;
93
+ return ProviderConfig[provider]?.[runtime]?.modalities?.includes(value);
94
+ },
95
+ message: props => `Invalid modality "${props.value}" for this provider`,
96
+ }
97
+ },
98
+ responseConfig: ResponseConfigSchema
99
+ }, { _id: false });
100
+ const responseConfigPath = RealtimeConfigSchema.path('responseConfig');
101
+ responseConfigPath.discriminator('openai', new Schema({
102
+ wssUrl: { type: String, default: ProviderConfig.openai?.REALTIME?.wssUrl },
103
+ audio: {
104
+ input: {
105
+ noise_reduction: { type: String, enum: ['near_field', 'far_field'], default: 'near_field' },
106
+ transcription: { model: { type: String, default: "whisper-1" }, prompt: String, language: String },
107
+ turn_detection: {
108
+ type: { type: String, enum: ['server_vad', 'semantic_vad'], default: 'server_vad' },
109
+ // shared
110
+ create_response: { type: Boolean, default: true },
111
+ interrupt_response: { type: Boolean, default: true },
112
+ // server_vad only
113
+ prefix_padding_ms: {
114
+ type: Number,
115
+ default: function () { return this._doc.audio.input.turn_detection.type === 'server_vad' ? 300 : undefined; },
116
+ validate: {
117
+ validator(value) {
118
+ if (value == null) return true;
119
+ return this._doc.audio.input.turn_detection.type === 'server_vad';
120
+ },
121
+ message: 'prefix_padding_ms is only valid for server_vad',
122
+ },
123
+ },
124
+ silence_duration_ms: {
125
+ type: Number,
126
+ default: function () { return this._doc.audio.input.turn_detection.type === 'server_vad' ? 500 : undefined; },
127
+ validate: {
128
+ validator(value) {
129
+ if (value == null) return true;
130
+ return this._doc.audio.input.turn_detection.type === 'server_vad';
131
+ },
132
+ message: 'silence_duration_ms is only valid for server_vad',
133
+ },
134
+ },
135
+ threshold: {
136
+ type: Number,
137
+ min: 0,
138
+ max: 1,
139
+ default: function () { return this._doc.audio.input.turn_detection.type === 'server_vad' ? 0.5 : undefined; },
140
+ validate: {
141
+ validator(value) {
142
+ if (value == null) return true;
143
+ return this._doc.audio.input.turn_detection.type === 'server_vad';
144
+ },
145
+ message: 'threshold is only valid for server_vad',
146
+ },
147
+ },
148
+ // semantic_vad only
149
+ eagerness: {
150
+ type: String,
151
+ enum: ['low', 'medium', 'high', 'auto'],
152
+ default: function () { return this._doc.audio.input.turn_detection.type === 'semantic_vad' ? 'auto' : undefined; },
153
+ validate: {
154
+ validator(value) {
155
+ if (value == null) return true;
156
+ return this._doc.audio.input.turn_detection.type === 'semantic_vad';
157
+ },
158
+ message: 'eagerness is only valid for semantic_vad',
159
+ },
160
+ },
161
+ }
162
+ },
163
+ output: {
164
+ speed: { type: Number, min: 0.25, max: 2.0, default: 1.0 },
165
+ voice: { type: String, validate: { validator: function (value) { return ProviderConfig.openai?.REALTIME?.voices.includes(value); }, message: props => `Invalid voice for openai` } },
166
+ }
167
+ }
168
+ }, { _id: false }));
169
+ responseConfigPath.discriminator('gemini', new Schema({
170
+ wssUrl: { type: String, default: ProviderConfig.gemini?.REALTIME?.wssUrl },
171
+ proactivity: { proactiveAudio: { type: Boolean, default: true } },
172
+ enableAffectiveDialog: { type: Boolean, default: true },
173
+ realtimeInputConfig: {
174
+ automaticActivityDetection: {
175
+ disabled: { type: Boolean, default: false },
176
+ startOfSpeechSensitivity: { type: String, enum: ['START_SENSITIVITY_LOW', 'START_SENSITIVITY_HIGH'], default: 'START_SENSITIVITY_HIGH' }, // gemini
177
+ endOfSpeechSensitivity: { type: String, enum: ['END_SENSITIVITY_LOW', 'END_SENSITIVITY_HIGH'], default: 'END_SENSITIVITY_LOW' }, // gemini
178
+ prefixPaddingMs: { type: Number, default: 300 },
179
+ silenceDurationMs: { type: Number, default: 1000 },
180
+ }
181
+ },
182
+ realtimeOutputConfig: {
183
+ speed: { type: Number, min: 0.25, max: 2.0, default: 1.0 },
184
+ voice: { type: String, validate: { validator: function (value) { return ProviderConfig.gemini?.REALTIME?.voices.includes(value); }, message: props => `Invalid voice for openai` } },
185
+ },
186
+ inputAudioTranscription: Schema.Types.Mixed,
187
+ outputAudioTranscription: Schema.Types.Mixed,
188
+ }, { _id: false }));
189
+ const AgentSchema = new Schema({
190
+ personalInfo: {
191
+ name: String,
192
+ description: String,
193
+ avatar: String,
194
+ systemPrompt: {
195
+ role: String,
196
+ objective: String,
197
+ instructionsAndWorkflow: String,
198
+ constraintsAndRules: String
199
+ }
200
+ },
201
+ runtime: { type: String, enum: ['TURN_BASED', 'REALTIME', 'BACKGROUND'], default: 'TURN_BASED' },
202
+ modelConfig: ModelConfigSchema,
203
+ workflow: { type: Schema.Types.ObjectId, ref: 'Workflow' },
204
+ collections: [{ type: Schema.Types.ObjectId, ref: 'Collection' }],
205
+ channels: [{ type: Schema.Types.ObjectId, ref: 'Channel' }],
206
+ actions: [{ type: Schema.Types.ObjectId, ref: 'Action' }],
207
+ tool_choice: { type: String, enum: ['auto', 'none', 'required'], default: "auto" },
208
+ business: { type: Schema.Types.ObjectId, ref: 'Businesses' },
209
+ createdBy: { type: Schema.Types.ObjectId, ref: 'Users' },
210
+ isPublic: { type: Boolean, default: false },
211
+ isFeatured: { type: Boolean, default: false },
212
+ }, {
213
+ discriminatorKey: 'runtime',
214
+ timestamps: true
215
+ });
216
+ AgentSchema.discriminator('TURN_BASED', new Schema({}, { _id: false }))
217
+ AgentSchema.discriminator('REALTIME', RealtimeConfigSchema)
218
+ AgentSchema.discriminator('BACKGROUND', new Schema({}, { _id: false }))
219
+ export const AgentModel = defineModel('Agent', AgentSchema, "Agent");
@@ -0,0 +1,131 @@
1
+ import { Schema } from "mongoose";
2
+ import { defineModel } from "../runtime.js";
3
+
4
+ // ─── Enums ────────────────────────────────────────────────────────────────────
5
+
6
+ const BILLING_CATEGORY = [
7
+ // AI
8
+ "ai.conversation", // agent replying to a lead
9
+ "ai.analysis", // post-conversation analysis
10
+ "ai.summarization", // conversation summarization
11
+ "ai.transcription", // voice/audio → text (Whisper)
12
+ "ai.embedding", // vector embeddings
13
+ // Messaging
14
+ "messaging.outbound", // outbound message sent via WhatsApp/Telegram/SMS
15
+ "messaging.inbound", // inbound (if your provider charges per inbound)
16
+ "messaging.template", // WhatsApp template message (meta charges separately)
17
+ // Workflow
18
+ "workflow.execution", // workflow triggered and ran
19
+ "workflow.step", // individual step within a workflow
20
+ // Platform
21
+ "platform.api_call", // external API call made on behalf of business
22
+ "platform.storage", // file/media storage
23
+ "platform.export", // data export
24
+ ];
25
+
26
+ const BILLING_STATUS = ["success", "failed", "refunded", "pending"];
27
+
28
+ const CHANNEL_TYPE = ["Whatsapp", "Telegram", "SMS", "Email", "Web", "Phone"];
29
+
30
+ // ─── Sub-schemas ──────────────────────────────────────────────────────────────
31
+
32
+ // AI-specific usage — only populated for ai.* categories
33
+ const aiUsageSchema = new Schema({
34
+ model: { type: String }, // "gpt-4o-mini", "whisper-1"
35
+ inputTokens: { type: Number, default: 0 },
36
+ cachedTokens: { type: Number, default: 0 }, // prompt cache hits (cheaper)
37
+ outputTokens: { type: Number, default: 0 },
38
+ totalTokens: { type: Number, default: 0 },
39
+ turns: { type: Number, default: 1 }, // how many agent turns ran
40
+ toolCallCount: { type: Number, default: 0 }, // number of tool calls made
41
+ }, { _id: false });
42
+
43
+ // Messaging-specific — only for messaging.* categories
44
+ const messagingUsageSchema = new Schema({
45
+ channel: { type: String, enum: CHANNEL_TYPE },
46
+ provider: { type: String }, // "meta", "twilio", "vonage"
47
+ messageType: { type: String }, // "text", "template", "media"
48
+ segmentCount: { type: Number, default: 1 }, // SMS segments
49
+ templateName: { type: String }, // WhatsApp template name
50
+ direction: { type: String, enum: ["inbound", "outbound"] },
51
+ }, { _id: false });
52
+
53
+ // Workflow-specific
54
+ const workflowUsageSchema = new Schema({
55
+ stepsExecuted: { type: Number, default: 0 },
56
+ stepsFailed: { type: Number, default: 0 },
57
+ durationMs: { type: Number }, // total execution time
58
+ }, { _id: false });
59
+
60
+ // Cost breakdown — always populated
61
+ const costSchema = new Schema({
62
+ // Raw provider cost
63
+ providerCostUsd: { type: Number, default: 0 }, // what YOU pay provider
64
+ // What you charge the business
65
+ creditRate: { type: Number }, // your spendRatio at time of billing
66
+ creditsCharged: { type: Number, default: 0 }, // credits deducted from business
67
+ // For reconciliation
68
+ currency: { type: String, default: "USD" },
69
+ }, { _id: false });
70
+
71
+ // References — what triggered this log entry
72
+ const sourceSchema = new Schema({
73
+ conversation: { type: Schema.Types.ObjectId, ref: "Conversation" },
74
+ message: { type: Schema.Types.ObjectId, ref: "Message" },
75
+ campaign: { type: Schema.Types.ObjectId, ref: "Campaign" },
76
+ workflow: { type: Schema.Types.ObjectId, ref: "Workflow" },
77
+ channel: { type: Schema.Types.ObjectId, ref: "Channel" },
78
+ agent: { type: Schema.Types.ObjectId, ref: "Agent" },
79
+ lead: { type: Schema.Types.ObjectId, ref: "Lead" },
80
+ // The external ID from the provider (wamid, etc.)
81
+ externalId: { type: String },
82
+ }, { _id: false });
83
+
84
+ // ─── Main Schema ──────────────────────────────────────────────────────────────
85
+
86
+ const BillingLogSchema = new Schema({
87
+ // ── Who ───────────────────────────────────────────────────────────
88
+ business: { type: Schema.Types.ObjectId, ref: 'Businesses', required: true },
89
+
90
+ // ── What ──────────────────────────────────────────────────────────
91
+ category: { type: String, enum: BILLING_CATEGORY, required: true },
92
+ status: { type: String, enum: BILLING_STATUS, default: "success" },
93
+
94
+ // ── Why / Where ───────────────────────────────────────────────────
95
+ source: { type: sourceSchema, default: () => ({}) },
96
+
97
+ // ── Usage detail (only relevant sub-schema will be populated) ─────
98
+ ai: { type: aiUsageSchema },
99
+ messaging: { type: messagingUsageSchema },
100
+ workflow: { type: workflowUsageSchema },
101
+
102
+ // ── Cost ──────────────────────────────────────────────────────────
103
+ cost: { type: costSchema, required: true },
104
+
105
+ // ── Failure detail (if status = failed) ───────────────────────────
106
+ error: { type: String },
107
+
108
+ // ── Catch-all for provider-specific raw data ───────────────────────
109
+ // e.g. WhatsApp pricing object, raw OpenAI usage response
110
+ raw: { type: Schema.Types.Mixed },
111
+
112
+ }, { timestamps: true });
113
+
114
+ // ─── Indexes ──────────────────────────────────────────────────────────────────
115
+
116
+ // Dashboard: "show me all logs for business X in date range"
117
+ BillingLogSchema.index({ business: 1, createdAt: -1 });
118
+
119
+ // Category breakdown: "show me all AI usage for business X"
120
+ BillingLogSchema.index({ business: 1, category: 1, createdAt: -1 });
121
+
122
+ // Conversation drill-down: "how much did this conversation cost?"
123
+ BillingLogSchema.index({ "source.conversation": 1 });
124
+
125
+ // Campaign cost totals
126
+ BillingLogSchema.index({ "source.campaign": 1 });
127
+
128
+ // Failed billing reconciliation
129
+ BillingLogSchema.index({ business: 1, status: 1 });
130
+
131
+ export const BillingLog = defineModel('BillingLog', BillingLogSchema, 'BillingLogs');
@@ -0,0 +1,34 @@
1
+ import { Schema } from "mongoose";
2
+ import { defineModel } from "../runtime.js";
3
+
4
+ const TranscriptSchema = new Schema({
5
+ speaker: { type: String, enum: ["Lead", "agent", "user", "system"] },
6
+ transcript: String,
7
+ startMs: Number, // offset from call start
8
+ endMs: Number,
9
+ confidence: Number,
10
+ source: String, // provider | whisper | deepgram | …
11
+ usage: Schema.Types.Mixed,
12
+ }, { _id: false });
13
+ const CallSessionSchema = new Schema({
14
+ lead: { type: Schema.Types.ObjectId, ref: 'Lead' },
15
+ conversation: { type: Schema.Types.ObjectId, ref: 'Conversation' },
16
+ campaign: Schema.Types.ObjectId, // participant of the campaign(bulk messaging)
17
+ business: { type: Schema.Types.ObjectId, ref: 'Businesses' },
18
+ channel: { type: Schema.Types.ObjectId, ref: "Channel" },
19
+ agent: { type: Schema.Types.ObjectId, ref: 'Agent' },
20
+ externalCallSessionId: String,
21
+ direction: String,
22
+ statusTimeline: {
23
+ scheduledAt: Date, initiatedAt: Date, ringingAt: Date, in_progressAt: Date, completedAt: Date, failedAt: Date, busyAt: Date, no_answerAt: Date, canceledAt: Date, rejectedAt: Date, durationSec: { type: Number, default: 0 },
24
+ },
25
+ recording: { status: { type: String, enum: ["none", "pending", "stored", "failed"], default: "none" }, key: String, url: String },
26
+ transcripts: { type: [TranscriptSchema], default: [] }, // see note in chat re: very long calls
27
+ callDetails: Schema.Types.Mixed,
28
+ usage: { type: Schema.Types.Mixed, default: {} },
29
+ sequenceOfEvents: [Schema.Types.Mixed],
30
+ isSummarized: { type: Boolean, default: false }
31
+ }, {
32
+ timestamps: true
33
+ });
34
+ export const CallSession = defineModel('CallSession', CallSessionSchema, 'CallSession');