182 lines
5.9 KiB
TypeScript
182 lines
5.9 KiB
TypeScript
// routes/api/update/tenant_invite.ts
|
|
import { define } from "../../../utils.ts";
|
|
import { getPropertyById } from "../../../services/propertyService.ts";
|
|
import {
|
|
createTenantInvite,
|
|
getTenantInviteById,
|
|
getTenantInviteByProperty,
|
|
updateTenantInvite,
|
|
} from "../../../services/tenantInviteService.ts";
|
|
import { SessionCookie } from "../../auth/callback.ts";
|
|
import type { TenantInvite } from "../../../db/interfaces.ts";
|
|
|
|
function parseInvitedEmails(input: unknown) {
|
|
if (Array.isArray(input)) {
|
|
return input.map((item) => String(item).trim()).filter(Boolean);
|
|
}
|
|
if (typeof input === "string") {
|
|
return input.split(/[\n,]+/).map((item) => item.trim()).filter(Boolean);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function parseStoredEmails(input: string | null | undefined) {
|
|
if (!input) return [];
|
|
try {
|
|
const parsed = JSON.parse(input);
|
|
if (Array.isArray(parsed)) {
|
|
return parsed.map((item) => String(item).trim()).filter(Boolean);
|
|
}
|
|
} catch (_error) {
|
|
// Fall back to delimited parsing.
|
|
}
|
|
return parseInvitedEmails(input);
|
|
}
|
|
|
|
function mergeEmails(existing: string[], incoming: string[]) {
|
|
const seen = new Set<string>();
|
|
const merged: string[] = [];
|
|
for (const email of [...existing, ...incoming]) {
|
|
const key = email.toLowerCase();
|
|
if (seen.has(key)) continue;
|
|
seen.add(key);
|
|
merged.push(email);
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
function parseAvailableOn(input: unknown) {
|
|
if (typeof input === "number") return input;
|
|
if (typeof input === "string" && input.trim() !== "") {
|
|
const parsed = Number(input);
|
|
if (!Number.isNaN(parsed)) return parsed;
|
|
const parsedDate = Date.parse(input);
|
|
return Number.isNaN(parsedDate) ? null : parsedDate;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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<string, unknown> = {};
|
|
|
|
if (contentType.includes("application/json")) {
|
|
data = await ctx.req.json();
|
|
} else {
|
|
const formData = await ctx.req.formData();
|
|
data = {
|
|
invite_id: formData.get("invite_id")?.toString() || "",
|
|
property_id: formData.get("property_id")?.toString() || "",
|
|
available_on: formData.get("available_on")?.toString() || "",
|
|
invited_emails: formData.get("invited_emails")?.toString() || "",
|
|
};
|
|
}
|
|
|
|
const inviteId = (data.invite_id || data.id || "").toString();
|
|
const propertyId = (data.property_id || "").toString();
|
|
const availableOn = parseAvailableOn(data.available_on);
|
|
const invitedEmails = parseInvitedEmails(data.invited_emails);
|
|
const hasAvailableOn = Object.prototype.hasOwnProperty.call(
|
|
data,
|
|
"available_on",
|
|
);
|
|
const hasInvitedEmails = Object.prototype.hasOwnProperty.call(
|
|
data,
|
|
"invited_emails",
|
|
);
|
|
|
|
if (!inviteId && !propertyId) {
|
|
return new Response(
|
|
JSON.stringify({ error: "Invite ID or property ID is required" }),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
|
|
let invite = inviteId
|
|
? await getTenantInviteById(inviteId)
|
|
: await getTenantInviteByProperty(user_id, propertyId);
|
|
|
|
if (inviteId && (!invite || invite.landlord_id !== user_id)) {
|
|
return new Response(
|
|
JSON.stringify({ error: "Invite not found" }),
|
|
{ status: 404, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
|
|
if (propertyId) {
|
|
const property = await getPropertyById(propertyId);
|
|
if (!property || property.owner_id !== user_id) {
|
|
return new Response(
|
|
JSON.stringify({ error: "Invalid property selection" }),
|
|
{ status: 403, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
}
|
|
|
|
if (
|
|
invite && propertyId && invite.property_id &&
|
|
invite.property_id !== propertyId
|
|
) {
|
|
const propertyInvite = await getTenantInviteByProperty(
|
|
user_id,
|
|
propertyId,
|
|
);
|
|
if (propertyInvite) {
|
|
invite = propertyInvite;
|
|
}
|
|
}
|
|
|
|
if (!invite && propertyId) {
|
|
const created = await createTenantInvite(user_id, propertyId, {
|
|
availableOn,
|
|
});
|
|
invite = created[0] || null;
|
|
} else if (!invite && !propertyId) {
|
|
return new Response(
|
|
JSON.stringify({ error: "Invite not found" }),
|
|
{ status: 404, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
|
|
let result = invite ? [invite] : [];
|
|
if (invite && (propertyId || hasAvailableOn || hasInvitedEmails)) {
|
|
const updates: Partial<TenantInvite> = {};
|
|
if (propertyId) updates.property_id = propertyId;
|
|
if (hasAvailableOn) updates.available_on = availableOn;
|
|
if (hasInvitedEmails) {
|
|
const existingEmails = parseStoredEmails(invite.invited_emails);
|
|
const mergedEmails = mergeEmails(existingEmails, invitedEmails);
|
|
updates.invited_emails = JSON.stringify(mergedEmails);
|
|
}
|
|
result = Object.keys(updates).length > 0
|
|
? await updateTenantInvite(invite.id, updates)
|
|
: [invite];
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify({ success: true, invite: result[0] || null }),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
} catch (error) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: error instanceof Error ? error.message : error,
|
|
}),
|
|
{ status: 500, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
},
|
|
});
|