integratedhub/services/e2eFixtures.ts

1292 lines
37 KiB
TypeScript

import type { SessionCookie } from "../routes/auth/callback.ts";
import type {
Lease,
LeaseDocument,
MaintenanceRequest,
Message,
PaymentTransaction,
Property,
PropertyMedia,
RentSchedule,
TenantApplication,
TenantInvite,
User,
} from "../db/interfaces.ts";
import type { PropertyMergeResult } from "./propertyMergeService.ts";
import type { PaymentMethod } from "./paymentService.ts";
import type { MaintenanceData } from "./maintenanceRequestService.ts";
const E2E_ENABLED = Deno.env.get("E2E_TEST") === "true";
const BASE_TIME = new Date("2024-01-15T12:00:00.000Z").getTime();
const e2eUserIds = {
landlord: "e2e-landlord-1",
"landlord-onboarding": "e2e-landlord-setup-1",
tenant: "e2e-tenant-1",
};
let e2eUsers: Record<string, User> = {
[e2eUserIds.landlord]: {
id: e2eUserIds.landlord,
name: "Alex Landlord",
email: "alex.landlord@example.com",
phone: "555-0100",
address_1: "88 Beacon Way",
address_2: "",
city: "Seattle",
state: "WA",
zip: "98101",
country: "USA",
user_type: "landlord",
onboarded_landlord: true,
onboarded_tenant: false,
email_verified: true,
banking_setup_complete: true,
onboarding_step: 0,
encrypted_data: null,
started_on: BASE_TIME - 1000 * 60 * 60 * 24,
updated_on: BASE_TIME,
},
[e2eUserIds["landlord-onboarding"]]: {
id: e2eUserIds["landlord-onboarding"],
name: "",
email: "",
phone: "",
address_1: "",
address_2: "",
city: "",
state: "",
zip: "",
country: "",
user_type: "landlord",
onboarded_landlord: false,
onboarded_tenant: false,
email_verified: false,
banking_setup_complete: false,
onboarding_step: 1,
encrypted_data: null,
started_on: BASE_TIME - 1000 * 60 * 60 * 48,
updated_on: BASE_TIME,
},
[e2eUserIds.tenant]: {
id: e2eUserIds.tenant,
name: "Taylor Tenant",
email: "taylor.tenant@example.com",
phone: "555-0199",
address_1: "402 Pine St",
address_2: "Unit 12",
city: "Seattle",
state: "WA",
zip: "98102",
country: "USA",
user_type: "tenant",
onboarded_landlord: false,
onboarded_tenant: true,
email_verified: true,
banking_setup_complete: false,
onboarding_step: 0,
encrypted_data: null,
started_on: BASE_TIME - 1000 * 60 * 60 * 24,
updated_on: BASE_TIME,
},
};
let e2eProperties: Property[] = [
{
id: "e2e-prop-1",
title: "Skyline Lofts",
description: "Modern loft with skyline views.",
address_1: "123 Harbor Ave",
address_2: "",
city: "Seattle",
state: "WA",
zip: "98109",
country: "USA",
bedrooms: 2,
bathrooms: 2,
square_footage: 980,
year_built: 2018,
property_type: "apartment",
listing_status: "active",
rental_status: "available",
monthly_rent: 2400,
security_deposit: 2400,
lease_duration_months: 12,
amenities: "Rooftop deck, gym",
created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 14,
updated_on: BASE_TIME - 1000 * 60 * 60 * 24 * 3,
owner_id: e2eUserIds.landlord,
},
{
id: "e2e-prop-2",
title: "Maple Row",
description: "Cozy townhome near the park.",
address_1: "456 Maple St",
address_2: "",
city: "Seattle",
state: "WA",
zip: "98122",
country: "USA",
bedrooms: 3,
bathrooms: 2,
square_footage: 1250,
year_built: 2012,
property_type: "townhome",
listing_status: "active",
rental_status: "occupied",
monthly_rent: 1800,
security_deposit: 1800,
lease_duration_months: 12,
amenities: "Garage, patio",
created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 30,
updated_on: BASE_TIME - 1000 * 60 * 60 * 24 * 1,
owner_id: e2eUserIds.landlord,
},
];
let e2eLeases: Lease[] = [
{
id: "e2e-lease-1",
title: "Maple Row Lease",
property_id: "e2e-prop-2",
property_address: "456 Maple St, Seattle, WA",
tenant_id: e2eUserIds.tenant,
owner_id: e2eUserIds.landlord,
start_date: BASE_TIME - 1000 * 60 * 60 * 24 * 120,
end_date: BASE_TIME + 1000 * 60 * 60 * 24 * 245,
rent: 1800,
security_deposit: 1800,
lease_status: "active",
approval_status: "approved",
approval_requested_at: BASE_TIME - 1000 * 60 * 60 * 24 * 115,
approved_at: BASE_TIME - 1000 * 60 * 60 * 24 * 114,
approved_by: e2eUserIds.landlord,
created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 120,
updated_on: BASE_TIME - 1000 * 60 * 60 * 24 * 5,
},
{
id: "e2e-lease-pending-1",
title: "Pending Oak Avenue Lease",
property_id: "e2e-prop-1",
property_address: "123 Oak Ave, Seattle, WA",
tenant_id: e2eUserIds.tenant,
owner_id: e2eUserIds.landlord,
start_date: BASE_TIME + 1000 * 60 * 60 * 24 * 30,
end_date: BASE_TIME + 1000 * 60 * 60 * 24 * 395,
rent: 2200,
security_deposit: 2200,
lease_status: "draft",
approval_status: "pending_approval",
approval_requested_at: BASE_TIME - 1000 * 60 * 60 * 24 * 2,
created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 3,
updated_on: BASE_TIME - 1000 * 60 * 60 * 24 * 2,
},
];
const extraE2EMessages: Message[] = Array.from({ length: 9 }, (_, index) => ({
id: `e2e-msg-${index + 3}`,
lease_id: "e2e-lease-1",
sender_id: e2eUserIds.landlord,
recipient_id: e2eUserIds.tenant,
message_type: "general",
subject: `Lease update ${index + 1}`,
content: `Follow-up note ${index + 1} about the lease timeline.`,
is_read: true,
created_on: BASE_TIME - 1000 * 60 * 60 * (4 + index),
}));
let e2eMessages: Message[] = [
{
id: "e2e-msg-1",
lease_id: "e2e-lease-1",
sender_id: e2eUserIds.tenant,
recipient_id: e2eUserIds.landlord,
message_type: "maintenance",
subject: "Leaking kitchen faucet",
content: "The kitchen faucet drips after turning it off.",
is_read: false,
created_on: BASE_TIME - 1000 * 60 * 60 * 3,
},
{
id: "e2e-msg-2",
lease_id: "e2e-lease-1",
sender_id: e2eUserIds.landlord,
recipient_id: e2eUserIds.tenant,
message_type: "notice",
subject: "Inspection scheduled",
content: "Annual inspection will take place next Tuesday.",
is_read: true,
created_on: BASE_TIME - 1000 * 60 * 60 * 26,
},
...extraE2EMessages,
];
export function addE2EMessage(
leaseId: string | null,
senderId: string,
recipientId: string,
messageType: string,
subject: string,
content: string,
parentMessageId: string | null = null,
attachmentsJson: string | null = null,
) {
const newMessage: Message = {
id: crypto.randomUUID(),
lease_id: leaseId ?? null,
sender_id: senderId,
recipient_id: recipientId,
message_type: messageType,
subject,
content,
content_encrypted: content,
parent_message_id: parentMessageId,
attachments_json: attachmentsJson ? JSON.parse(attachmentsJson) as Record<string, unknown> : null,
is_read: false,
created_on: BASE_TIME,
};
e2eMessages = [newMessage, ...e2eMessages];
return newMessage;
}
export function getE2EMessageById(messageId: string): Message | null {
return e2eMessages.find((message) => message.id === messageId) ?? null;
}
export function getE2EMessageThread(messageId: string): Message[] | null {
const initialMessage = getE2EMessageById(messageId);
if (!initialMessage) return null;
const rootId = initialMessage.parent_message_id || initialMessage.id;
return e2eMessages
.filter((message) =>
message.id === rootId || message.parent_message_id === rootId
)
.sort((a, b) => (a.created_on ?? 0) - (b.created_on ?? 0));
}
export function getE2EMessagesByLease(leaseId: string) {
return e2eMessages.filter((message) => message.lease_id === leaseId);
}
let e2eMaintenanceRequests: MaintenanceRequest[] = [
{
id: "e2e-maint-1",
property_id: "e2e-prop-2",
tenant_id: e2eUserIds.tenant,
request_type: "plumbing",
description: "Kitchen sink is leaking.",
status: "open",
priority: "high",
created_on: BASE_TIME - 1000 * 60 * 60 * 12,
resolved_on: null,
},
{
id: "e2e-maint-2",
property_id: "e2e-prop-2",
tenant_id: e2eUserIds.tenant,
request_type: "electrical",
description: "Living room outlet not working.",
status: "in_progress",
priority: "medium",
created_on: BASE_TIME - 1000 * 60 * 60 * 72,
resolved_on: null,
},
];
let e2eTenantApplications: TenantApplication[] = [
{
id: "e2e-app-1",
property_id: "e2e-prop-1",
applicant_id: e2eUserIds.tenant,
application_status: "pending",
application_data_encrypted: '{"source":"e2e"}',
submitted_on: BASE_TIME - 1000 * 60 * 60 * 6,
reviewed_on: null,
},
];
let e2eTenantInvites: TenantInvite[] = [];
let e2eRentSchedules: RentSchedule[] = [];
let e2ePropertyMedia: PropertyMedia[] = [
{
id: "e2e-media-1",
property_id: "e2e-prop-1",
media_type: "photo",
file_url: "/logo.svg",
file_name: "Skyline Lofts",
display_order: 0,
created_on: BASE_TIME,
},
{
id: "e2e-media-2",
property_id: "e2e-prop-1",
media_type: "photo",
file_url: "/logo.svg",
file_name: "Skyline Lofts Interior",
display_order: 1,
created_on: BASE_TIME,
},
];
let e2ePaymentMethods: PaymentMethod[] = [
{
id: "e2e-pay-method-1",
user_id: e2eUserIds.tenant,
bank_name: "Northwest Credit Union",
method_type: "bank_account",
account_type: "checking",
routing_number_encrypted: "test-routing",
account_number_encrypted: "test-account",
is_default: true,
created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 10,
updated_on: BASE_TIME - 1000 * 60 * 60 * 24 * 2,
},
];
let e2ePaymentTransactions: PaymentTransaction[] = [
{
id: "e2e-pay-0",
lease_id: "e2e-lease-1",
amount: 1800,
payment_date: BASE_TIME - 1000 * 60 * 60 * 24 * 10,
payment_method: "ach",
transaction_status: "completed",
stripe_payment_id: "pi_test_jan_1",
created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 10,
},
{
id: "e2e-pay-late-1",
lease_id: "e2e-lease-1",
amount: 1800,
payment_date: BASE_TIME - 1000 * 60 * 60 * 24 * 7,
payment_method: "ach",
transaction_status: "pending",
stripe_payment_id: "pi_test_late_1",
created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 7,
},
{
id: "e2e-pay-1",
lease_id: "e2e-lease-1",
amount: 1800,
payment_date: BASE_TIME - 1000 * 60 * 60 * 24 * 25,
payment_method: "ach",
transaction_status: "completed",
stripe_payment_id: "pi_test_123",
created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 25,
},
{
id: "e2e-pay-2",
lease_id: "e2e-lease-1",
amount: 1800,
payment_date: BASE_TIME - 1000 * 60 * 60 * 24 * 55,
payment_method: "ach",
transaction_status: "completed",
stripe_payment_id: "pi_test_456",
created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 55,
},
];
let e2eLeaseDocuments: LeaseDocument[] = [
{
id: "e2e-doc-lease-1",
lease_id: "e2e-lease-1",
document_type: "lease",
file_url: "/static/sample-lease.pdf",
file_name: "maple-row-lease.pdf",
signed_by_landlord: false,
signed_by_tenant: false,
signature_date: null,
created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 20,
},
{
id: "e2e-doc-receipt-1",
lease_id: "e2e-lease-1",
document_type: "receipt",
file_url: "/static/sample-receipt.pdf",
file_name: "rent-receipt-jan.pdf",
signed_by_landlord: true,
signed_by_tenant: true,
signature_date: BASE_TIME - 1000 * 60 * 60 * 24 * 15,
created_on: BASE_TIME - 1000 * 60 * 60 * 24 * 15,
},
];
export function getE2ENow() {
return BASE_TIME;
}
export function addE2EPaymentMethod(
userId: string,
input: Partial<PaymentMethod>,
) {
const now = BASE_TIME;
const isDefault = input.is_default ?? false;
if (isDefault) {
e2ePaymentMethods = e2ePaymentMethods.map((method) =>
method.user_id === userId ? { ...method, is_default: false } : method
);
}
const newMethod: PaymentMethod = {
id: crypto.randomUUID(),
user_id: userId,
bank_name: input.bank_name ?? null,
method_type: input.method_type ?? "bank_account",
account_type: input.account_type ?? null,
routing_number_encrypted: input.routing_number_encrypted ?? null,
account_number_encrypted: input.account_number_encrypted ?? null,
is_default: isDefault,
created_on: now,
updated_on: now,
};
e2ePaymentMethods = [...e2ePaymentMethods, newMethod];
return newMethod;
}
export function updateE2EPaymentMethod(
id: string,
updates: Partial<PaymentMethod>,
) {
let updated: PaymentMethod | null = null;
e2ePaymentMethods = e2ePaymentMethods.map((method) => {
if (method.id !== id) return method;
updated = { ...method, ...updates, updated_on: BASE_TIME };
return updated;
});
return updated;
}
export function deleteE2EPaymentMethod(id: string) {
const existing = e2ePaymentMethods.find((method) => method.id === id) || null;
e2ePaymentMethods = e2ePaymentMethods.filter((method) => method.id !== id);
return existing;
}
export function getE2EDefaultPaymentMethod(userId: string) {
return e2ePaymentMethods.find((method) =>
method.user_id === userId && method.is_default
) || null;
}
export function addE2EPaymentTransaction(
leaseId: string,
amount: number,
paymentDate: number,
paymentMethod: string,
transactionStatus: string,
stripePaymentId?: string,
) {
const newTransaction: PaymentTransaction = {
id: crypto.randomUUID(),
lease_id: leaseId,
amount,
payment_date: paymentDate,
payment_method: paymentMethod,
transaction_status: transactionStatus,
stripe_payment_id: stripePaymentId ?? null,
created_on: BASE_TIME,
};
e2ePaymentTransactions = [newTransaction, ...e2ePaymentTransactions];
return newTransaction;
}
export function isE2EEnabled() {
return E2E_ENABLED;
}
export function resolveE2EUserKey(input: string | null | undefined) {
if (!input) return null;
const trimmed = input.trim();
if (!trimmed) return null;
const roleMatch = (e2eUserIds as Record<string, string>)[trimmed];
if (roleMatch) return roleMatch;
if (e2eUsers[trimmed]) return trimmed;
return null;
}
export function getE2ESession(
input: string | null | undefined,
): SessionCookie | null {
const userId = resolveE2EUserKey(input);
if (!userId) return null;
const user = e2eUsers[userId];
if (!user) return null;
return {
started_on: user.started_on || BASE_TIME,
user_name: user.name || "E2E User",
user_encrypted: false,
user_id: user.id,
role: user.user_type || "",
};
}
export function getE2EUserById(userId: string): User | null {
return e2eUsers[userId] ? { ...e2eUsers[userId] } : null;
}
export function setE2EUser(user: User) {
e2eUsers = { ...e2eUsers, [user.id]: { ...user } };
}
export function updateE2EUser(
userId: string,
updates: Partial<User>,
): User | null {
const existing = e2eUsers[userId];
if (!existing) return null;
const updated = { ...existing, ...updates, updated_on: BASE_TIME };
e2eUsers = { ...e2eUsers, [userId]: updated };
return { ...updated };
}
export function getE2EAllProperties() {
return [...e2eProperties];
}
export function getE2EPropertiesByOwner(ownerId: string) {
return e2eProperties.filter((property) => property.owner_id === ownerId);
}
export function getE2EPropertyById(propertyId: string): Property | null {
return e2eProperties.find((property) => property.id === propertyId) || null;
}
export function addE2EProperty(ownerId: string, input: Partial<Property>) {
const newProperty: Property = {
id: crypto.randomUUID(),
owner_id: ownerId,
title: input.title || "New Property",
description: input.description || "",
address_1: input.address_1 || "",
address_2: input.address_2 || "",
city: input.city || "",
state: input.state || "",
zip: input.zip || "",
country: input.country || "",
bedrooms: input.bedrooms || 0,
bathrooms: input.bathrooms || 0,
property_type: input.property_type || "",
monthly_rent: input.monthly_rent || 0,
security_deposit: input.security_deposit || 0,
lease_duration_months: input.lease_duration_months || 12,
amenities: input.amenities || "",
rental_status: "draft",
created_on: BASE_TIME,
updated_on: BASE_TIME,
};
e2eProperties = [...e2eProperties, newProperty];
return newProperty;
}
export function addE2EPropertyMedia(
propertyId: string,
mediaType: string,
fileUrl: string,
fileName: string,
displayOrder = 0,
) {
const media: PropertyMedia = {
id: crypto.randomUUID(),
property_id: propertyId,
media_type: mediaType,
file_url: fileUrl,
file_name: fileName,
display_order: displayOrder,
created_on: BASE_TIME,
};
e2ePropertyMedia = [...e2ePropertyMedia, media];
return media;
}
function sortMediaByOrder(media: PropertyMedia[]) {
return [...media].sort((a, b) =>
(a.display_order ?? 0) - (b.display_order ?? 0)
);
}
export function getE2EMediaByProperty(propertyId: string) {
return sortMediaByOrder(
e2ePropertyMedia.filter((media) => media.property_id === propertyId),
);
}
export function getE2EMediaByPropertyAndType(
propertyId: string,
mediaType: string,
) {
return sortMediaByOrder(
e2ePropertyMedia.filter((media) =>
media.property_id === propertyId && media.media_type === mediaType
),
);
}
export function updateE2EPropertyMediaOrder(id: string, displayOrder: number) {
let updated: PropertyMedia | null = null;
e2ePropertyMedia = e2ePropertyMedia.map((media) => {
if (media.id !== id) return media;
updated = { ...media, display_order: displayOrder };
return updated;
});
return updated ? [updated] : [];
}
export function updateE2EMultipleMediaOrders(
updates: { id: string; display_order: number }[],
) {
const updatesMap = new Map(
updates.map((update) => [update.id, update.display_order]),
);
const changed: PropertyMedia[] = [];
e2ePropertyMedia = e2ePropertyMedia.map((media) => {
const nextOrder = updatesMap.get(media.id);
if (nextOrder === undefined) return media;
const updated = { ...media, display_order: nextOrder };
changed.push(updated);
return updated;
});
return changed;
}
export function deleteE2EPropertyMedia(id: string) {
const existing = e2ePropertyMedia.find((media) => media.id === id) || null;
e2ePropertyMedia = e2ePropertyMedia.filter((media) => media.id !== id);
return existing ? [existing] : [];
}
export function deleteE2EAllMediaForProperty(propertyId: string) {
const removed = e2ePropertyMedia.filter((media) =>
media.property_id === propertyId
);
e2ePropertyMedia = e2ePropertyMedia.filter((media) =>
media.property_id !== propertyId
);
return removed;
}
export function getE2EMediaCountByProperty(propertyId: string) {
const media = getE2EMediaByProperty(propertyId);
return {
total: media.length,
photos: media.filter((item) => item.media_type === "photo").length,
videos: media.filter((item) => item.media_type === "video").length,
documents: media.filter((item) => item.media_type === "document").length,
};
}
export function getE2EPrimaryPhoto(propertyId: string) {
const photos = getE2EMediaByPropertyAndType(propertyId, "photo");
return photos[0] || null;
}
export function mergeE2EProperty(
canonicalPropertyId: string,
duplicatePropertyId: string,
canonicalAddress: string,
): PropertyMergeResult {
if (canonicalPropertyId === duplicatePropertyId) {
throw new Error("Select two different properties to merge.");
}
const canonical = getE2EPropertyById(canonicalPropertyId);
const duplicate = getE2EPropertyById(duplicatePropertyId);
if (!canonical || !duplicate) {
throw new Error("Property not found.");
}
const warnings: string[] = [];
const canonicalActive = e2eLeases.some((lease) =>
lease.property_id === canonicalPropertyId && lease.lease_status === "active"
);
const duplicateActive = e2eLeases.some((lease) =>
lease.property_id === duplicatePropertyId && lease.lease_status === "active"
);
if (canonicalActive && duplicateActive) {
warnings.push(
"Both properties have active leases. Confirm lease expectations after merge.",
);
}
const canonicalSchedules =
e2eRentSchedules.filter((schedule) =>
schedule.property_id === canonicalPropertyId
).length;
const duplicateSchedules =
e2eRentSchedules.filter((schedule) =>
schedule.property_id === duplicatePropertyId
).length;
if (canonicalSchedules && duplicateSchedules) {
warnings.push(
"Both properties have rent schedules. Review schedules after merge.",
);
}
let leaseUpdates = 0;
let scheduleUpdates = 0;
let applicationUpdates = 0;
let maintenanceUpdates = 0;
let mediaUpdates = 0;
e2eLeases = e2eLeases.map((lease) => {
if (lease.property_id !== duplicatePropertyId) return lease;
leaseUpdates += 1;
return {
...lease,
property_id: canonicalPropertyId,
property_address: canonicalAddress || lease.property_address || "",
updated_on: BASE_TIME,
};
});
e2eRentSchedules = e2eRentSchedules.map((schedule) => {
if (schedule.property_id !== duplicatePropertyId) return schedule;
scheduleUpdates += 1;
return {
...schedule,
property_id: canonicalPropertyId,
updated_on: BASE_TIME,
};
});
e2eTenantApplications = e2eTenantApplications.map((application) => {
if (application.property_id !== duplicatePropertyId) return application;
applicationUpdates += 1;
return { ...application, property_id: canonicalPropertyId };
});
e2eMaintenanceRequests = e2eMaintenanceRequests.map((request) => {
if (request.property_id !== duplicatePropertyId) return request;
maintenanceUpdates += 1;
return { ...request, property_id: canonicalPropertyId };
});
e2ePropertyMedia = e2ePropertyMedia.map((media) => {
if (media.property_id !== duplicatePropertyId) return media;
mediaUpdates += 1;
return { ...media, property_id: canonicalPropertyId };
});
e2eProperties = e2eProperties
.map((property) =>
property.id === canonicalPropertyId
? { ...property, updated_on: BASE_TIME }
: property
)
.filter((property) => property.id !== duplicatePropertyId);
return {
canonicalPropertyId,
duplicatePropertyId,
updated: {
leases: leaseUpdates,
rentSchedules: scheduleUpdates,
tenantApplications: applicationUpdates,
maintenanceRequests: maintenanceUpdates,
propertyMedia: mediaUpdates,
},
warnings,
};
}
export function getE2ELeasesByTenant(tenantId: string) {
return e2eLeases.filter((lease) => lease.tenant_id === tenantId);
}
export function getE2ELeasesByOwner(ownerId: string) {
return e2eLeases.filter((lease) => lease.owner_id === ownerId);
}
export function getE2ELeaseById(leaseId: string): Lease | null {
return e2eLeases.find((lease) => lease.id === leaseId) || null;
}
export function getE2EMessagesByUser(userId: string) {
return e2eMessages.filter((message) =>
message.sender_id === userId || message.recipient_id === userId
);
}
export function markE2EMessageAsRead(messageId: string) {
e2eMessages = e2eMessages.map((message) =>
message.id === messageId ? { ...message, is_read: true } : message
);
return true;
}
export function deleteE2EMessage(messageId: string) {
const existing = e2eMessages.find((message) => message.id === messageId) ||
null;
e2eMessages = e2eMessages.filter((message) => message.id !== messageId);
return existing;
}
export function deleteE2EMessagesForUser(userId: string, messageIds: string[]) {
if (messageIds.length === 0) return [];
const idSet = new Set(messageIds);
const removed: Message[] = [];
e2eMessages = e2eMessages.filter((message) => {
const isOwned = message.sender_id === userId ||
message.recipient_id === userId;
const shouldDelete = isOwned && idSet.has(message.id);
if (shouldDelete) removed.push(message);
return !shouldDelete;
});
return removed;
}
export function getE2EMaintenanceRequestsByUser(userId: string) {
return e2eMaintenanceRequests.filter((request) =>
request.tenant_id === userId
);
}
export function getE2EMaintenanceRequestsByLandlord(landlordId: string) {
const propertyIds = e2eProperties
.filter((property) => property.owner_id === landlordId)
.map((property) => property.id);
return e2eMaintenanceRequests.filter((request) =>
propertyIds.includes(request.property_id || "")
);
}
export function getE2EMaintenanceDataByLandlord(
landlordId: string,
): MaintenanceData[] {
return getE2EMaintenanceRequestsByLandlord(landlordId).map((request) => {
const tenant = request.tenant_id ? e2eUsers[request.tenant_id] : null;
const property = request.property_id
? e2eProperties.find((item) => item.id === request.property_id)
: null;
return {
id: request.id,
property_id: request.property_id || "",
tenant_id: request.tenant_id || "",
request_type: request.request_type || null,
description: request.description || null,
status: request.status || null,
priority: request.priority || null,
created_on: request.created_on || BASE_TIME,
resolved_on: request.resolved_on || null,
tenant_name: tenant?.name || "Tenant",
tenant_email: tenant?.email || "",
tenant_phone: tenant?.phone || "",
property_address: property?.address_1 || "",
property_city: property?.city || "",
property_state: property?.state || "",
property_zip: property?.zip || "",
property_title: property?.title || "Property",
};
});
}
export function updateE2EMaintenanceRequest(
requestId: string,
updates: { status?: string; priority?: string },
) {
e2eMaintenanceRequests = e2eMaintenanceRequests.map((request) => {
if (request.id !== requestId) return request;
return {
...request,
status: updates.status ?? request.status,
priority: updates.priority ?? request.priority,
resolved_on: updates.status === "completed"
? BASE_TIME
: request.resolved_on,
};
});
}
export function createE2EMaintenanceRequest(
propertyId: string,
tenantId: string,
requestType: string,
description: string,
priority: string,
) {
const newRequest: MaintenanceRequest = {
id: crypto.randomUUID(),
property_id: propertyId,
tenant_id: tenantId,
request_type: requestType,
description,
status: "open",
priority,
created_on: BASE_TIME,
resolved_on: null,
};
e2eMaintenanceRequests = [newRequest, ...e2eMaintenanceRequests];
return newRequest;
}
export function getE2ETenantApplicationsByLandlord(landlordId: string) {
const propertyIds = e2eProperties
.filter((property) => property.owner_id === landlordId)
.map((property) => property.id);
return e2eTenantApplications
.filter((
app,
): app is TenantApplication & { property_id: string } =>
typeof app.property_id === "string" &&
propertyIds.includes(app.property_id)
)
.map((app) => {
const applicantId = app.applicant_id ?? "";
const applicant = applicantId ? e2eUsers[applicantId] : null;
const property = e2eProperties.find((item) =>
item.id === app.property_id
);
return {
...app,
applicant_name: applicant?.name || "Applicant",
applicant_email: applicant?.email || "",
property_address: property?.address_1 || "",
monthly_income: 6800,
application_notes: "Strong rental history.",
status: app.application_status || "pending",
created_at: app.submitted_on,
document_urls_json: app.document_urls_json ? JSON.stringify(app.document_urls_json) : null,
};
});
}
export function hasE2EApplication(propertyId: string, applicantId: string) {
return e2eTenantApplications.some((app) =>
app.property_id === propertyId && app.applicant_id === applicantId
);
}
export function createE2ETenantApplication(
propertyId: string,
applicantId: string,
applicationData: string,
documentUrlsJson: string | null = null,
) {
const newApplication: TenantApplication = {
id: crypto.randomUUID(),
property_id: propertyId,
applicant_id: applicantId,
application_status: "pending",
application_data_encrypted: applicationData,
document_urls_json: documentUrlsJson ? JSON.parse(documentUrlsJson) as Record<string, unknown> : null,
submitted_on: BASE_TIME,
reviewed_on: null,
};
e2eTenantApplications = [newApplication, ...e2eTenantApplications];
return newApplication;
}
export function updateE2ETenantApplicationStatus(
applicationId: string,
status: "approved" | "rejected",
) {
e2eTenantApplications = e2eTenantApplications.map((app) =>
app.id === applicationId
? { ...app, application_status: status, reviewed_on: BASE_TIME }
: app
);
}
export function addE2ETenantInvite(
landlordId: string,
propertyId: string,
options: { availableOn?: number | null } = {},
) {
const existing = e2eTenantInvites.find((item) =>
item.landlord_id === landlordId && item.property_id === propertyId
);
if (existing) return { ...existing };
const now = BASE_TIME;
const invite: TenantInvite = {
id: crypto.randomUUID(),
landlord_id: landlordId,
property_id: propertyId,
available_on: options.availableOn ?? null,
invited_emails: JSON.stringify([]),
created_on: now,
updated_on: now,
};
e2eTenantInvites = [invite, ...e2eTenantInvites];
return invite;
}
export function getE2ETwitterInviteById(inviteId: string) {
const invite = e2eTenantInvites.find((item) => item.id === inviteId);
return invite ? { ...invite } : null;
}
export function getE2ETwitterInviteByProperty(
landlordId: string,
propertyId: string,
) {
const invite = e2eTenantInvites.find((item) =>
item.landlord_id === landlordId && item.property_id === propertyId
);
return invite ? { ...invite } : null;
}
export function updateE2ETwitterInvite(
inviteId: string,
updates: Partial<TenantInvite>,
) {
let updated: TenantInvite | null = null;
e2eTenantInvites = e2eTenantInvites.map((invite) => {
if (invite.id !== inviteId) return invite;
updated = { ...invite, ...updates, updated_on: BASE_TIME };
return updated;
});
return updated;
}
function parseInviteEmails(rawEmails?: string | null) {
if (!rawEmails) return [];
try {
const parsed = JSON.parse(rawEmails);
if (!Array.isArray(parsed)) return [];
return parsed.filter((entry) => typeof entry === "string");
} catch {
return [];
}
}
export function getE2ETwitterInvitesByEmail(email: string) {
const normalizedEmail = email.trim().toLowerCase();
if (!normalizedEmail) return [];
return e2eTenantInvites.filter((invite) => {
const emails = parseInviteEmails(invite.invited_emails);
return emails.some((entry) => entry.toLowerCase() === normalizedEmail);
}).map((invite) => ({ ...invite }));
}
export function getE2ETenantInvitesByLandlord(landlordId: string) {
return e2eTenantInvites
.filter((invite) => invite.landlord_id === landlordId)
.sort((a, b) => (b.created_on || 0) - (a.created_on || 0));
}
export function deleteE2ETenantInvite(inviteId: string) {
const index = e2eTenantInvites.findIndex((invite) => invite.id === inviteId);
if (index !== -1) {
e2eTenantInvites.splice(index, 1);
}
}
export function getE2EPaymentMethodsByUser(userId: string) {
return e2ePaymentMethods.filter((method) => method.user_id === userId);
}
export function getE2EPaymentTransactionsByLease(leaseId: string) {
return e2ePaymentTransactions.filter((tx) => tx.lease_id === leaseId);
}
export function getE2EPaymentTransactionsByUser(userId: string) {
const leaseIds = e2eLeases
.filter((lease) => lease.tenant_id === userId)
.map((lease) => lease.id);
return e2ePaymentTransactions.filter((tx) =>
leaseIds.includes(tx.lease_id || "")
);
}
export function getE2EPaymentTransactionsByOwner(ownerId: string) {
const leaseIds = e2eLeases
.filter((lease) => lease.owner_id === ownerId)
.map((lease) => lease.id);
return e2ePaymentTransactions.filter((tx) =>
leaseIds.includes(tx.lease_id || "")
);
}
export function getE2EDocumentsByLease(leaseId: string) {
return e2eLeaseDocuments.filter((doc) => doc.lease_id === leaseId);
}
export function getE2EDocumentById(documentId: string) {
return e2eLeaseDocuments.find((doc) => doc.id === documentId) || null;
}
export function getE2EDocumentsByLeaseAndType(
leaseId: string,
documentType: string,
) {
return e2eLeaseDocuments.filter((doc) =>
doc.lease_id === leaseId && doc.document_type === documentType
);
}
export function createE2ELeaseDocument(
leaseId: string,
documentType: string,
fileUrl: string,
fileName: string,
) {
const newDocument: LeaseDocument = {
id: crypto.randomUUID(),
lease_id: leaseId,
document_type: documentType,
file_url: fileUrl,
file_name: fileName,
signed_by_landlord: false,
signed_by_tenant: false,
signature_date: null,
created_on: BASE_TIME,
};
e2eLeaseDocuments = [newDocument, ...e2eLeaseDocuments];
return newDocument;
}
export function deleteE2ELeaseDocument(documentId: string) {
const existing = getE2EDocumentById(documentId);
e2eLeaseDocuments = e2eLeaseDocuments.filter((doc) => doc.id !== documentId);
return existing;
}
export function signE2EDocumentAsLandlord(documentId: string) {
let updated: LeaseDocument | null = null;
e2eLeaseDocuments = e2eLeaseDocuments.map((doc) => {
if (doc.id !== documentId) return doc;
updated = {
...doc,
signed_by_landlord: true,
signature_date: BASE_TIME,
};
return updated;
});
return updated;
}
export function signE2EDocumentAsTenant(documentId: string) {
let updated: LeaseDocument | null = null;
e2eLeaseDocuments = e2eLeaseDocuments.map((doc) => {
if (doc.id !== documentId) return doc;
updated = {
...doc,
signed_by_tenant: true,
signature_date: BASE_TIME,
};
return updated;
});
return updated;
}
export function updateE2EDocumentFile(
documentId: string,
fileUrl: string,
fileName: string,
) {
let updated: LeaseDocument | null = null;
e2eLeaseDocuments = e2eLeaseDocuments.map((doc) => {
if (doc.id !== documentId) return doc;
updated = {
...doc,
file_url: fileUrl,
file_name: fileName,
signed_by_landlord: false,
signed_by_tenant: false,
signature_date: null,
};
return updated;
});
return updated;
}
export function getE2ELandlordDashboardData(userId: string) {
const properties = getE2EPropertiesByOwner(userId);
const leases = getE2ELeasesByOwner(userId);
const tenantApplications = getE2ETenantApplicationsByLandlord(userId);
const maintenanceRequests = getE2EMaintenanceRequestsByLandlord(userId).map(
(request) => ({
...request,
tenant_name: e2eUsers[request.tenant_id || ""]?.name || "Tenant",
tenant_email: e2eUsers[request.tenant_id || ""]?.email || "",
property_address:
getE2EPropertyById(request.property_id || "")?.address_1 || "",
created_at: request.created_on,
}),
);
const recentMessages = getE2EMessagesByUser(userId).map((message) => ({
...message,
sender_name: e2eUsers[message.sender_id || ""]?.name || "Tenant",
created_at: message.created_on,
}));
const monthlyRevenue = properties.reduce(
(sum, property) => sum + (property.monthly_rent || 0),
0,
);
return {
propertyData: {
total: properties.length,
available:
properties.filter((property) => property.rental_status === "available")
.length,
occupied:
properties.filter((property) => property.rental_status === "occupied")
.length,
draft: properties.filter((property) => property.rental_status === "draft")
.length,
},
leaseData: {
active: leases.filter((lease) => lease.lease_status === "active").length,
draft: leases.filter((lease) => lease.lease_status === "draft").length,
expired:
leases.filter((lease) => lease.lease_status === "expired").length,
},
applicationData: {
pending: tenantApplications.filter((app) => app.status === "pending")
.length,
total_this_month: tenantApplications.length,
},
paymentData: {
expected_this_month: leases
.filter((lease) => lease.lease_status === "active")
.reduce((sum, lease) => sum + (lease.rent || 0), 0),
received_this_month: monthlyRevenue,
overdue: 0,
},
properties,
tenantApplications,
maintenanceRequests,
recentMessages,
monthlyRevenue,
leases,
};
}
export function getE2EPendingApprovals() {
return e2eLeases.filter((lease) =>
lease.approval_status === "pending_approval" || lease.approval_status === "pending" || lease.approval_status === "requested"
);
}
export function addE2ELeaseWithApproval(
propertyId: string,
tenantId: string,
ownerId: string,
approvalStatus: string = "pending",
) {
const newLease: Lease = {
id: crypto.randomUUID(),
property_id: propertyId,
property_address: getE2EPropertyById(propertyId)?.address_1 || "",
tenant_id: tenantId,
owner_id: ownerId,
start_date: BASE_TIME + 1000 * 60 * 60 * 24 * 7,
end_date: BASE_TIME + 1000 * 60 * 60 * 24 * 377,
rent: 2000,
security_deposit: 2000,
lease_status: "draft",
approval_status: approvalStatus,
approval_requested_at: approvalStatus === "pending" ? BASE_TIME : null,
created_on: BASE_TIME,
updated_on: BASE_TIME,
};
e2eLeases = [newLease, ...e2eLeases];
return newLease;
}