payservedb 9.0.0 → 9.0.2

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/index.js CHANGED
@@ -259,8 +259,76 @@ const models = {
259
259
  MoveinLandlord: require("./src/models/movein_landlord"),
260
260
  MoveinUser: require("./src/models/movein_user"),
261
261
  InvoiceWithholdingTax: require("./src/models/InvoiceWithholdingTax"),
262
+ // Customer Obsession admin-managed config
263
+ EmailCcConfig: require("./src/models/email_cc_config"),
264
+ AutoReplyRule: require("./src/models/auto_reply_rule"),
265
+ RecipientGroup: require("./src/models/recipient_group"),
266
+ RecipientGroupMember: require("./src/models/recipient_group_member"),
262
267
  };
263
268
 
269
+ // ── Move-In platform model schema sources (keyed by model name) ─────────────
270
+ const moveInModelDefs = {
271
+ MoveInUser: { file: "./src/models/movein_user", name: "MoveInUser" },
272
+ MoveInUnit: { file: "./src/models/movein_unit", name: "MoveInUnit" },
273
+ MoveInLandlord: { file: "./src/models/movein_landlord", name: "MoveInLandlord" },
274
+ MoveInApplication: { file: "./src/models/movein_application", name: "MoveInApplication" },
275
+ CustomerPreference: { file: "./src/models/customer_preference", name: "CustomerPreference" },
276
+ MoveInViewingSlot: { file: "./src/models/movein_viewing_slot", name: "MoveInViewingSlot" },
277
+ MoveInBooking: { file: "./src/models/movein_booking", name: "MoveInBooking" },
278
+ MoveInReservation: { file: "./src/models/movein_reservation", name: "MoveInReservation" },
279
+ MoveInPayment: { file: "./src/models/movein_payment", name: "MoveInPayment" },
280
+ MoveInDeal: { file: "./src/models/movein_deal", name: "MoveInDeal" },
281
+ MoveInCommission: { file: "./src/models/movein_commission", name: "MoveInCommission" },
282
+ MoveInNotification: { file: "./src/models/movein_notification", name: "MoveInNotification" },
283
+ MoveInAuditLog: { file: "./src/models/movein_audit_log", name: "MoveInAuditLog" },
284
+ MoveInOtp: { file: "./src/models/movein_otp", name: "MoveInOtp" },
285
+ MoveInConversation: { file: "./src/models/movein_conversation", name: "MoveInConversation" },
286
+ MoveInMessage: { file: "./src/models/movein_message", name: "MoveInMessage" },
287
+ MoveInLandlordUser: { file: "./src/models/movein_landlord_user", name: "MoveInLandlordUser" },
288
+ MoveInHandoffToken: { file: "./src/models/movein_handoff_token", name: "MoveInHandoffToken" },
289
+ };
290
+
291
+ // Initial moveIn namespace — models on default connection (payserve_property).
292
+ // connectToMoveInDB replaces these with models bound to payserve_movein.
293
+ const moveInModels = {};
294
+ for (const [key, def] of Object.entries(moveInModelDefs)) {
295
+ moveInModels[key] = require(def.file);
296
+ }
297
+
298
+ // Opens a dedicated connection to payserve_movein and re-registers all
299
+ // Move-In models on that connection so queries hit the correct database.
300
+ async function connectToMoveInDB(dbName, secured, username, password, url, port) {
301
+ try {
302
+ let connectionString;
303
+ if (secured === false) {
304
+ connectionString = `mongodb://${url}:${port}/${dbName}`;
305
+ } else {
306
+ const source = "?authSource=admin";
307
+ connectionString = `mongodb://${username}:${password}@${url}:${port}/${dbName}${source}`;
308
+ }
309
+
310
+ const conn = await mongoose.createConnection(connectionString, {
311
+ useNewUrlParser: true,
312
+ });
313
+
314
+ console.log(`Connected to Move-In DB: ${dbName}`);
315
+
316
+ for (const [key, def] of Object.entries(moveInModelDefs)) {
317
+ const baseModel = require(def.file);
318
+ const schema = baseModel.schema;
319
+ moveInModels[key] = conn.models[def.name]
320
+ ? conn.models[def.name]
321
+ : conn.model(def.name, schema);
322
+ }
323
+
324
+ module.exports.moveInConnection = conn;
325
+ return conn;
326
+ } catch (err) {
327
+ console.error("Error connecting to Move-In DB:", err);
328
+ throw err;
329
+ }
330
+ }
331
+
264
332
  // Function to get models dynamically from a specific database connection
