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 +69 -0
- package/package.json +1 -1
- package/src/models/auto_reply_rule.js +68 -0
- package/src/models/email_cc_config.js +48 -0
- package/src/models/movein_audit_log.js +21 -0
- package/src/models/movein_booking.js +33 -0
- package/src/models/movein_commission.js +46 -0
- package/src/models/movein_conversation.js +25 -0
- package/src/models/movein_deal.js +79 -0
- package/src/models/movein_handoff_token.js +16 -0
- package/src/models/movein_landlord_user.js +20 -0
- package/src/models/movein_message.js +27 -0
- package/src/models/movein_notification.js +27 -0
- package/src/models/movein_otp.js +14 -0
- package/src/models/movein_payment.js +46 -0
- package/src/models/movein_reservation.js +31 -0
- package/src/models/movein_unit.js +59 -0
- package/src/models/movein_viewing_slot.js +21 -0
- package/src/models/recipient_group.js +61 -0
- package/src/models/recipient_group_member.js +62 -0
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
|
@@ -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);
|