diff --git a/db/interfaces.ts b/db/interfaces.ts old mode 100755 new mode 100644 index c4dd030..17c2ca1 --- a/db/interfaces.ts +++ b/db/interfaces.ts @@ -155,6 +155,11 @@ export interface Lease { payment_schedule_id?: string | null; late_fee_rules?: string | null; auto_renewal?: boolean | null; + approval_status?: string | null; + approval_requested_at?: number | null; + approved_at?: number | null; + approved_by?: string | null; + rejection_reason?: string | null; encrypted_data?: string | null; created_on?: number | null; updated_on?: number | null; diff --git a/db/schema.ts b/db/schema.ts index 006538b..e460ea7 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -182,6 +182,11 @@ export const leases = pgTable("leases", { ), late_fee_rules: text("late_fee_rules"), // JSON string of late fee rules auto_renewal: boolean("auto_renewal").default(false), + approval_status: text("approval_status").default("pending_approval"), // 'pending_approval', 'approved', 'rejected' + approval_requested_at: bigint("approval_requested_at", { mode: "number" }), // Timestamp when tenant requested approval + approved_at: bigint("approved_at", { mode: "number" }), // Timestamp when landlord approved + approved_by: text("approved_by").references(() => users.id), // User ID of landlord who approved + rejection_reason: text("rejection_reason"), // Reason for rejection if rejected encrypted_data: text("encrypted_data"), // Encrypted lease information created_on: bigint("created_on", { mode: "number" }), updated_on: bigint("updated_on", { mode: "number" }), diff --git a/drizzle/0008_add_lease_approval_fields.sql b/drizzle/0008_add_lease_approval_fields.sql new file mode 100644 index 0000000..e3906b4 --- /dev/null +++ b/drizzle/0008_add_lease_approval_fields.sql @@ -0,0 +1,34 @@ +-- Migration: Add lease approval workflow fields to leases table + +ALTER TABLE "leases" ADD COLUMN "approval_status" TEXT DEFAULT 'pending_approval'; + +--> statement-breakpoint + +ALTER TABLE "leases" ADD COLUMN "approval_requested_at" BIGINT; + +--> statement-breakpoint + +ALTER TABLE "leases" ADD COLUMN "approved_at" BIGINT; + +--> statement-breakpoint + +ALTER TABLE "leases" ADD COLUMN "approved_by" TEXT; + +--> statement-breakpoint + +ALTER TABLE "leases" ADD COLUMN "rejection_reason" TEXT; + +--> statement-breakpoint + +CREATE INDEX IF NOT EXISTS "leases_approval_status_idx" + ON "leases" ("approval_status"); + +--> statement-breakpoint + +CREATE INDEX IF NOT EXISTS "leases_approved_by_idx" + ON "leases" ("approved_by"); + +--> statement-breakpoint + +ALTER TABLE "leases" ADD CONSTRAINT "leases_approved_by_users_id_fk" + FOREIGN KEY ("approved_by") REFERENCES "users"("id") ON DELETE SET NULL; diff --git a/routes/api/lease/approve.ts b/routes/api/lease/approve.ts new file mode 100644 index 0000000..410c1f2 --- /dev/null +++ b/routes/api/lease/approve.ts @@ -0,0 +1,102 @@ +// routes/api/lease/approve.ts +import { define } from "../../../utils.ts"; +import { page } from "fresh"; +import { SessionCookie } from "../../auth/callback.ts"; +import { approveLease, getLeaseById } from "../../../services/leaseService.ts"; + +export const handler = define.handlers({ + async POST(ctx) { + try { + const stateData: SessionCookie = ctx.state.data; + const { user_id } = stateData; + + if (!user_id) { + return new Response( + JSON.stringify({ error: "Unauthorized" }), + { + status: 401, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const contentType = ctx.req.headers.get("content-type") || ""; + let data: Record = {}; + + if (contentType.includes("application/json")) { + data = await ctx.req.json(); + } else { + const formData = await ctx.req.formData(); + for (const [key, value] of formData.entries()) { + data[key] = typeof value === "string" ? value : String(value); + } + } + + const lease_id = (data.lease_id as string) || + (data["lease_id"] as string); + + if (!lease_id) { + return new Response( + JSON.stringify({ error: "lease_id is required" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const lease = await getLeaseById(lease_id); + if (!lease) { + return new Response( + JSON.stringify({ error: "Lease not found" }), + { + status: 404, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + if (lease.owner_id !== user_id) { + return new Response( + JSON.stringify({ error: "Unauthorized: You do not own this lease" }), + { + status: 403, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const result = await approveLease(lease_id, user_id); + + if (!result || result.length === 0) { + return new Response( + JSON.stringify({ error: "Failed to approve lease" }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + return new Response( + JSON.stringify({ + success: true, + lease: result[0], + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + } catch (error) { + console.error("Error approving lease:", error); + return new Response( + JSON.stringify({ error: String(error) }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); + } + }, +}); diff --git a/routes/api/lease/pending-approvals.ts b/routes/api/lease/pending-approvals.ts new file mode 100644 index 0000000..d5e7de0 --- /dev/null +++ b/routes/api/lease/pending-approvals.ts @@ -0,0 +1,114 @@ +// routes/api/lease/pending-approvals.ts +import { define } from "../../../utils.ts"; +import { page } from "fresh"; +import { SessionCookie } from "../../auth/callback.ts"; +import { getPendingApprovals } from "../../../services/leaseService.ts"; +import { properties, users } from "../../../db/schema.ts"; +import { and, eq } from "drizzle-orm"; +import { db } from "../../../db/db.ts"; + +export const handler = define.handlers({ + async GET(ctx) { + try { + const stateData: SessionCookie = ctx.state.data; + const { user_id } = stateData; + + if (!user_id) { + return new Response( + JSON.stringify({ error: "Unauthorized" }), + { + status: 401, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const pendingLeases = await getPendingApprovals(user_id); + + if (!pendingLeases || pendingLeases.length === 0) { + return new Response( + JSON.stringify({ + success: true, + approvals: [], + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const propertyIds = pendingLeases + .map((lease) => lease.property_id) + .filter((id): id is string => Boolean(id)); + + const tenantIds = pendingLeases + .map((lease) => lease.tenant_id) + .filter((id): id is string => Boolean(id)); + + const propertiesData = propertyIds.length > 0 + ? await db + .select({ + id: properties.id, + title: properties.title, + address_1: properties.address_1, + address_2: properties.address_2, + city: properties.city, + state: properties.state, + zip: properties.zip, + image_url: properties.image_url, + monthly_rent: properties.monthly_rent, + bedrooms: properties.bedrooms, + bathrooms: properties.bathrooms, + }) + .from(properties) + .where(eq(properties.id, propertyIds[0])) + : []; + + const tenantsData = tenantIds.length > 0 + ? await db + .select({ + id: users.id, + name: users.name, + email: users.email, + phone: users.phone, + }) + .from(users) + .where(eq(users.id, tenantIds[0])) + : []; + + const propertyMap = new Map( + propertiesData.map((p) => [p.id, p]), + ); + const tenantMap = new Map( + tenantsData.map((t) => [t.id, t]), + ); + + const enrichedApprovals = pendingLeases.map((lease) => ({ + ...lease, + property: lease.property_id ? propertyMap.get(lease.property_id) : null, + tenant: lease.tenant_id ? tenantMap.get(lease.tenant_id) : null, + })); + + return new Response( + JSON.stringify({ + success: true, + approvals: enrichedApprovals, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + } catch (error) { + console.error("Error fetching pending approvals:", error); + return new Response( + JSON.stringify({ error: String(error) }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); + } + }, +}); diff --git a/routes/api/lease/reject.ts b/routes/api/lease/reject.ts new file mode 100644 index 0000000..8ffb88a --- /dev/null +++ b/routes/api/lease/reject.ts @@ -0,0 +1,104 @@ +// routes/api/lease/reject.ts +import { define } from "../../../utils.ts"; +import { page } from "fresh"; +import { SessionCookie } from "../../auth/callback.ts"; +import { rejectLease, getLeaseById } from "../../../services/leaseService.ts"; + +export const handler = define.handlers({ + async POST(ctx) { + try { + const stateData: SessionCookie = ctx.state.data; + const { user_id } = stateData; + + if (!user_id) { + return new Response( + JSON.stringify({ error: "Unauthorized" }), + { + status: 401, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const contentType = ctx.req.headers.get("content-type") || ""; + let data: Record = {}; + + if (contentType.includes("application/json")) { + data = await ctx.req.json(); + } else { + const formData = await ctx.req.formData(); + for (const [key, value] of formData.entries()) { + data[key] = typeof value === "string" ? value : String(value); + } + } + + const lease_id = (data.lease_id as string) || + (data["lease_id"] as string); + const rejection_reason = (data.rejection_reason as string) || + (data["rejection_reason"] as string); + + if (!lease_id) { + return new Response( + JSON.stringify({ error: "lease_id is required" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const lease = await getLeaseById(lease_id); + if (!lease) { + return new Response( + JSON.stringify({ error: "Lease not found" }), + { + status: 404, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + if (lease.owner_id !== user_id) { + return new Response( + JSON.stringify({ error: "Unauthorized: You do not own this lease" }), + { + status: 403, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const result = await rejectLease(lease_id, user_id, rejection_reason); + + if (!result || result.length === 0) { + return new Response( + JSON.stringify({ error: "Failed to reject lease" }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + return new Response( + JSON.stringify({ + success: true, + lease: result[0], + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + } catch (error) { + console.error("Error rejecting lease:", error); + return new Response( + JSON.stringify({ error: String(error) }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); + } + }, +}); diff --git a/routes/api/lease/request-approval.ts b/routes/api/lease/request-approval.ts new file mode 100644 index 0000000..41acc74 --- /dev/null +++ b/routes/api/lease/request-approval.ts @@ -0,0 +1,123 @@ +// routes/api/lease/request-approval.ts +import { define } from "../../../utils.ts"; +import { page } from "fresh"; +import { SessionCookie } from "../../auth/callback.ts"; +import { + requestLeaseApproval, + getLeaseById, +} from "../../../services/leaseService.ts"; +import { + getTenantInviteById, + updateTenantInvite, +} from "../../../services/tenantInviteService.ts"; +import { getPropertyById } from "../../../services/propertyService.ts"; + +export const handler = define.handlers({ + async POST(ctx) { + try { + const stateData: SessionCookie = ctx.state.data; + const { user_id } = stateData; + + if (!user_id) { + return new Response( + JSON.stringify({ error: "Unauthorized" }), + { + status: 401, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const contentType = ctx.req.headers.get("content-type") || ""; + let data: Record = {}; + + if (contentType.includes("application/json")) { + data = await ctx.req.json(); + } else { + const formData = await ctx.req.formData(); + for (const [key, value] of formData.entries()) { + data[key] = typeof value === "string" ? value : String(value); + } + } + + const invite_id = (data.invite_id as string) || + (data["invite_id"] as string); + const property_id = (data.property_id as string) || + (data["property_id"] as string); + + if (!invite_id || !property_id) { + return new Response( + JSON.stringify({ + error: "invite_id and property_id are required", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const invite = await getTenantInviteById(invite_id); + if (!invite) { + return new Response( + JSON.stringify({ error: "Invite not found" }), + { + status: 404, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const property = await getPropertyById(property_id); + if (!property) { + return new Response( + JSON.stringify({ error: "Property not found" }), + { + status: 404, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const leaseResult = await requestLeaseApproval( + user_id, + property_id, + invite_id, + ); + + if (!leaseResult || leaseResult.length === 0) { + return new Response( + JSON.stringify({ error: "Failed to create lease request" }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + const lease = leaseResult[0]; + + await updateTenantInvite(invite_id, { lease_id: lease.id }); + + return new Response( + JSON.stringify({ + success: true, + lease, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + } catch (error) { + console.error("Error requesting lease approval:", error); + return new Response( + JSON.stringify({ error: String(error) }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); + } + }, +}); diff --git a/routes/tenant/invite/[invite_id].tsx b/routes/tenant/invite/[invite_id].tsx index 6795b4b..888d861 100644 --- a/routes/tenant/invite/[invite_id].tsx +++ b/routes/tenant/invite/[invite_id].tsx @@ -1,10 +1,6 @@ import { define } from "../../../utils.ts"; import { page } from "fresh"; import type { SessionCookie } from "../../auth/callback.ts"; -import { - createTenantApplication, - hasUserAppliedForProperty, -} from "../../../services/tenantApplicationService.ts"; import { markRoleOnboarded, startGuestOnboarding, @@ -16,12 +12,14 @@ import { removeTenantInviteEmail, } from "../../../services/tenantInviteService.ts"; import { getUserById } from "../../../services/userService.ts"; +import { getLeaseById } from "../../../services/leaseService.ts"; import LoginPurposeButton from "../../../islands/LoginPurposeButton.tsx"; import type { Property, PropertyMedia, TenantInvite, User, + Lease, } from "../../../db/interfaces.ts"; type InvitePreviewData = { @@ -32,6 +30,7 @@ type InvitePreviewData = { inviteeEmail: string; status?: string; error?: string; + lease?: Lease | null; }; function buildPropertyLabel(property: Property | null) { @@ -84,6 +83,7 @@ async function buildInvitePreview( : null; const photos = property?.id ? await getPhotosByProperty(property.id) : []; const inviteeEmail = user?.email?.trim() || ""; + const lease = invite?.lease_id ? await getLeaseById(invite.lease_id) : null; return page({ invite, @@ -92,6 +92,7 @@ async function buildInvitePreview( user, inviteeEmail, status, + lease, error: options.error, }); } @@ -130,23 +131,28 @@ export const handler = define.handlers({ } await startGuestOnboarding(userId, "tenant"); - const alreadyApplied = await hasUserAppliedForProperty( - invite.property_id, - userId, - ); - if (!alreadyApplied) { - const payload = JSON.stringify({ - source: "invite", + + const response = await fetch("/api/lease/request-approval", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ invite_id: invite.id, - requested_at: Date.now(), + property_id: invite.property_id, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: "Failed to request lease approval" })); + return await buildInvitePreview(ctx, { + error: errorData.error || "Failed to request lease approval", }); - await createTenantApplication(invite.property_id, userId, payload); } + await markRoleOnboarded(userId, "tenant"); return new Response("", { status: 302, - headers: { Location: "/tenant/dashboard" }, + headers: { Location: `/tenant/invite/${encodeURIComponent(invite.id)}?status=pending` }, }); } @@ -190,9 +196,16 @@ export default define.page(function TenantInvitePreview({ data }) { user, inviteeEmail, status, + lease, error, } = data as InvitePreviewData; + const approvalStatus = lease?.approval_status; + const isPending = approvalStatus === "pending_approval"; + const isApproved = approvalStatus === "approved"; + const isRejected = approvalStatus === "rejected"; + const hasLease = Boolean(lease); + if (!invite || !property) { return (
@@ -234,6 +247,20 @@ export default define.page(function TenantInvitePreview({ data }) { const cancelHref = user?.id ? "/tenant/dashboard" : "/"; const isLoggedIn = Boolean(user?.id); + const approvalSteps = [ + { label: "Request Lease", completed: hasLease }, + { label: "Landlord Review", completed: isApproved }, + { label: "Lease Active", completed: isApproved }, + ]; + + const getStepStatus = (stepIndex: number) => { + if (!hasLease) return stepIndex === 0 ? "current" : "pending"; + if (isPending) return stepIndex <= 1 ? "current" : "pending"; + if (isApproved) return stepIndex <= 2 ? "current" : "completed"; + if (isRejected) return stepIndex === 1 ? "error" : "completed"; + return "pending"; + }; + return (
@@ -403,14 +430,171 @@ export default define.page(function TenantInvitePreview({ data }) {
+
+

+ Approval Status +

+ {hasLease && ( +
+
+ {approvalSteps.map((step, index) => ( +
+
+ {index + 1} +
+ {index < approvalSteps.length - 1 && ( +
+ )} +
+ ))} +
+
+ {isPending && ( +
+
+ + + + Pending Landlord Approval +
+

+ Your landlord has been notified and will review your lease request. You will receive an email once a decision is made. +

+
+ )} + {isApproved && ( +
+
+ + + + Lease Approved +
+

+ Congratulations! Your lease has been approved. You can now access your tenant portal and view your lease details. +

+
+ )} + {isRejected && ( +
+
+ + + + Lease Rejected +
+

+ Your landlord has declined this lease request. If you have questions or would like to discuss this decision, please contact your landlord directly. +

+ {lease?.rejection_reason && ( +
+

+ Reason for rejection +

+

+ {lease.rejection_reason} +

+
+ )} +
+ )} +
+
+ )} + {!hasLease && ( +
+

+ Click "Start Lease" to request landlord approval for this property. +

+
+ )} +
+

What happens next

    -
  • - Start the lease to submit your tenant application.
  • -
  • - Your landlord will review and respond.
  • -
  • - Track your status in the tenant portal.
  • + {!hasLease && ( + <> +
  • - Click "Start Lease" to request landlord approval.
  • +
  • - Your landlord will review your request.
  • +
  • - Track your approval status on this page.
  • + + )} + {isPending && ( + <> +
  • - Your landlord has been notified of your request.
  • +
  • - Wait for landlord to review and approve/reject.
  • +
  • - You will receive an email notification with a decision.
  • + + )} + {isApproved && ( + <> +
  • - Your lease has been approved and activated.
  • +
  • - Access your tenant portal to view lease details.
  • +
  • - Set up payment methods and start paying rent.
  • + + )} + {isRejected && ( + <> +
  • - Your lease request was declined.
  • +
  • - Contact your landlord if you have questions.
  • +
  • - You can request a new invite from your landlord.
  • + + )}
@@ -423,33 +607,43 @@ export default define.page(function TenantInvitePreview({ data }) { Ready to proceed?

- Start your lease, decline the invite, or come back later. + {!hasLease && "Start your lease request, decline the invite, or come back later."} + {isPending && "Your request is pending landlord review. Check back here for updates."} + {isApproved && "Your lease is approved! Continue to your tenant portal."} + {isRejected && "Your lease request was declined. Contact your landlord for more information."}

- {isLoggedIn - ? ( -
- - -
- ) - : ( - + + + + )} + {isApproved && isLoggedIn && ( + + Continue to Tenant Portal + + )} + {!isLoggedIn && ( + + Sign In to Start Lease + + )}
diff --git a/services/landlordService.ts b/services/landlordService.ts index a77574b..845fa32 100644 --- a/services/landlordService.ts +++ b/services/landlordService.ts @@ -30,6 +30,7 @@ import { getE2ELandlordDashboardData, getE2EPaymentMethodsByUser, getE2EPropertiesByOwner, + getE2EPendingApprovals, getE2EUserById, isE2EEnabled, } from "./e2eFixtures.ts"; @@ -210,6 +211,81 @@ export async function getLandlordOnboardingStatus( }; } +// Get pending lease approvals for a landlord +export async function getPendingLeaseApprovals( + landlord_id: string, +) { + if (isE2EEnabled()) { + return getE2EPendingApprovals(landlord_id); + } + + const pendingLeases = await db.select() + .from(leases) + .where( + and( + eq(leases.owner_id, landlord_id), + eq(leases.approval_status, "pending_approval"), + ), + ) + .orderBy(leases.approval_requested_at); + + if (pendingLeases.length === 0) { + return []; + } + + const propertyIds = pendingLeases + .map((lease) => lease.property_id) + .filter((id): id is string => Boolean(id)); + + const tenantIds = pendingLeases + .map((lease) => lease.tenant_id) + .filter((id): id is string => Boolean(id)); + + const propertiesData = propertyIds.length > 0 + ? await db + .select({ + id: properties.id, + title: properties.title, + address_1: properties.address_1, + address_2: properties.address_2, + city: properties.city, + state: properties.state, + zip: properties.zip, + image_url: properties.image_url, + monthly_rent: properties.monthly_rent, + bedrooms: properties.bedrooms, + bathrooms: properties.bathrooms, + }) + .from(properties) + .where(eq(properties.id, propertyIds[0])) + : []; + + const tenantsData = tenantIds.length > 0 + ? await db + .select({ + id: users.id, + name: users.name, + email: users.email, + phone: users.phone, + }) + .from(users) + .where(eq(users.id, tenantIds[0])) + : []; + + const propertyMap = new Map( + propertiesData.map((p) => [p.id, p]), + ); + const tenantMap = new Map( + tenantsData.map((t) => [t.id, t]), + ); + + return pendingLeases.map((lease) => ({ + ...lease, + property: lease.property_id ? propertyMap.get(lease.property_id) : null, + tenant: lease.tenant_id ? tenantMap.get(lease.tenant_id) : null, + })); +} + // Get landlord dashboard data export async function getLandlordDashboardData( user_id: string, diff --git a/services/leaseService.ts b/services/leaseService.ts index 2fc0601..5aa4618 100644 --- a/services/leaseService.ts +++ b/services/leaseService.ts @@ -10,6 +10,8 @@ import { getE2ELeasesByOwner, getE2ELeasesByTenant, isE2EEnabled, + getE2EPendingApprovals, + addE2ELeaseWithApproval, } from "./e2eFixtures.ts"; // export interface Lease { @@ -263,8 +265,8 @@ export async function getLeaseStatsByOwner(owner_id: string) { /** * Link a lease to a tenant invite. If an inviteId is provided, update that * tenant_invites row to reference this lease. Otherwise attempt to find an - * existing invite by the lease's property and owner and attach the lease_id - * there; if none exists, create a new tenant_invites row linking to the lease. + * existing invite by: lease's property and owner and attach lease_id + * there; if none exists, create a new tenant_invites row linking to: lease. */ export async function inviteLease(lease_id: string, inviteId?: string) { // If an explicit inviteId was provided, update that invite record @@ -276,7 +278,7 @@ export async function inviteLease(lease_id: string, inviteId?: string) { .returning(); } - // Otherwise derive the property and landlord from the lease + // Otherwise derive of property and landlord from lease const lease = await getLeaseById(lease_id); if (!lease) return null; @@ -314,3 +316,134 @@ export async function inviteLease(lease_id: string, inviteId?: string) { updated_on: now, }).returning(); } + +// Request lease approval from tenant +export async function requestLeaseApproval( + tenant_id: string, + property_id: string, + invite_id: string, +) { + if (isE2EEnabled()) { + return [addE2ELeaseWithApproval(tenant_id, property_id, invite_id)]; + } + + const now = Date.now(); + return await db.insert(leases) + .values({ + id: crypto.randomUUID(), + title: `Lease for Property ${property_id}`, + property_id, + tenant_id, + owner_id: null, + start_date: now, + end_date: now + 365 * 24 * 60 * 60 * 1000, + rent: null, + security_deposit: null, + lease_status: "draft", + approval_status: "pending_approval", + approval_requested_at: now, + approved_at: null, + approved_by: null, + rejection_reason: null, + lease_document_url: null, + payment_schedule_id: null, + late_fee_rules: null, + auto_renewal: false, + encrypted_data: null, + created_on: now, + updated_on: now, + }) + .returning(); +} + +// Approve a lease request +export async function approveLease( + lease_id: string, + landlord_id: string, +) { + if (isE2EEnabled()) { + const lease = getE2ELeaseById(lease_id); + if (lease && lease.owner_id === landlord_id) { + const now = Date.now(); + return [{ + ...lease, + approval_status: "approved", + approved_at: now, + approved_by: landlord_id, + lease_status: "active", + updated_on: now, + }]; + } + return []; + } + + const now = Date.now(); + return await db.update(leases) + .set({ + approval_status: "approved", + approved_at: now, + approved_by: landlord_id, + lease_status: "active", + updated_on: now, + }) + .where( + and( + eq(leases.id, lease_id), + eq(leases.owner_id, landlord_id), + ), + ) + .returning(); +} + +// Reject a lease request +export async function rejectLease( + lease_id: string, + landlord_id: string, + rejection_reason?: string, +) { + if (isE2EEnabled()) { + const lease = getE2ELeaseById(lease_id); + if (lease && lease.owner_id === landlord_id) { + const now = Date.now(); + return [{ + ...lease, + approval_status: "rejected", + rejection_reason: rejection_reason || null, + updated_on: now, + }]; + } + return []; + } + + const now = Date.now(); + return await db.update(leases) + .set({ + approval_status: "rejected", + rejection_reason: rejection_reason || null, + updated_on: now, + }) + .where( + and( + eq(leases.id, lease_id), + eq(leases.owner_id, landlord_id), + ), + ) + .returning(); +} + +// Get pending lease approvals for a landlord +export async function getPendingApprovals(landlord_id: string) { + if (isE2EEnabled()) { + return getE2EPendingApprovals(landlord_id); + } + + return await db.select() + .from(leases) + .where( + and( + eq(leases.owner_id, landlord_id), + eq(leases.approval_status, "pending_approval"), + ), + ) + .orderBy(leases.approval_requested_at); +}