@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 ADDED
@@ -0,0 +1,7 @@
1
+ UNLICENSED
2
+
3
+ Copyright (c) Campus Root. All rights reserved.
4
+
5
+ This software is proprietary. Publishing a package to the public npm registry
6
+ does not grant a license to use, copy, modify, or redistribute it. Contact
7
+ Campus Root for permission.
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,3 @@
1
+ export * from "./models/index.js";
2
+ export * from "./enums/index.js";
3
+ export { configureSchemas, defineModel, getSchemaConfig } from "./runtime.js";
@@ -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
+ }