265
333
  async function getModelFromDB(dbConnection, modelName, schema) {
266
334
  if (!dbConnection.models[modelName]) {
@@ -323,6 +391,7 @@ function initializeService(modelNames = []) {
323
391
 
324
392
  module.exports = {
325
393
  connectToMongoDB,
394
+ connectToMoveInDB,
326
395
  switchDB,
327
396
  getModelFromDB,
328
397
  initializeService,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payservedb",
3
- "version": "9.0.0",
3
+ "version": "9.0.2",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -0,0 +1,68 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ /**
4
+ * Admin-managed auto-reply rules. On every inbound email or WhatsApp
5
+ * message, the matcher (utils/auto_reply.js) walks enabled rules ordered by
6
+ * priority and fires the FIRST rule whose keyword appears in the body as a
7
+ * whole word (case-insensitive). Senders from @payserve.co.ke are skipped.
8
+ *
9
+ * Replaces the legacy per-agent immediate / delay UI that used to live in
10
+ * customer_obsession/settings/agent_settings.js (PR4).
11
+ */
12
+ const autoReplyRuleSchema = new mongoose.Schema({
13
+ channel: {
14
+ type: String,
15
+ enum: ['email', 'whatsapp'],
16
+ required: [true, 'channel is required'],
17
+ index: true,
18
+ },
19
+ keyword: {
20
+ type: String,
21
+ required: [true, 'keyword is required'],
22
+ trim: true,
23
+ maxlength: 100,
24
+ },
25
+ reply: {
26
+ type: String,
27
+ required: [true, 'reply text is required'],
28
+ trim: true,
29
+ maxlength: 1000,
30
+ },
31
+ priority: {
32
+ type: Number,
33
+ default: 0,
34
+ index: true,
35
+ },
36
+ enabled: {
37
+ type: Boolean,
38
+ default: true,
39
+ },
40
+ created_by: {
41
+ type: mongoose.Schema.Types.ObjectId,
42
+ ref: 'User',
43
+ },
44
+ updated_by: {
45
+ type: mongoose.Schema.Types.ObjectId,
46
+ ref: 'User',
47
+ },
48
+ created_at: {
49
+ type: Date,
50
+ default: Date.now,
51
+ },
52
+ updated_at: {
53
+ type: Date,
54
+ default: Date.now,
55
+ },
56
+ }, {
57
+ timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' },
58
+ });
59
+
60
+ // First-match-wins semantics — duplicate (channel, keyword) makes lower-priority
61
+ // rules unreachable. Enforced at the DB level.
62
+ autoReplyRuleSchema.index({ channel: 1, keyword: 1 }, {
63
+ unique: true,
64
+ collation: { locale: 'en', strength: 2 }, // case-insensitive
65
+ });
66
+ autoReplyRuleSchema.index({ channel: 1, priority: 1 });
67
+
68
+ module.exports = mongoose.model('AutoReplyRule', autoReplyRuleSchema);
@@ -0,0 +1,48 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ /**
4
+ * Admin-managed always-CC list for outbound agent-sent emails.
5
+ *
6
+ * Every enabled row is appended to the CC of every email reply / new send /
7
+ * bulk send that originates from an agent. Auto-replies bypass this list.
8
+ *
9
+ * Managed from core_main at /core/customer-obsession/email-cc-config. Agents
10
+ * read the enabled subset via /api/customer_obsession/settings/email-cc.
11
+ */
12
+ const emailCcConfigSchema = new mongoose.Schema({
13
+ address: {
14
+ type: String,
15
+ required: [true, 'Email address is required'],
16
+ trim: true,
17
+ lowercase: true,
18
+ maxlength: 254,
19
+ match: [/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'Invalid email address'],
20
+ },
21
+ enabled: {
22
+ type: Boolean,
23
+ default: true,
24
+ },
25
+ added_by: {
26
+ type: mongoose.Schema.Types.ObjectId,
27
+ ref: 'User',
28
+ },
29
+ updated_by: {
30
+ type: mongoose.Schema.Types.ObjectId,
31
+ ref: 'User',
32
+ },
33
+ created_at: {
34
+ type: Date,
35
+ default: Date.now,
36
+ },
37
+ updated_at: {
38
+ type: Date,
39
+ default: Date.now,
40
+ },
41
+ }, {
42
+ timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' },
43
+ });
44
+
45
+ emailCcConfigSchema.index({ address: 1 }, { unique: true });
46
+ emailCcConfigSchema.index({ enabled: 1 });
47
+
48
+ module.exports = mongoose.model('EmailCcConfig', emailCcConfigSchema);
@@ -0,0 +1,21 @@
1
+ const mongoose = require('mongoose');
2
+ const ObjectId = mongoose.Schema.Types.ObjectId;
3
+
4
+ const moveInAuditLogSchema = new mongoose.Schema(
5
+ {
6
+ adminId: { type: ObjectId, ref: 'User', default: null, index: true },
7
+ action: { type: String, required: true },
8
+ resourceType: {
9
+ type: String,
10
+ enum: ['listing', 'application', 'customer', 'landlord', 'reservation', 'payment', 'viewing', 'other'],
11
+ required: true,
12
+ },
13
+ resourceId: { type: ObjectId, default: null, index: true },
14
+ details: { type: String, default: null },
15
+ ipAddress: { type: String, default: null },
16
+ },
17
+ { timestamps: true }
18
+ );
19
+
20
+ const MoveInAuditLog = mongoose.model('MoveInAuditLog', moveInAuditLogSchema);
21
+ module.exports = MoveInAuditLog;
@@ -0,0 +1,33 @@
1
+ const mongoose = require('mongoose');
2
+ const ObjectId = mongoose.Schema.Types.ObjectId;
3
+
4
+ const moveInBookingSchema = new mongoose.Schema(
5
+ {
6
+ slotId: { type: ObjectId, ref: 'MoveInViewingSlot', default: null, index: true },
7
+ tenantId: { type: ObjectId, ref: 'MoveInUser', default: null, index: true },
8
+ tenantName: { type: String, default: null },
9
+ tenantEmail: { type: String, default: null },
10
+ tenantPhone: { type: String, default: null },
11
+ isGuest: { type: Boolean, default: false, index: true },
12
+ unitId: { type: ObjectId, required: true, index: true },
13
+ unitName: { type: String, default: null },
14
+ facilityId: { type: ObjectId, ref: 'Facility', default: null, index: true },
15
+ landlordId: { type: ObjectId, ref: 'User', default: null, index: true },
16
+ scheduledDate:{ type: Date, default: null },
17
+ scheduledTime:{ type: String, default: null },
18
+ status: {
19
+ type: String,
20
+ enum: ['pending', 'confirmed', 'cancelled', 'completed'],
21
+ default: 'pending',
22
+ index: true,
23
+ },
24
+ tenantNote: { type: String, default: null },
25
+ landlordNote: { type: String, default: null },
26
+ cancelledAt: { type: Date, default: null },
27
+ cancelReason: { type: String, default: null },
28
+ },
29
+ { timestamps: true }
30
+ );
31
+
32
+ const MoveInBooking = mongoose.model('MoveInBooking', moveInBookingSchema);
33
+ module.exports = MoveInBooking;
@@ -0,0 +1,46 @@
1
+ const mongoose = require('mongoose');
2
+ const ObjectId = mongoose.Schema.Types.ObjectId;
3
+
4
+ const moveInCommissionSchema = new mongoose.Schema(
5
+ {
6
+ dealId: { type: ObjectId, ref: 'MoveInDeal', required: true, index: true },
7
+ unitId: { type: ObjectId, required: true, index: true },
8
+ unitName: { type: String, default: null },
9
+ landlordId: { type: ObjectId, default: null, index: true },
10
+ tenantId: { type: ObjectId, ref: 'MoveInUser', default: null, index: true },
11
+ tenantEmail: { type: String, default: null, index: true },
12
+ payerType: {
13
+ type: String,
14
+ enum: ['landlord', 'tenant', 'owner', 'admin'],
15
+ default: 'landlord',
16
+ index: true,
17
+ },
18
+ ruleType: {
19
+ type: String,
20
+ enum: ['percentage_of_rent', 'fixed', 'manual'],
21
+ default: 'percentage_of_rent',
22
+ },
23
+ ruleValue: { type: Number, default: 10 },
24
+ baseAmount: { type: Number, required: true },
25
+ amount: { type: Number, required: true },
26
+ currency: { type: String, default: 'KES' },
27
+ status: {
28
+ type: String,
29
+ enum: ['due', 'invoiced', 'paid', 'waived', 'refunded', 'disputed', 'cancelled'],
30
+ default: 'due',
31
+ index: true,
32
+ },
33
+ dueAt: { type: Date, default: null },
34
+ invoiceRef: { type: String, default: null, index: true },
35
+ paymentRef: { type: String, default: null, index: true },
36
+ paymentMethod: { type: String, default: null },
37
+ paidAt: { type: Date, default: null },
38
+ notes: { type: String, default: null },
39
+ },
40
+ { timestamps: true }
41
+ );
42
+
43
+ moveInCommissionSchema.index({ dealId: 1, status: 1 });
44
+
45
+ const MoveInCommission = mongoose.model('MoveInCommission', moveInCommissionSchema);
46
+ module.exports = MoveInCommission;
@@ -0,0 +1,25 @@
1
+ const mongoose = require('mongoose');
2
+ const ObjectId = mongoose.Schema.Types.ObjectId;
3
+
4
+ const moveInConversationSchema = new mongoose.Schema(
5
+ {
6
+ tenantId: { type: ObjectId, ref: 'MoveInUser', required: true, index: true },
7
+ landlordId: { type: ObjectId, ref: 'User', required: true, index: true },
8
+ unitId: { type: ObjectId, required: true, index: true },
9
+ unitName: { type: String, default: null },
10
+ lastMessage: { type: String, default: null },
11
+ lastMessageAt: { type: Date, default: null },
12
+ tenantUnread: { type: Number, default: 0 },
13
+ landlordUnread:{ type: Number, default: 0 },
14
+ status: {
15
+ type: String,
16
+ enum: ['active', 'closed'],
17
+ default: 'active',
18
+ index: true,
19
+ },
20
+ },
21
+ { timestamps: true }
22
+ );
23
+
24
+ const MoveInConversation = mongoose.model('MoveInConversation', moveInConversationSchema);
25
+ module.exports = MoveInConversation;
@@ -0,0 +1,79 @@
1
+ const mongoose = require('mongoose');
2
+ const ObjectId = mongoose.Schema.Types.ObjectId;
3
+
4
+ const moveInDealSchema = new mongoose.Schema(
5
+ {
6
+ unitId: { type: ObjectId, required: true, index: true },
7
+ unitName: { type: String, default: null },
8
+ source: {
9
+ type: String,
10
+ enum: ['standalone', 'payserve'],
11
+ default: 'standalone',
12
+ index: true,
13
+ },
14
+ sourceFacilityId: { type: ObjectId, ref: 'Facility', default: null, index: true },
15
+ sourceUnitId: { type: ObjectId, default: null, index: true },
16
+ landlordId: { type: ObjectId, default: null, index: true },
17
+ tenantId: { type: ObjectId, ref: 'MoveInUser', default: null, index: true },
18
+ tenantName: { type: String, default: null },
19
+ tenantEmail: { type: String, default: null, index: true },
20
+ tenantPhone: { type: String, default: null },
21
+ isGuest: { type: Boolean, default: false, index: true },
22
+ applicationId: { type: ObjectId, ref: 'MoveInApplication', default: null, index: true },
23
+ reservationId: { type: ObjectId, ref: 'MoveInReservation', default: null, index: true },
24
+ bookingId: { type: ObjectId, ref: 'MoveInBooking', default: null, index: true },
25
+ desiredMoveInDate:{ type: Date, default: null },
26
+ agreedMoveInDate: { type: Date, default: null },
27
+ agreedRentAmount: { type: Number, default: null },
28
+ currency: { type: String, default: 'KES' },
29
+ status: {
30
+ type: String,
31
+ enum: [
32
+ 'lead',
33
+ 'viewing_requested',
34
+ 'viewing_confirmed',
35
+ 'applied',
36
+ 'application_approved',
37
+ 'reserved',
38
+ 'offer_sent',
39
+ 'tenant_confirmed',
40
+ 'payment_pending',
41
+ 'rented',
42
+ 'cancelled',
43
+ 'expired',
44
+ 'lost',
45
+ ],
46
+ default: 'lead',
47
+ index: true,
48
+ },
49
+ commissionStatus: {
50
+ type: String,
51
+ enum: ['not_due', 'due', 'invoiced', 'paid', 'waived', 'refunded', 'disputed'],
52
+ default: 'not_due',
53
+ index: true,
54
+ },
55
+ lastEvent: { type: String, default: null },
56
+ cancelledReason: { type: String, default: null },
57
+ notes: { type: String, default: null },
58
+ payserveSync: {
59
+ status: {
60
+ type: String,
61
+ enum: ['not_applicable', 'pending', 'synced', 'failed'],
62
+ default: 'not_applicable',
63
+ },
64
+ residentId: { type: ObjectId, default: null },
65
+ leaseId: { type: ObjectId, default: null },
66
+ invoiceId: { type: ObjectId, default: null },
67
+ lastError: { type: String, default: null },
68
+ syncedAt: { type: Date, default: null },
69
+ },
70
+ },
71
+ { timestamps: true }
72
+ );
73
+
74
+ moveInDealSchema.index({ unitId: 1, status: 1 });
75
+ moveInDealSchema.index({ tenantId: 1, unitId: 1 });
76
+ moveInDealSchema.index({ tenantEmail: 1, unitId: 1 });
77
+
78
+ const MoveInDeal = mongoose.model('MoveInDeal', moveInDealSchema);
79
+ module.exports = MoveInDeal;
@@ -0,0 +1,16 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ // Short-lived, single-use token that lets a landlord_main session
4
+ // bootstrap a Move-In session without re-logging in.
5
+ const moveInHandoffTokenSchema = new mongoose.Schema(
6
+ {
7
+ token: { type: String, required: true, unique: true, index: true },
8
+ landlordId: { type: mongoose.Schema.Types.ObjectId, required: true, index: true },
9
+ used: { type: Boolean, default: false },
10
+ expiresAt: { type: Date, required: true },
11
+ },
12
+ { timestamps: true }
13
+ );
14
+
15
+ const MoveInHandoffToken = mongoose.model('MoveInHandoffToken', moveInHandoffTokenSchema);
16
+ module.exports = MoveInHandoffToken;
@@ -0,0 +1,20 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const moveInLandlordUserSchema = new mongoose.Schema(
4
+ {
5
+ fullName: { type: String, required: true, trim: true },
6
+ email: { type: String, required: true, unique: true, lowercase: true, trim: true },
7
+ phoneNumber: { type: String, required: true, unique: true },
8
+ // Standalone landlords have a bcrypt password.
9
+ // PayServe-linked landlords have MANAGED_BY_PAYSERVE — auth defers to payserve_property.
10
+ password: { type: String, default: 'MANAGED_BY_PAYSERVE' },
11
+ companyName: { type: String, trim: true, default: null },
12
+ isEnabled: { type: Boolean, default: true },
13
+ // Set when provisioned from an existing PayServe landlord account.
14
+ payserveUserId: { type: mongoose.Schema.Types.ObjectId, default: null, index: true },
15
+ },
16
+ { timestamps: true }
17
+ );
18
+
19
+ const MoveInLandlordUser = mongoose.model('MoveInLandlordUser', moveInLandlordUserSchema);
20
+ module.exports = MoveInLandlordUser;
@@ -0,0 +1,27 @@
1
+ const mongoose = require('mongoose');
2
+ const ObjectId = mongoose.Schema.Types.ObjectId;
3
+
4
+ const moveInMessageSchema = new mongoose.Schema(
5
+ {
6
+ conversationId: { type: ObjectId, ref: 'MoveInConversation', required: true, index: true },
7
+ senderId: { type: ObjectId, required: true, index: true },
8
+ senderType: {
9
+ type: String,
10
+ enum: ['tenant', 'landlord'],
11
+ required: true,
12
+ },
13
+ body: { type: String, required: true },
14
+ type: {
15
+ type: String,
16
+ enum: ['text', 'image'],
17
+ default: 'text',
18
+ },
19
+ attachmentUrl: { type: String, default: null },
20
+ isRead: { type: Boolean, default: false, index: true },
21
+ readAt: { type: Date, default: null },
22
+ },
23
+ { timestamps: true }
24
+ );
25
+
26
+ const MoveInMessage = mongoose.model('MoveInMessage', moveInMessageSchema);
27
+ module.exports = MoveInMessage;
@@ -0,0 +1,27 @@
1
+ const mongoose = require('mongoose');
2
+ const ObjectId = mongoose.Schema.Types.ObjectId;
3
+
4
+ const moveInNotificationSchema = new mongoose.Schema(
5
+ {
6
+ recipientId: { type: ObjectId, required: true, index: true },
7
+ recipientType: {
8
+ type: String,
9
+ enum: ['tenant', 'landlord'],
10
+ required: true,
11
+ },
12
+ title: { type: String, required: true },
13
+ body: { type: String, required: true },
14
+ type: {
15
+ type: String,
16
+ enum: ['application', 'viewing', 'reservation', 'payment', 'message', 'general'],
17
+ default: 'general',
18
+ },
19
+ relatedId: { type: ObjectId, default: null },
20
+ isRead: { type: Boolean, default: false, index: true },
21
+ readAt: { type: Date, default: null },
22
+ },
23
+ { timestamps: true }
24
+ );
25
+
26
+ const MoveInNotification = mongoose.model('MoveInNotification', moveInNotificationSchema);
27
+ module.exports = MoveInNotification;
@@ -0,0 +1,14 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const moveInOtpSchema = new mongoose.Schema(
4
+ {
5
+ email: { type: String, required: true, lowercase: true, trim: true, index: true },
6
+ otp: { type: String, required: true },
7
+ expiresAt: { type: Date, required: true },
8
+ used: { type: Boolean, default: false },
9
+ },
10
+ { timestamps: true }
11
+ );
12
+
13
+ const MoveInOtp = mongoose.model('MoveInOtp', moveInOtpSchema);
14
+ module.exports = MoveInOtp;
@@ -0,0 +1,46 @@
1
+ const mongoose = require('mongoose');
2
+ const ObjectId = mongoose.Schema.Types.ObjectId;
3
+
4
+ const moveInPaymentSchema = new mongoose.Schema(
5
+ {
6
+ tenantId: { type: ObjectId, ref: 'MoveInUser', default: null, index: true },
7
+ tenantName: { type: String, default: null },
8
+ unitId: { type: ObjectId, default: null, index: true },
9
+ unitName: { type: String, default: null },
10
+ reservationId: { type: ObjectId, ref: 'MoveInReservation', default: null, index: true },
11
+ dealId: { type: ObjectId, ref: 'MoveInDeal', default: null, index: true },
12
+ commissionId: { type: ObjectId, ref: 'MoveInCommission', default: null, index: true },
13
+ type: {
14
+ type: String,
15
+ enum: ['reservation_fee', 'deposit', 'first_month_rent', 'commission', 'other'],
16
+ required: true,
17
+ },
18
+ amount: { type: Number, required: true },
19
+ currency: { type: String, default: 'KES' },
20
+ status: {
21
+ type: String,
22
+ enum: ['pending', 'paid', 'failed', 'refunded', 'waived', 'cancelled', 'disputed'],
23
+ default: 'pending',
24
+ index: true,
25
+ },
26
+ reference: { type: String, default: null },
27
+ externalReference: { type: String, default: null, index: true },
28
+ paymentMethod: {
29
+ type: String,
30
+ enum: ['mpesa', 'card', 'cash', 'other'],
31
+ default: null,
32
+ },
33
+ payerType: {
34
+ type: String,
35
+ enum: ['tenant', 'landlord', 'owner', 'admin'],
36
+ default: 'tenant',
37
+ index: true,
38
+ },
39
+ paidAt: { type: Date, default: null },
40
+ notes: { type: String, default: null },
41
+ },
42
+ { timestamps: true }
43
+ );
44
+
45
+ const MoveInPayment = mongoose.model('MoveInPayment', moveInPaymentSchema);
46
+ module.exports = MoveInPayment;
@@ -0,0 +1,31 @@
1
+ const mongoose = require('mongoose');
2
+ const ObjectId = mongoose.Schema.Types.ObjectId;
3
+
4
+ const moveInReservationSchema = new mongoose.Schema(
5
+ {
6
+ tenantId: { type: ObjectId, ref: 'MoveInUser', default: null, index: true },
7
+ tenantName: { type: String, default: null },
8
+ tenantEmail: { type: String, default: null },
9
+ tenantPhone: { type: String, default: null },
10
+ isGuest: { type: Boolean, default: false, index: true },
11
+ unitId: { type: ObjectId, required: true, index: true },
12
+ unitName: { type: String, default: null },
13
+ facilityId: { type: ObjectId, ref: 'Facility', default: null, index: true },
14
+ landlordId: { type: ObjectId, ref: 'User', default: null, index: true },
15
+ desiredMoveInDate:{ type: Date, default: null },
16
+ monthsToStay: { type: Number, default: null },
17
+ status: {
18
+ type: String,
19
+ enum: ['pending', 'confirmed', 'expired', 'cancelled'],
20
+ default: 'pending',
21
+ index: true,
22
+ },
23
+ expiresAt: { type: Date, default: null },
24
+ adminNote: { type: String, default: null },
25
+ landlordNote: { type: String, default: null },
26
+ },
27
+ { timestamps: true }
28
+ );
29
+
30
+ const MoveInReservation = mongoose.model('MoveInReservation', moveInReservationSchema);
31
+ module.exports = MoveInReservation;
@@ -0,0 +1,59 @@
1
+ const mongoose = require('mongoose');
2
+ const ObjectId = mongoose.Schema.Types.ObjectId;
3
+
4
+ const moveInUnitSchema = new mongoose.Schema(
5
+ {
6
+ landlordId: { type: ObjectId, ref: 'User', required: true, index: true },
7
+ title: { type: String, required: true, trim: true },
8
+ description: { type: String, default: null },
9
+ listingType: {
10
+ type: String,
11
+ enum: ['Apartment', 'Studio', 'Bedsitter', 'Bungalow', 'Maisonette', 'Townhouse', 'Villa', 'Office'],
12
+ default: null,
13
+ },
14
+ bedrooms: { type: Number, default: null },
15
+ bathrooms: { type: Number, default: null },
16
+ grossArea: { type: Number, default: null },
17
+ price: { type: Number, required: true },
18
+ location: {
19
+ address: { type: String, default: null },
20
+ area: { type: String, default: null, index: true },
21
+ city: { type: String, default: null, index: true },
22
+ county: { type: String, default: null },
23
+ landmarks: { type: [String], default: [] },
24
+ googleMapsUrl: { type: String, default: null },
25
+ coordinates: {
26
+ lat: { type: Number, default: null },
27
+ lng: { type: Number, default: null },
28
+ },
29
+ },
30
+ amenities: { type: [String], default: [] },
31
+ nearbyServices: { type: [String], default: [] },
32
+ images: {
33
+ type: [{
34
+ category: { type: String, default: 'Other' },
35
+ label: { type: String, default: '' },
36
+ url: { type: String, required: true },
37
+ }],
38
+ default: [],
39
+ },
40
+ moveInApproval: {
41
+ type: String,
42
+ enum: ['pending', 'approved', 'rejected'],
43
+ default: null,
44
+ index: true,
45
+ },
46
+ isListed: { type: Boolean, default: false, index: true },
47
+ moveInStatus: {
48
+ type: String,
49
+ enum: ['draft', 'pending_approval', 'approved_unlisted', 'listed', 'under_offer', 'reserved', 'rented', 'suspended'],
50
+ default: 'draft',
51
+ index: true,
52
+ },
53
+ activeDealId: { type: ObjectId, ref: 'MoveInDeal', default: null, index: true },
54
+ },
55
+ { timestamps: true }
56
+ );
57
+
58
+ const MoveInUnit = mongoose.model('MoveInUnit', moveInUnitSchema);
59
+ module.exports = MoveInUnit;
@@ -0,0 +1,21 @@
1
+ const mongoose = require('mongoose');
2
+ const ObjectId = mongoose.Schema.Types.ObjectId;
3
+
4
+ const moveInViewingSlotSchema = new mongoose.Schema(
5
+ {
6
+ landlordId: { type: ObjectId, ref: 'User', required: true, index: true },
7
+ unitId: { type: ObjectId, required: true, index: true },
8
+ unitName: { type: String, default: null },
9
+ facilityId: { type: ObjectId, ref: 'Facility', default: null, index: true },
10
+ date: { type: Date, required: true, index: true },
11
+ time: { type: String, required: true }, // "14:00"
12
+ durationMins:{ type: Number, default: 30 },
13
+ capacity: { type: Number, default: 1 }, // max bookings for this slot
14
+ bookedCount: { type: Number, default: 0 },
15
+ isAvailable: { type: Boolean, default: true, index: true },
16
+ },
17
+ { timestamps: true }
18
+ );
19
+
20
+ const MoveInViewingSlot = mongoose.model('MoveInViewingSlot', moveInViewingSlotSchema);
21
+ module.exports = MoveInViewingSlot;
@@ -0,0 +1,61 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ /**
4
+ * Admin-managed recipient group. Agents pick from these groups when sending
5
+ * bulk email or WhatsApp messages. Membership lives in `recipient_group_member`.
6
+ *
7
+ * Channel determines what kind of bulk send can target this group:
8
+ * - 'email' : every member needs an email
9
+ * - 'whatsapp': every member needs a phone
10
+ * - 'both' : every member needs both
11
+ */
12
+ const recipientGroupSchema = new mongoose.Schema({
13
+ name: {
14
+ type: String,
15
+ required: [true, 'Group name is required'],
16
+ trim: true,
17
+ maxlength: 120,
18
+ },
19
+ channel: {
20
+ type: String,
21
+ enum: ['email', 'whatsapp', 'both'],
22
+ required: [true, 'channel is required'],
23
+ index: true,
24
+ },
25
+ description: {
26
+ type: String,
27
+ trim: true,
28
+ maxlength: 500,
29
+ },
30
+ member_count: {
31
+ type: Number,
32
+ default: 0,
33
+ min: 0,
34
+ },
35
+ created_by: {
36
+ type: mongoose.Schema.Types.ObjectId,
37
+ ref: 'User',
38
+ },
39
+ updated_by: {
40
+ type: mongoose.Schema.Types.ObjectId,
41
+ ref: 'User',
42
+ },
43
+ created_at: {
44
+ type: Date,
45
+ default: Date.now,
46
+ },
47
+ updated_at: {
48
+ type: Date,
49
+ default: Date.now,
50
+ },
51
+ }, {
52
+ timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' },
53
+ });
54
+
55
+ recipientGroupSchema.index({ name: 1 }, {
56
+ unique: true,
57
+ collation: { locale: 'en', strength: 2 },
58
+ });
59
+ recipientGroupSchema.index({ channel: 1, created_at: -1 });
60
+
61
+ module.exports = mongoose.model('RecipientGroup', recipientGroupSchema);
@@ -0,0 +1,62 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ /**
4
+ * Members of a recipient group. Either references an existing Customer
5
+ * (`customer_id`) for first-class membership or carries free-form
6
+ * `email`/`phone`/`name` for ad-hoc additions (landlords, contractors, etc.).
7
+ *
8
+ * Dedup happens at insert time: see add_members controller. The group's
9
+ * `member_count` is denormalised on the parent for cheap listing.
10
+ */
11
+ const recipientGroupMemberSchema = new mongoose.Schema({
12
+ group_id: {
13
+ type: mongoose.Schema.Types.ObjectId,
14
+ ref: 'RecipientGroup',
15
+ required: true,
16
+ index: true,
17
+ },
18
+ customer_id: {
19
+ type: mongoose.Schema.Types.ObjectId,
20
+ ref: 'Customer',
21
+ },
22
+ name: {
23
+ type: String,
24
+ trim: true,
25
+ maxlength: 200,
26
+ },
27
+ email: {
28
+ type: String,
29
+ trim: true,
30
+ lowercase: true,
31
+ maxlength: 254,
32
+ },
33
+ phone: {
34
+ type: String,
35
+ trim: true,
36
+ maxlength: 30,
37
+ },
38
+ added_by: {
39
+ type: mongoose.Schema.Types.ObjectId,
40
+ ref: 'User',
41
+ },
42
+ added_at: {
43
+ type: Date,
44
+ default: Date.now,
45
+ },
46
+ });
47
+
48
+ // Dedup helpers
49
+ recipientGroupMemberSchema.index({ group_id: 1, customer_id: 1 }, {
50
+ unique: true,
51
+ partialFilterExpression: { customer_id: { $type: 'objectId' } },
52
+ });
53
+ recipientGroupMemberSchema.index({ group_id: 1, email: 1 }, {
54
+ unique: true,
55
+ partialFilterExpression: { email: { $type: 'string' } },
56
+ });
57
+ recipientGroupMemberSchema.index({ group_id: 1, phone: 1 }, {
58
+ unique: true,
59
+ partialFilterExpression: { phone: { $type: 'string' } },
60
+ });
61
+
62
+ module.exports = mongoose.model('RecipientGroupMember', recipientGroupMemberSchema);