@avakado.ai/schemas 1.0.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/LICENSE +7 -0
- package/README.md +96 -0
- package/package.json +44 -0
- package/src/enums/index.js +64 -0
- package/src/index.js +3 -0
- package/src/models/Business.js +37 -0
- package/src/models/Campaign.js +69 -0
- package/src/models/Conversations.js +97 -0
- package/src/models/Payments.js +38 -0
- package/src/models/Subscriptions.js +66 -0
- package/src/models/Triggers.js +17 -0
- package/src/models/UsageLogs.js +32 -0
- package/src/models/index.js +7 -0
- package/src/runtime.js +45 -0
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @avakado.ai/schemas
|
|
2
|
+
|
|
3
|
+
The canonical Mongo schemas, models, enums and index declarations for the Avakado platform. Four
|
|
4
|
+
services share one database; this package makes them share one definition of it.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @avakado.ai/schemas
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`mongoose` is a **peer** dependency (`^8.0.0`). The service owns the mongoose instance and the
|
|
11
|
+
connection; this package only registers models on it.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import mongoose from "mongoose";
|
|
17
|
+
import { configureSchemas } from "@avakado.ai/schemas";
|
|
18
|
+
import { sendKafkaMessage } from "./src/utils/kafka.js";
|
|
19
|
+
|
|
20
|
+
// once, at startup, before any model method runs
|
|
21
|
+
configureSchemas({ sendKafkaMessage });
|
|
22
|
+
await mongoose.connect(process.env.MONGO_URI);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { Business, Payment, Subscription, UsageLog, Conversation, Campaign, Task } from "@avakado.ai/schemas";
|
|
27
|
+
import { SUBSCRIPTION_STATUS, CURRENT_SUBSCRIPTION_STATUSES } from "@avakado.ai/schemas/enums";
|
|
28
|
+
import { Payment } from "@avakado.ai/schemas/models/Payments.js"; // narrower import, same registration
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Every entry point returns the **same** registered model. Importing a model twice — via the barrel and
|
|
32
|
+
via a subpath, or from two places in a dependency tree — reuses the existing registration instead of
|
|
33
|
+
throwing `OverwriteModelError`.
|
|
34
|
+
|
|
35
|
+
## What is in v1
|
|
36
|
+
|
|
37
|
+
| Model | Collection | Notes |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `Business` | `Businesses` | `credits` is the nested canonical shape; only socketio-server writes `credits.balance` |
|
|
40
|
+
| `Payment` | `Payments` | Razorpay ids live here; there is no `Invoices` collection |
|
|
41
|
+
| `Subscription` | `Subscriptions` | owns the unique partial index `one_current_subscription_per_business` |
|
|
42
|
+
| `UsageLog` | `UsageLogs` | the credit ledger; unique `idempotencyKey` |
|
|
43
|
+
| `Conversation` | `Conversations` | includes `updateStatus`, which needs `sendKafkaMessage` |
|
|
44
|
+
| `Campaign` / `Task` | `Campaign` / `Tasks` | includes `updateTimeLine`; the task executor stays in socketio-server |
|
|
45
|
+
| `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`.
|
|
55
|
+
|
|
56
|
+
## Behaviour that needs the host service
|
|
57
|
+
|
|
58
|
+
A package cannot import a service's `utils/`, so anything a shared model needs is injected once via
|
|
59
|
+
`configureSchemas`:
|
|
60
|
+
|
|
61
|
+
| Option | Used by | If missing |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| `sendKafkaMessage` | `Conversation.updateStatus` | throws a named error telling you to call `configureSchemas` |
|
|
64
|
+
|
|
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:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
import { Task } from "@avakado.ai/schemas";
|
|
70
|
+
Task.prototype.trigger = async function () { /* socketio-only executor */ };
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Fields are the cross-service contract. Behaviour is a role, and roles differ: socketio-server is the
|
|
74
|
+
only ledger writer, Lambda is the only service that advances campaign state.
|
|
75
|
+
|
|
76
|
+
## Deliberate behaviour changes in v1
|
|
77
|
+
|
|
78
|
+
- **`Conversation.updateStatus` saves before it publishes.** The per-service copies published the
|
|
79
|
+
summarisation job and the customer's workflow triggers *first*, so a status that failed validation
|
|
80
|
+
still dispatched work for a change that never persisted.
|
|
81
|
+
- **`updateStatus()` with no argument throws** instead of quietly unsetting `status`.
|
|
82
|
+
- **`credits.lastUpdated` defaults to `Date.now`**, not `new Date()` evaluated once at import — the
|
|
83
|
+
old form gave every document the timestamp of process start.
|
|
84
|
+
|
|
85
|
+
## Index changes need a migration
|
|
86
|
+
|
|
87
|
+
Mongo will not redefine an index that already exists under the same name with different options; it
|
|
88
|
+
keeps the old one and the `createIndex` call fails, which `autoIndex` swallows. When an index spec in
|
|
89
|
+
this package changes, each environment needs an explicit drop first:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
await db.collection("Subscriptions").dropIndex("one_current_subscription_per_business");
|
|
93
|
+
await Subscription.createIndexes();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
That is why an index or enum change is a major version bump.
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@avakado.ai/schemas",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Canonical Mongo schemas, models, enums and index declarations shared by the Avakado services.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./models": "./src/models/index.js",
|
|
10
|
+
"./models/*": "./src/models/*",
|
|
11
|
+
"./enums": "./src/enums/index.js",
|
|
12
|
+
"./runtime": "./src/runtime.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"mongoose": "^8.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"mongoose": "^8.19.2"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "node --test test/*.test.js"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/Campus-Root/avakado-shared.git",
|
|
37
|
+
"directory": "packages/schemas"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"avakado",
|
|
41
|
+
"mongoose",
|
|
42
|
+
"schemas"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Enums are exported on their own because more of this platform's cross-service bugs lived in enum
|
|
2
|
+
// drift than in field lists: a status one service wrote and another rejected, a ledger source type
|
|
3
|
+
// missing from a validator, a partial index filtering on a status list that had fallen behind.
|
|
4
|
+
// Anything that validates or filters on these values must read them from here.
|
|
5
|
+
|
|
6
|
+
export const SUBSCRIPTION_STATUS = [
|
|
7
|
+
"created",
|
|
8
|
+
"pending_payment",
|
|
9
|
+
"authenticated",
|
|
10
|
+
"active",
|
|
11
|
+
"pending",
|
|
12
|
+
"pending_downgrade",
|
|
13
|
+
"cancel_at_period_end",
|
|
14
|
+
"paused",
|
|
15
|
+
"halted",
|
|
16
|
+
"cancelled",
|
|
17
|
+
"expired",
|
|
18
|
+
"completed",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/** Statuses that make a subscription "the current one" for a business. Feeds the unique partial index. */
|
|
22
|
+
export const CURRENT_SUBSCRIPTION_STATUSES = [
|
|
23
|
+
"created",
|
|
24
|
+
"pending_payment",
|
|
25
|
+
"authenticated",
|
|
26
|
+
"active",
|
|
27
|
+
"pending",
|
|
28
|
+
"pending_downgrade",
|
|
29
|
+
"cancel_at_period_end",
|
|
30
|
+
"paused",
|
|
31
|
+
"halted",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export const PENDING_CHANGE_TYPES = ["upgrade", "downgrade", "cancel"];
|
|
35
|
+
|
|
36
|
+
export const PAYMENT_STATUS = ["authorized", "captured", "failed", "refunded"];
|
|
37
|
+
|
|
38
|
+
export const LEDGER_DIRECTIONS = ["credit", "debit", "reset"];
|
|
39
|
+
export const LEDGER_STATUSES = ["posted", "pending", "failed", "reversed"];
|
|
40
|
+
export const LEDGER_SOURCE_TYPES = ["Message", "CallSession", "Subscription", "Payment", "Collection"];
|
|
41
|
+
|
|
42
|
+
export const LEDGER_CATEGORY = {
|
|
43
|
+
PLAN_GRANT: "plan.grant",
|
|
44
|
+
TOPUP_GRANT: "topup.grant",
|
|
45
|
+
UPGRADE_GRANT: "upgrade.grant",
|
|
46
|
+
REFUND: "refund",
|
|
47
|
+
ADJUSTMENT: "adjustment",
|
|
48
|
+
AI_LLM: "ai.llm",
|
|
49
|
+
AI_TRANSCRIPTION: "ai.transcription",
|
|
50
|
+
AI_EMBEDDING: "ai.embedding",
|
|
51
|
+
MESSAGING_WHATSAPP: "messaging.whatsapp",
|
|
52
|
+
MESSAGING_SMS: "messaging.sms",
|
|
53
|
+
PLATFORM_STORAGE: "platform.storage",
|
|
54
|
+
PLATFORM_API: "platform.api",
|
|
55
|
+
THIRD_PARTY: "third_party",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const CONVERSATION_STATUS = ["open", "pending", "snoozed", "completed", "closed", "archived", "spam"];
|
|
59
|
+
export const CONVERSATION_PRIORITY = ["low", "normal", "high", "urgent"];
|
|
60
|
+
|
|
61
|
+
export const CAMPAIGN_STATUS = ["pending", "active", "completed"];
|
|
62
|
+
export const TASK_STATUS = ["pending", "in-progress", "completed", "failed", "skipped"];
|
|
63
|
+
export const TASK_TYPES = ["quick", "webhook"];
|
|
64
|
+
export const TASK_REFERENCE_TYPES = ["Message", "CallSession"];
|
package/src/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Schema } from "mongoose";
|
|
2
|
+
import { defineModel } from "../runtime.js";
|
|
3
|
+
|
|
4
|
+
const BusinessSchema = new Schema({
|
|
5
|
+
name: String,
|
|
6
|
+
logoURL: String,
|
|
7
|
+
facts: [String],
|
|
8
|
+
quickQuestions: [{ label: String, value: String }],
|
|
9
|
+
sector: String,
|
|
10
|
+
tagline: String,
|
|
11
|
+
address: String,
|
|
12
|
+
description: String,
|
|
13
|
+
MAX_DAYS: { type: Number, default: 45 },
|
|
14
|
+
contact: {
|
|
15
|
+
mail: String,
|
|
16
|
+
phone: String,
|
|
17
|
+
website: String
|
|
18
|
+
},
|
|
19
|
+
createdBy: { type: Schema.Types.ObjectId, ref: "Users" },
|
|
20
|
+
documents: [{ type: Schema.Types.ObjectId, ref: "document" }],
|
|
21
|
+
// The credit balance. Only socketio-server writes `balance` — it is derived from the UsageLogs
|
|
22
|
+
// ledger, so treat it as a cache of the ledger and never increment it directly.
|
|
23
|
+
credits: {
|
|
24
|
+
freeTrailClaimed: { type: Boolean, default: false },
|
|
25
|
+
freeTrailExpiry: { type: Date },
|
|
26
|
+
currentSubscription: { type: Schema.Types.ObjectId, ref: "Subscriptions" },
|
|
27
|
+
active: { type: Boolean, default: true },
|
|
28
|
+
balance: { type: Number, default: 0, min: 0 },
|
|
29
|
+
carryForward: { type: Number, default: 0, min: 0 },
|
|
30
|
+
lastUpdated: { type: Date, default: Date.now }
|
|
31
|
+
}
|
|
32
|
+
}, {
|
|
33
|
+
timestamps: true
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export { BusinessSchema };
|
|
37
|
+
export const Business = defineModel("Businesses", BusinessSchema, "Businesses");
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Schema } from "mongoose";
|
|
2
|
+
import { defineModel } from "../runtime.js";
|
|
3
|
+
import { CAMPAIGN_STATUS, TASK_REFERENCE_TYPES, TASK_STATUS, TASK_TYPES } from "../enums/index.js";
|
|
4
|
+
|
|
5
|
+
const TaskSchema = new Schema({
|
|
6
|
+
business: { type: Schema.Types.ObjectId, ref: 'Businesses', required: true },
|
|
7
|
+
campaign: { type: Schema.Types.ObjectId, ref: 'Campaign' },
|
|
8
|
+
lead: { type: Schema.Types.ObjectId, ref: "Lead" },
|
|
9
|
+
status: { type: String, enum: TASK_STATUS, default: "pending" },
|
|
10
|
+
timeLine: {
|
|
11
|
+
startedAt: Date,// start of task
|
|
12
|
+
failedAt: Date,// failed of task
|
|
13
|
+
completedAt: Date,// end of task
|
|
14
|
+
},
|
|
15
|
+
attempts: { type: Number, default: 1 },
|
|
16
|
+
type: { type: String, enum: TASK_TYPES, required: true },
|
|
17
|
+
data: { type: Schema.Types.Mixed, default: null },
|
|
18
|
+
error: { type: Schema.Types.Mixed, default: null },
|
|
19
|
+
response: { type: Schema.Types.Mixed, default: null },
|
|
20
|
+
references: { type: { type: String, enum: TASK_REFERENCE_TYPES }, id: { type: Schema.Types.ObjectId, refPath: "references.type" } },
|
|
21
|
+
}, { timestamps: true });
|
|
22
|
+
|
|
23
|
+
TaskSchema.methods.updateTimeLine = async function (timeLine, currentStatus) {
|
|
24
|
+
this.status = currentStatus;
|
|
25
|
+
this.timeLine[timeLine] = new Date();
|
|
26
|
+
return this.save();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
TaskSchema.methods.updateError = async function (error) {
|
|
30
|
+
this.error = error;
|
|
31
|
+
return this.save();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
TaskSchema.methods.updateResponse = async function (response) {
|
|
35
|
+
this.response = response;
|
|
36
|
+
return this.save();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const CampaignSchema = new Schema({
|
|
40
|
+
name: String,
|
|
41
|
+
business: { type: Schema.Types.ObjectId, ref: 'Businesses', required: true },
|
|
42
|
+
channel: { type: Schema.Types.ObjectId, ref: "Channel", required: true },
|
|
43
|
+
leads: [{ type: Schema.Types.ObjectId, ref: "Lead" }],
|
|
44
|
+
config: { type: Schema.Types.Mixed, default: null },
|
|
45
|
+
status: { type: String, enum: CAMPAIGN_STATUS, default: "pending" },
|
|
46
|
+
timeLines: {
|
|
47
|
+
scheduledAt: Date,// scheduled time
|
|
48
|
+
startedAt: Date,// start of chain
|
|
49
|
+
completedAt: Date,// end of chain
|
|
50
|
+
cancelledAt: Date, // breaking the chain
|
|
51
|
+
lastTaskStartedAt: Date, // last task started time
|
|
52
|
+
},
|
|
53
|
+
cancel_requested: { type: Boolean, default: false }, // if true, the next task will be cancelled
|
|
54
|
+
createdBy: { type: Schema.Types.ObjectId, ref: 'Users', required: true },
|
|
55
|
+
}, { timestamps: true });
|
|
56
|
+
|
|
57
|
+
CampaignSchema.methods.updateTimeLine = async function (timeLine, currentStatus) {
|
|
58
|
+
this.timeLines[timeLine] = new Date();
|
|
59
|
+
this.status = currentStatus;
|
|
60
|
+
return this.save();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export { CampaignSchema, TaskSchema };
|
|
64
|
+
export const Campaign = defineModel('Campaign', CampaignSchema, 'Campaign');
|
|
65
|
+
export const Task = defineModel('Task', TaskSchema, 'Tasks');
|
|
66
|
+
|
|
67
|
+
// `Task.trigger` and `Task.updateReference` stay in socketio-server: they need its RealtimeServer and
|
|
68
|
+
// the Message / CallSession models, which are not in this package yet. socketio attaches them with
|
|
69
|
+
// `Task.prototype.trigger = fn`, which is why the executor is not declared here.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Schema } from "mongoose";
|
|
2
|
+
import { defineModel, requireDep } from "../runtime.js";
|
|
3
|
+
import { CONVERSATION_PRIORITY, CONVERSATION_STATUS } from "../enums/index.js";
|
|
4
|
+
import { Trigger } from "./Triggers.js";
|
|
5
|
+
|
|
6
|
+
const ConversationSchema = new Schema({
|
|
7
|
+
agent: { type: Schema.Types.ObjectId, ref: 'Agent' }, // the AI agent
|
|
8
|
+
business: { type: Schema.Types.ObjectId, ref: 'Businesses' },
|
|
9
|
+
channel: { type: Schema.Types.ObjectId, ref: "Channel" },
|
|
10
|
+
lead: { type: Schema.Types.ObjectId, ref: "Lead" }, // from sender of the inbound message / call or recipient of the outbound message / call
|
|
11
|
+
campaign: { type: Schema.Types.ObjectId, ref: "Campaign" }, // participant of the campaign(bulk messaging)
|
|
12
|
+
// Deterministic provider thread key used to find-or-create on every inbound
|
|
13
|
+
// event. e.g. WhatsApp: contact wa_id · Telegram: chat.id · Messenger: PSID.
|
|
14
|
+
externalConversationId: { type: String },
|
|
15
|
+
// config is the setting that are applied to the conversation,
|
|
16
|
+
config: {
|
|
17
|
+
// Human/agent routing — promoted to top level so the agent-inbox index can use it.
|
|
18
|
+
assignment: {
|
|
19
|
+
agentReply: { type: Boolean, default: true },
|
|
20
|
+
handoffReason: String,
|
|
21
|
+
handoffUrgency: String,
|
|
22
|
+
// team: { type: Schema.Types.ObjectId, ref: "Team" },
|
|
23
|
+
assignedAt: Date,
|
|
24
|
+
assignedTo: String // for support/agent routing in case of support ping to business notifications else give it to agent,
|
|
25
|
+
},
|
|
26
|
+
tags: [String],
|
|
27
|
+
// more settings to be added here
|
|
28
|
+
},
|
|
29
|
+
status: { type: String, enum: CONVERSATION_STATUS, default: "open" },
|
|
30
|
+
priority: { type: String, enum: CONVERSATION_PRIORITY, default: "normal" },
|
|
31
|
+
metadata: {
|
|
32
|
+
openai: { "lastResponseId": String, "lastResponseAt": Date },
|
|
33
|
+
extractedData: Schema.Types.Mixed,
|
|
34
|
+
userLocation: Schema.Types.Mixed,
|
|
35
|
+
CreditsUsage: {
|
|
36
|
+
conversationCredits: { type: Number, default: 0 },
|
|
37
|
+
analysisCredits: { type: Number, default: 0 },
|
|
38
|
+
// knowledgeCredits:Number,
|
|
39
|
+
miscellaneousCredits: { type: Number, default: 0 },
|
|
40
|
+
totalCredits: { type: Number, default: 0 },
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}, {
|
|
44
|
+
timestamps: true
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Moves the conversation to `status`, then announces it.
|
|
49
|
+
*
|
|
50
|
+
* The save happens before any publish on purpose. The per-service copies of this method published
|
|
51
|
+
* the summarisation job and the customer's workflow triggers first, so a status that failed to
|
|
52
|
+
* validate still dispatched work for a change that never persisted.
|
|
53
|
+
*/
|
|
54
|
+
ConversationSchema.methods.updateStatus = async function (status) {
|
|
55
|
+
if (!status) throw new Error("@avakado.ai/schemas: Conversation.updateStatus(status) requires a status");
|
|
56
|
+
const sendKafkaMessage = requireDep("sendKafkaMessage", "Conversation.updateStatus");
|
|
57
|
+
|
|
58
|
+
this.status = status;
|
|
59
|
+
const saved = await this.save();
|
|
60
|
+
|
|
61
|
+
// `business` may or may not be populated depending on the caller.
|
|
62
|
+
const businessId = String(this.business?._id ?? this.business ?? "");
|
|
63
|
+
const conversationId = this._id.toString();
|
|
64
|
+
|
|
65
|
+
await sendKafkaMessage({
|
|
66
|
+
topic: 'socket-event',
|
|
67
|
+
message: {
|
|
68
|
+
key: conversationId,
|
|
69
|
+
value: JSON.stringify({ event: "conversation.statusUpdated", payload: { conversationId, status }, nameSpace: "CONVERSATION", roomId: businessId }),
|
|
70
|
+
},
|
|
71
|
+
acks: -1
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (status === "completed") {
|
|
75
|
+
await sendKafkaMessage({
|
|
76
|
+
topic: 'agentic-data-summarisation',
|
|
77
|
+
message: { key: 'conversationStatusUpdated', value: JSON.stringify({ conversationId }) },
|
|
78
|
+
acks: -1
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (businessId) {
|
|
83
|
+
const trigger = await Trigger.findOne({ business: businessId, name: "conversationStatusUpdated", type: status });
|
|
84
|
+
for (const workflow of trigger?.workflows ?? []) {
|
|
85
|
+
await sendKafkaMessage({
|
|
86
|
+
topic: 'workflow-execution-trigger',
|
|
87
|
+
message: { key: workflow.toString(), value: JSON.stringify({ conversationId }) },
|
|
88
|
+
acks: -1
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return saved;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export { ConversationSchema };
|
|
97
|
+
export const Conversation = defineModel('Conversation', ConversationSchema, "Conversations");
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Schema } from "mongoose";
|
|
2
|
+
import { defineModel } from "../runtime.js";
|
|
3
|
+
import { PAYMENT_STATUS } from "../enums/index.js";
|
|
4
|
+
|
|
5
|
+
const AmountSchema = new Schema({
|
|
6
|
+
value: { type: Number, required: true }, // eg: 22000
|
|
7
|
+
currency: { type: String, default: "INR" }
|
|
8
|
+
}, { _id: false });
|
|
9
|
+
|
|
10
|
+
// One document per money event. Razorpay is the system of record for the money itself, so we keep
|
|
11
|
+
// its ids here instead of a separate Invoices collection. Credits granted for a payment are posted
|
|
12
|
+
// to the ledger by socketio-server, which reads `notes.credits`.
|
|
13
|
+
const PaymentSchema = new Schema({
|
|
14
|
+
business: { type: Schema.Types.ObjectId, ref: 'Businesses', required: true },
|
|
15
|
+
subscription: { type: Schema.Types.ObjectId, ref: 'Subscriptions' },
|
|
16
|
+
gateway: String,
|
|
17
|
+
gatewayReference: Schema.Types.Mixed, //{ paymentId: String, orderId: String, invoiceId: String },
|
|
18
|
+
gatewayPaymentId: { type: String, index: true, sparse: true },
|
|
19
|
+
gatewayInvoiceId: String, // Razorpay invoice id (inv_xxx) for a subscription cycle; fetch the hosted PDF from Razorpay with it
|
|
20
|
+
amount: AmountSchema,
|
|
21
|
+
notes: Schema.Types.Mixed, // { planId, planCode, action, credits }
|
|
22
|
+
events: {
|
|
23
|
+
authorized: Date,
|
|
24
|
+
captured: Date,
|
|
25
|
+
failed: Date,
|
|
26
|
+
refunded: Date
|
|
27
|
+
},
|
|
28
|
+
status: { type: String, enum: PAYMENT_STATUS, default: 'authorized' },
|
|
29
|
+
failureReason: String,
|
|
30
|
+
retryCount: { type: Number, default: 0 },
|
|
31
|
+
paidAt: Date
|
|
32
|
+
}, {
|
|
33
|
+
timestamps: true,
|
|
34
|
+
versionKey: false
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export { AmountSchema, PaymentSchema };
|
|
38
|
+
export const Payment = defineModel('Payments', PaymentSchema, "Payments");
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Schema } from "mongoose";
|
|
2
|
+
import { defineModel } from "../runtime.js";
|
|
3
|
+
import { AmountSchema } from "./Payments.js";
|
|
4
|
+
import { CURRENT_SUBSCRIPTION_STATUSES, PENDING_CHANGE_TYPES, SUBSCRIPTION_STATUS } from "../enums/index.js";
|
|
5
|
+
|
|
6
|
+
const SubscriptionSchema = new Schema({
|
|
7
|
+
business: { type: Schema.Types.ObjectId, ref: 'Businesses', required: true, index: true },
|
|
8
|
+
createdBy: { type: Schema.Types.ObjectId, ref: 'Users' },
|
|
9
|
+
plan: { type: Schema.Types.ObjectId, ref: 'Plans', required: true },
|
|
10
|
+
planCode: { type: String, required: true, index: true },
|
|
11
|
+
gateway: { type: String, enum: ['razorpay', 'none'], default: 'razorpay' },
|
|
12
|
+
gatewaySubscriptionId: { type: String },
|
|
13
|
+
status: { type: String, enum: SUBSCRIPTION_STATUS, default: 'created', index: true },
|
|
14
|
+
amount: AmountSchema,
|
|
15
|
+
creditsPerCycle: { type: Number, default: 0 },
|
|
16
|
+
spendRatio: { type: Number, enum: [1080, 1666, 1583], default: 1583 },
|
|
17
|
+
billing: {
|
|
18
|
+
periodStart: Date,
|
|
19
|
+
periodEnd: Date,
|
|
20
|
+
nextChargeAt: Date,
|
|
21
|
+
paidCount: { type: Number, default: 0 },
|
|
22
|
+
totalCount: Number
|
|
23
|
+
},
|
|
24
|
+
pendingChange: {
|
|
25
|
+
type: { type: String, enum: PENDING_CHANGE_TYPES },
|
|
26
|
+
targetPlan: { type: Schema.Types.ObjectId, ref: 'Plans' },
|
|
27
|
+
targetPlanCode: String,
|
|
28
|
+
applyAt: Date,
|
|
29
|
+
chargeAmount: Number,
|
|
30
|
+
creditDelta: Number,
|
|
31
|
+
orderId: String
|
|
32
|
+
},
|
|
33
|
+
cancelAtPeriodEnd: { type: Boolean, default: false },
|
|
34
|
+
cancelledAt: Date,
|
|
35
|
+
cancelReason: String,
|
|
36
|
+
startedAt: Date,
|
|
37
|
+
endedAt: Date,
|
|
38
|
+
shortUrl: String,
|
|
39
|
+
creditGrant: {
|
|
40
|
+
lastCycle: { type: Number, default: 0 },
|
|
41
|
+
lastGrantedAt: Date,
|
|
42
|
+
lastPaymentId: String
|
|
43
|
+
},
|
|
44
|
+
gatewayPayload: Schema.Types.Mixed
|
|
45
|
+
}, {
|
|
46
|
+
timestamps: true,
|
|
47
|
+
versionKey: false
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
SubscriptionSchema.index({ business: 1, createdAt: -1 });
|
|
51
|
+
SubscriptionSchema.index({ business: 1, status: 1, createdAt: -1 });
|
|
52
|
+
SubscriptionSchema.index({ gatewaySubscriptionId: 1 }, { unique: true, sparse: true });
|
|
53
|
+
// Mongo cannot redefine an index under an existing name with different options — it keeps the old
|
|
54
|
+
// one and the createIndex call fails. Changing this filter is a MAJOR bump, and each environment
|
|
55
|
+
// needs `dropIndex('one_current_subscription_per_business')` before the new spec can be built.
|
|
56
|
+
SubscriptionSchema.index(
|
|
57
|
+
{ business: 1 },
|
|
58
|
+
{
|
|
59
|
+
unique: true,
|
|
60
|
+
name: 'one_current_subscription_per_business',
|
|
61
|
+
partialFilterExpression: { status: { $in: CURRENT_SUBSCRIPTION_STATUSES } }
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
export { SubscriptionSchema };
|
|
66
|
+
export const Subscription = defineModel('Subscriptions', SubscriptionSchema, "Subscriptions");
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Schema } from "mongoose";
|
|
2
|
+
import { defineModel } from "../runtime.js";
|
|
3
|
+
|
|
4
|
+
const TriggerSchema = new Schema({
|
|
5
|
+
business: { type: Schema.Types.ObjectId, ref: 'Businesses' },
|
|
6
|
+
name: String, // conversationStatusUpdated, leadStatusUpdated,
|
|
7
|
+
description: String,
|
|
8
|
+
type: String, // [open, pending, snoozed, completed, closed, archived, spam] || [new, contacted, qualified, converted, lost]
|
|
9
|
+
workflows: [{ type: Schema.Types.ObjectId, ref: "Workflow" }],
|
|
10
|
+
}, {
|
|
11
|
+
timestamps: true,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export { TriggerSchema };
|
|
15
|
+
// No explicit collection name, matching the services: mongoose pluralises this to `triggers`.
|
|
16
|
+
export const Trigger = defineModel('Trigger', TriggerSchema);
|
|
17
|
+
export default Trigger;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Schema } from "mongoose";
|
|
2
|
+
import { defineModel } from "../runtime.js";
|
|
3
|
+
import { LEDGER_DIRECTIONS, LEDGER_SOURCE_TYPES, LEDGER_STATUSES } from "../enums/index.js";
|
|
4
|
+
|
|
5
|
+
// The credit ledger, and the only source of truth for credit movement. socketio-server is its only
|
|
6
|
+
// writer; every other service asks for a ledger entry over Kafka. `Business.credits.balance` is a
|
|
7
|
+
// cache of the running total here.
|
|
8
|
+
const UsageLogSchema = new Schema({
|
|
9
|
+
business: { type: Schema.Types.ObjectId, ref: "Businesses", required: true, index: true },
|
|
10
|
+
direction: { type: String, enum: LEDGER_DIRECTIONS, required: true },
|
|
11
|
+
credits: { type: Number, required: true, min: 0 },
|
|
12
|
+
status: { type: String, enum: LEDGER_STATUSES, default: "posted" },
|
|
13
|
+
payment: { type: Schema.Types.ObjectId, ref: "Payments" },
|
|
14
|
+
source: {
|
|
15
|
+
type: { type: String, enum: LEDGER_SOURCE_TYPES },
|
|
16
|
+
id: { type: Schema.Types.ObjectId, refPath: "source.type" },
|
|
17
|
+
},
|
|
18
|
+
idempotencyKey: { type: String },
|
|
19
|
+
meta: Schema.Types.Mixed,
|
|
20
|
+
note: String,
|
|
21
|
+
createdBy: { type: Schema.Types.ObjectId, ref: "Users" }
|
|
22
|
+
}, {
|
|
23
|
+
timestamps: true,
|
|
24
|
+
versionKey: false
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
UsageLogSchema.index({ business: 1, direction: 1, createdAt: -1 });
|
|
28
|
+
// The idempotency guarantee for every credit movement. Replaying a Kafka message must not double-post.
|
|
29
|
+
UsageLogSchema.index({ idempotencyKey: 1 }, { unique: true, sparse: true });
|
|
30
|
+
|
|
31
|
+
export { UsageLogSchema };
|
|
32
|
+
export const UsageLog = defineModel("UsageLog", UsageLogSchema, "UsageLogs");
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { Business, BusinessSchema } from "./Business.js";
|
|
2
|
+
export { AmountSchema, Payment, PaymentSchema } from "./Payments.js";
|
|
3
|
+
export { Subscription, SubscriptionSchema } from "./Subscriptions.js";
|
|
4
|
+
export { UsageLog, UsageLogSchema } from "./UsageLogs.js";
|
|
5
|
+
export { Conversation, ConversationSchema } from "./Conversations.js";
|
|
6
|
+
export { Campaign, CampaignSchema, Task, TaskSchema } from "./Campaign.js";
|
|
7
|
+
export { Trigger, TriggerSchema } from "./Triggers.js";
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Registers a model once per mongoose instance.
|
|
5
|
+
*
|
|
6
|
+
* Importing the same model file twice — two copies of this package on disk, a test that reloads a
|
|
7
|
+
* module, a service that imports both `@avakado.ai/schemas` and `@avakado.ai/schemas/models/Business.js` —
|
|
8
|
+
* would otherwise throw `OverwriteModelError`. Reusing the already-registered model also guarantees
|
|
9
|
+
* one model per name, which is what `populate` resolves refs against.
|
|
10
|
+
*/
|
|
11
|
+
export function defineModel(name, schema, collection) {
|
|
12
|
+
return mongoose.models[name] ?? mongoose.model(name, schema, collection);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Host-supplied capabilities. The package never imports a service's utils, so anything a shared
|
|
17
|
+
* model needs from its host arrives here at startup instead.
|
|
18
|
+
*/
|
|
19
|
+
const deps = {
|
|
20
|
+
/** ({ topic, message, acks }) => Promise<void> — the service's Kafka producer. */
|
|
21
|
+
sendKafkaMessage: null,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function configureSchemas(next = {}) {
|
|
25
|
+
for (const [key, value] of Object.entries(next)) {
|
|
26
|
+
if (!(key in deps)) throw new Error(`@avakado.ai/schemas: unknown option "${key}" passed to configureSchemas`);
|
|
27
|
+
deps[key] = value;
|
|
28
|
+
}
|
|
29
|
+
return { ...deps };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getSchemaConfig() {
|
|
33
|
+
return { ...deps };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function requireDep(name, neededBy) {
|
|
37
|
+
const value = deps[name];
|
|
38
|
+
if (typeof value !== "function") {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`@avakado.ai/schemas: "${name}" is not configured but ${neededBy} needs it. ` +
|
|
41
|
+
`Call configureSchemas({ ${name} }) once during service startup.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|