diff --git a/CLAUDE.md b/CLAUDE.md index 3debd3ff6..4d7de616b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -234,7 +234,7 @@ OpenInspection runs as ONE Cloudflare Worker (cloudflare/react-router-hono-fulls - **iat claim**: `signJwt()` auto-injects `iat: Math.floor(Date.now() / 1000)` when the caller omits it. Without `iat`, KV session invalidation (`pwchanged:{userId}`) cannot work. - **No HS256 fallback**: There is NO legacy HS256 path. Pre-launch architectural choice — see rotation scripts and docs. The remaining `JWT_SECRET` env binding is KDF input only, never a JWT signing key. State it that way rather than listing its consumers: there are dozens of files reading it (secret crypto, signed-URL and session HMACs, token derivation), and every enumeration written here has gone stale. - **Key rotation flow**: To rotate, provision `JWT_PRIVATE_KEY_V` + `JWT_PUBLIC_KEY_V` first (verify-only window), then flip `JWT_CURRENT_KID` to the new version. Old tokens remain verifiable until V is retired. -- **Token NOT in response body — one carved-out exception.** Login, setup and join MUST NOT return the JWT to a browser; delivery is `Set-Cookie` (HttpOnly). The exception is the in-process BFF, which identifies itself with `x-token-relay: 1` and then receives `token` in the body, because Workers `fetch()` may strip `Set-Cookie` on a server-to-server hop and the BFF has to write its own session cookie. A browser never sends that header. Do not add a second exception without one here. +- **Token NOT in response body — one carved-out exception.** No auth endpoint returns the JWT to a browser; delivery is `Set-Cookie` (HttpOnly). The exception is login only — both the password step (`POST /api/auth/login`) and the second-factor step (`POST /api/auth/login/2fa`): a caller sending `x-token-relay: 1` also receives `token` in the body, because the in-process BFF client does not forward the API's `Set-Cookie` to the browser and the login page has to write its own session cookie (`LoginResponseSchema` / `Login2faResponseSchema` declare it). A browser never sends that header. Setup and join are NOT part of the exception: they never put the JWT in the body, whatever the headers, and their pages read the session JWT out of the `Set-Cookie` header on the in-process API response instead. Do not add a second exception without one here. - **Cookie name**: Always use `__Host-inspector_token` (enforces `Secure`, `Path=/`, no `Domain`). - **setCookie attributes**: Every `setCookie()` MUST include `httpOnly: true, secure: true, sameSite: 'Strict', path: '/'`. - **deleteCookie secure**: Every `deleteCookie()` MUST include `{ path: '/', secure: true }`. Omitting `secure` on `__Host-` cookies throws a runtime exception. diff --git a/app/components/OutboundVerification.test.tsx b/app/components/OutboundVerification.test.tsx new file mode 100644 index 000000000..d2b74dd91 --- /dev/null +++ b/app/components/OutboundVerification.test.tsx @@ -0,0 +1,137 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { act, fireEvent, render, screen } from "@testing-library/react"; + +let emitToken: ((t: string) => void) | null = null; +vi.mock("~/lib/turnstile", () => ({ + useTurnstileWidget: (_k: unknown, _r: unknown, _s: unknown, onToken: (t: string) => void) => { + emitToken = onToken; + }, +})); + +import { OutboundVerificationProvider, useChallengeOnRefusal, useEnsureHuman } from "./OutboundVerification"; + +function Probe({ onResult }: { onResult: (ok: boolean) => void }) { + const ensureHuman = useEnsureHuman(); + return ; +} + +const setup = (windowOpen: boolean, siteKey: string | null = "site-key") => { + const onResult = vi.fn(); + render( + + + , + ); + return onResult; +}; + +const click = async () => { await act(async () => { fireEvent.click(screen.getByText("send")); }); }; + +beforeEach(() => { + emitToken = null; + document.cookie = "oi-human-token=; Max-Age=0; Path=/"; +}); + +describe("OutboundVerificationProvider", () => { + it("resolves true at once, with no dialog, outside the cooling window", async () => { + const onResult = setup(false); + await click(); + expect(onResult).toHaveBeenCalledWith(true); + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + it("opens the challenge inside the window and resolves true once a token is solved, leaving it for the next request", async () => { + const onResult = setup(true); + await click(); + expect(screen.getByRole("dialog")).toBeTruthy(); + expect(onResult).not.toHaveBeenCalled(); + await act(async () => emitToken!("0.token-value-1234567890")); + expect(onResult).toHaveBeenCalledWith(true); + expect(document.cookie).toContain("oi-human-token="); + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + it("resolves false and stores nothing when the person cancels", async () => { + const onResult = setup(true); + await click(); + await act(async () => { fireEvent.click(screen.getByRole("button", { name: "Cancel" })); }); + expect(onResult).toHaveBeenCalledWith(false); + expect(document.cookie).not.toContain("oi-human-token=0"); + }); + + it("lets the server decide when there is no site key", async () => { + const onResult = setup(true, null); + await click(); + expect(onResult).toHaveBeenCalledWith(true); + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + it("tells the first caller 'not verified' when a second request replaces its dialog", async () => { + const first = vi.fn(); + const second = vi.fn(); + function Two() { + const ensureHuman = useEnsureHuman(); + return ( + <> + + + + ); + } + render( + + + , + ); + await act(async () => { fireEvent.click(screen.getByText("one")); }); + await act(async () => { fireEvent.click(screen.getByText("two")); }); + expect(first).toHaveBeenCalledWith(false); + await act(async () => emitToken!("0.token-value-1234567890")); + expect(second).toHaveBeenCalledWith(true); + }); +}); + +describe("useChallengeOnRefusal", () => { + function Page({ data, resubmit }: { data?: { verifySiteKey?: string }; resubmit: () => void }) { + const challenge = useChallengeOnRefusal(data, "idle", resubmit); + return
{challenge.dialog}{String(challenge.blocked)}
; + } + + it("shows nothing for a normal response", () => { + render(); + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + it("opens the check for a refusal and sends again once it is passed", async () => { + const resubmit = vi.fn(); + render(); + expect(screen.getByRole("dialog")).toBeTruthy(); + await act(async () => emitToken!("0.token-value-1234567890")); + expect(resubmit).toHaveBeenCalledOnce(); + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + it("does not send again when the person cancels", async () => { + const resubmit = vi.fn(); + render(); + await act(async () => { fireEvent.click(screen.getByRole("button", { name: "Cancel" })); }); + expect(resubmit).not.toHaveBeenCalled(); + }); + + it("does not loop: a refusal straight after a passed check is left to the page's error", async () => { + const resubmit = vi.fn(); + const { rerender } = render(); + await act(async () => emitToken!("0.token-value-1234567890")); + expect(resubmit).toHaveBeenCalledOnce(); + // The same refusal comes back for the re-send: no second dialog, and the page is told + // the check could not be completed so it can show its own error. + expect(screen.getByTestId("blocked").textContent).toBe("false"); + rerender(); + expect(screen.queryByRole("dialog")).toBeNull(); + expect(screen.getByTestId("blocked").textContent).toBe("true"); + // A later, separate refusal is a new attempt and asks again. + rerender(); + expect(screen.getByRole("dialog")).toBeTruthy(); + }); +}); diff --git a/app/components/OutboundVerification.tsx b/app/components/OutboundVerification.tsx new file mode 100644 index 000000000..ce147d5a2 --- /dev/null +++ b/app/components/OutboundVerification.tsx @@ -0,0 +1,227 @@ +import { createContext, useCallback, useContext, useEffect, useRef, useState, type FormEvent, type ReactNode } from "react"; +import { Button, Modal } from "@core/shared-ui"; +import { setHumanToken } from "~/lib/human-token"; +import { useTurnstileWidget } from "~/lib/turnstile"; +import { m } from "~/paraglide/messages"; + +/** + * Inside a new company's first 24 hours, a message to a client goes out only + * after the sender proves they are a person. Every surface that sends one calls + * `await ensureHuman()` before submitting; it resolves true at once outside the + * window, and otherwise after a fresh Turnstile challenge is solved (the token + * is left for the next request, see `~/lib/human-token`). False means the + * person closed the dialog and the send should not go ahead. + * + * A new challenge every time is deliberate: a token redeems once. + */ +type EnsureHuman = () => Promise; + +const EnsureHumanContext = createContext(async () => true); + +export function useEnsureHuman(): EnsureHuman { + return useContext(EnsureHumanContext); +} + +/** + * `onSubmit` for a native form whose submit has no handler of its own: holds + * the browser submit, runs the challenge, and re-fires it once the person is + * verified. A second press during the challenge is ignored. Cancelling submits + * nothing. + */ +export function useHumanGatedSubmit() { + const ensureHuman = useEnsureHuman(); + const busy = useRef(false); + const pass = useRef(false); + return useCallback( + async (e: FormEvent) => { + if (pass.current) { + pass.current = false; + return; + } + e.preventDefault(); + const form = e.currentTarget; + if (busy.current) return; + busy.current = true; + try { + if (!(await ensureHuman())) return; + // Let the submit event that started this finish dispatching first. When no + // challenge is needed the promise above is already resolved, so the code + // below would otherwise run INSIDE that dispatch, where the browser + // ignores a nested requestSubmit() and the form silently never posts. + await new Promise((resolve) => setTimeout(resolve, 0)); + pass.current = true; + form.requestSubmit(); + // The flag only bridges the re-fired event, which dispatches synchronously. + // If no event fired (validation now fails, form gone) it must not outlive this call. + pass.current = false; + } finally { + busy.current = false; + } + }, + [ensureHuman], + ); +} + +/** + * For a page with no session context (a public share page, the agent portal): the + * action reports `verifySiteKey` when the server refused the send because the + * company is inside its first 24 hours. This shows the challenge for that + * response and calls `resubmit` once the person has passed it. Render the + * returned element anywhere in the component. + */ +export function useChallengeOnRefusal( + data: { verifySiteKey?: string } | undefined, + state: string, + resubmit: () => void, +): { dialog: ReactNode; blocked: boolean } { + const [open, setOpen] = useState<{ id: number; siteKey: string } | null>(null); + // True once the check was passed and the re-send was refused anyway: the page should + // show its own error then (and only then, not behind the dialog on the first refusal). + const [blocked, setBlocked] = useState(false); + const handled = useRef(null); + const counter = useRef(0); + // When the page last sent again after a passed check. A refusal right after that is + // the same refusal; one long after it is a new attempt (the re-send may never have run). + const retriedAt = useRef(0); + useEffect(() => { + if (state !== "idle" || !data || data === handled.current) return; + handled.current = data; + if (!data.verifySiteKey) { + retriedAt.current = 0; + setBlocked(false); + return; + } + // Refused again straight after a passed check: asking again would loop forever + // (the server cannot accept this token). Stop and let the page show its error. + if (Date.now() - retriedAt.current < RETRY_WINDOW_MS) { + retriedAt.current = 0; + setBlocked(true); + return; + } + setBlocked(false); + counter.current += 1; + setOpen({ id: counter.current, siteKey: data.verifySiteKey }); + }, [data, state]); + const dialog = !open ? null : ( + { + // A late callback from a dialog that has since been replaced is ignored. + if (counter.current !== open.id) return; + setOpen(null); + if (ok) { + retriedAt.current = Date.now(); + resubmit(); + } + }} + /> + ); + return { dialog, blocked }; +} + +/** How long a refusal after a re-send counts as the answer to that re-send. */ +const RETRY_WINDOW_MS = 90_000; + +interface Pending { + id: number; + resolve: (ok: boolean) => void; +} + +export function OutboundVerificationProvider({ + windowOpen, + siteKey, + children, +}: { + /** True only while the cooling window is open (the server says so). */ + windowOpen: boolean; + siteKey: string | null | undefined; + children: ReactNode; +}) { + const [pending, setPending] = useState(null); + // The request on screen, held outside React state so settling it is never a side + // effect of a state updater (which React may run more than once). + const pendingRef = useRef(null); + const counter = useRef(0); + + const ensureHuman = useCallback(() => { + // Without a site key the widget cannot render; let the server decide, it + // refuses with the same code if it needed the token. + if (!windowOpen || !siteKey) return Promise.resolve(true); + return new Promise((resolve) => { + counter.current += 1; + const id = counter.current; + // A second request while a dialog is open replaces it; the first caller is + // told "not verified" instead of waiting forever. + pendingRef.current?.resolve(false); + const next = { id, resolve }; + pendingRef.current = next; + setPending(next); + }); + }, [windowOpen, siteKey]); + + const finish = useCallback((ok: boolean, id: number) => { + const current = pendingRef.current; + // A late callback from a dialog that has since been replaced must not settle its successor. + if (!current || current.id !== id) return; + pendingRef.current = null; + setPending(null); + current.resolve(ok); + }, []); + + return ( + + {children} + {pending && ( + finish(ok, pending.id)} /> + )} + + ); +} + +function VerifyDialog({ + step, + siteKey, + onDone, +}: { + step: number; + siteKey: string | null | undefined; + onDone: (ok: boolean) => void; +}) { + const widgetRef = useRef(null); + const [loadFailed, setLoadFailed] = useState(false); + + useTurnstileWidget( + siteKey, + widgetRef, + step, + (token) => { + setHumanToken(token); + onDone(true); + }, + { onLoadFailed: () => setLoadFailed(true) }, + ); + + return ( + onDone(false)} + title={m.outbound_verify_title()} + size="sm" + footer={ + + } + > +

{m.outbound_verify_body()}

+
+ {loadFailed && ( +

+ {m.outbound_verify_load_failed()} +

+ )} + + ); +} diff --git a/app/components/agent/AgentRepairInspectionBlock.tsx b/app/components/agent/AgentRepairInspectionBlock.tsx index 3a0a2474b..f629ee21a 100644 --- a/app/components/agent/AgentRepairInspectionBlock.tsx +++ b/app/components/agent/AgentRepairInspectionBlock.tsx @@ -17,6 +17,7 @@ import { useState } from "react"; import { useFetcher } from "react-router"; import { RepairDefectRowView, type RepairDefectPhoto } from "~/components/portal/sections/repair/RepairDefectRowView"; import { RepairSharePanel } from "~/components/portal/sections/repair/RepairSharePanel"; +import { useChallengeOnRefusal } from "~/components/OutboundVerification"; import { agentMayWriteRepairList, type AgentRepairAccess } from "~/lib/agent-repair-access"; import { m } from "~/paraglide/messages"; @@ -59,7 +60,7 @@ export function AgentRepairInspectionBlock({ photosFor, }: AgentRepairInspectionBlockProps) { const shareFetcher = useFetcher(); - const emailFetcher = useFetcher<{ ok?: boolean; error?: string }>(); + const emailFetcher = useFetcher<{ ok?: boolean; error?: string; verifySiteKey?: string }>(); const [copyLabel, setCopyLabel] = useState(m.portal_repair_copy_share()); const [emailTo, setEmailTo] = useState(""); const [emailMsg, setEmailMsg] = useState(""); @@ -97,9 +98,12 @@ export function AgentRepairInspectionBlock({ if (emailMsg) fd.append("message", emailMsg); emailFetcher.submit(fd, { method: "post" }); }; + // A company in its first 24 hours is asked for the human check; send again once passed. + const challenge = useChallengeOnRefusal(emailFetcher.data, emailFetcher.state, sendEmail); return (
+ {challenge.dialog}

{tenantName}

{/* Sharing creates a repair_requests row, which the API refuses under @@ -151,7 +155,7 @@ export function AgentRepairInspectionBlock({ emailMsg={emailMsg} emailSent={emailFetcher.data?.ok === true} emailSubmitting={emailFetcher.state === "submitting"} - emailError={emailFetcher.data?.error} + emailError={emailFetcher.data?.verifySiteKey && !challenge.blocked ? undefined : emailFetcher.data?.error} onCopyShareLink={copyShareLink} onEmailToChange={setEmailTo} onEmailMsgChange={setEmailMsg} diff --git a/app/components/agreements/RequestDetail.tsx b/app/components/agreements/RequestDetail.tsx index 57c0ed788..a0d3d0085 100644 --- a/app/components/agreements/RequestDetail.tsx +++ b/app/components/agreements/RequestDetail.tsx @@ -4,6 +4,7 @@ import { SignerList, type SignerRow } from "~/components/agreements/SignerList"; import type { action } from "~/routes/resources/agreement-signers"; import { AGREEMENT_SIGNERS_ACTION } from "~/routes/resources/agreement-signers"; import { useGuardedSubmit } from "~/hooks/useGuardedSubmit"; +import { useEnsureHuman } from "~/components/OutboundVerification"; import { m } from "~/paraglide/messages"; /** @@ -18,6 +19,7 @@ export function RequestDetail({ requestId }: { requestId: string }) { // Separate fetchers per competing mutation (RR rule: shared fetcher aborts in-flight). // #106 - a reminder sends a real email to a real signer. The two load // fetchers around it stay raw: they are selects, not writes. + const ensureHuman = useEnsureHuman(); // submit-guard-allow-no-busy: the row swaps to a "sent" state on the reply. const { fetcher: remindFetcher, submit: submitRemind } = useGuardedSubmit(); const copyFetcher = useFetcher(); @@ -50,7 +52,8 @@ export function RequestDetail({ requestId }: { requestId: string }) { // Remind is fire-and-forget through its own fetcher; the result (including a // 429/409 friendly message) renders as an inline banner, never an alert. - const onRemind = (signerId: string) => { + const onRemind = async (signerId: string) => { + if (!(await ensureHuman())) return; submitRemind({ intent: "remind", requestId, signerId }, post); }; diff --git a/app/components/editor/PublishModal.tsx b/app/components/editor/PublishModal.tsx index 6be6a8524..2a49c8f3f 100644 --- a/app/components/editor/PublishModal.tsx +++ b/app/components/editor/PublishModal.tsx @@ -66,8 +66,8 @@ export function PublishModal({ open, progress, status, publishError, isSubmittin // first 24 hours. Said HERE, on the button that sets the expectation, because // the account-wide banner answers a different question: it says the window // exists, not that THIS publish is about to land in it. Publishing itself is - // unaffected — the report goes live and the client's email is re-scheduled to - // the unlock instant, so the wording promises a delay, never a loss. + // unaffected — the report goes live, and the person is asked to confirm they are + // a person before the client email goes out; the wording never promises a loss. // // Same `unlockAtMs` the banner reads, so the two cannot name different times; // it is non-null only while the window is open, and the server decides that diff --git a/app/components/editor/publish-modal-cooling-notice.test.tsx b/app/components/editor/publish-modal-cooling-notice.test.tsx index 92bb12647..69e60d0cc 100644 --- a/app/components/editor/publish-modal-cooling-notice.test.tsx +++ b/app/components/editor/publish-modal-cooling-notice.test.tsx @@ -61,8 +61,7 @@ describe('PublishModal — outbound cooling window', () => { unlockAtMs = Date.now() + 3_600_000; render(); const notice = screen.getByText(/less than a day old/i); - expect(notice.textContent).toMatch(/sends itself/i); - expect(notice.textContent).toMatch(/nothing to redo/i); + expect(notice.textContent).toMatch(/confirm you are a person/i); expect(notice.textContent).toMatch(/goes live now/i); }); diff --git a/app/components/inspection/SendReportModal.tsx b/app/components/inspection/SendReportModal.tsx index 82750115f..bddd43d50 100644 --- a/app/components/inspection/SendReportModal.tsx +++ b/app/components/inspection/SendReportModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useHumanGatedSubmit } from "~/components/OutboundVerification"; import type { useFetcher } from "react-router"; import { Modal, Button, Checkbox, Input, Select } from "@core/shared-ui"; import type { action } from "~/routes/inspector-portal"; @@ -73,6 +74,7 @@ export function SendReportModal({ fetcher: ReturnType>; onClose: () => void; }) { + const gatedSubmit = useHumanGatedSubmit(); // Computed in a lazy initializer, which holds only because this component is // MOUNTED per open (`{sendReportOpen && }`) — so every // opening re-reads the current people. If it is ever kept mounted behind an @@ -154,7 +156,12 @@ export function SendReportModal({ } > - + diff --git a/app/components/inspector-portal/CommunicationSection.tsx b/app/components/inspector-portal/CommunicationSection.tsx index 291f85c09..f77281aa2 100644 --- a/app/components/inspector-portal/CommunicationSection.tsx +++ b/app/components/inspector-portal/CommunicationSection.tsx @@ -33,6 +33,8 @@ import { useCallback, useEffect, useState } from "react"; import { useFetcher } from "react-router"; import { Card } from "@core/shared-ui"; import { useGuardedSubmit } from "~/hooks/useGuardedSubmit"; +import { useEnsureHuman } from "~/components/OutboundVerification"; +import { VerificationCancelledError } from "~/lib/verification-cancelled"; import { m } from "~/paraglide/messages"; import { useDisplayLocale, useDisplayTimeZone } from "~/hooks/useSessionContext"; import { BlockHeading } from "./BlockHeading"; @@ -83,6 +85,7 @@ export function CommunicationSection({ // MessageThread owns the pending affordance here: it holds its own `sending` // state, disables Send while `onSend` is awaited, and refuses a second press // before that resolves. + const ensureHuman = useEnsureHuman(); // submit-guard-allow-no-busy: the composer disables its own Send button. const { fetcher: send, submit: submitSend } = useGuardedSubmit<{ ok: boolean }>(); // Resend posts to the PAGE action's send-report intent (BFF — the browser @@ -138,7 +141,7 @@ export function CommunicationSection({ if (resendState === "idle" && resend.data) load(); }, [resendState, resend.data, load]); - function handleResend(row: DeliveryRow) { + async function handleResend(row: DeliveryRow) { // Channel-faithful: the resend rides the row's OWN channel so it reaches // the same provider that failed. Email → send-report; SMS → send-sms. // An email endpoint must never be the fallback for an SMS failure. @@ -155,6 +158,7 @@ export function CommunicationSection({ return; } if (row.channel !== "email") return; + if (!(await ensureHuman())) return; const recipient = row.recipientContactId ? { contactId: row.recipientContactId, roleKey: row.roleKey } : { email: row.recipient, roleKey: row.roleKey }; @@ -174,6 +178,8 @@ export function CommunicationSection({ const effectiveRecipient = recipientId || lastInbound?.contactId || threadOptions[0]?.contactId || ""; async function handleSend(body: string) { + // Throw (not return) on cancel so MessageThread keeps the draft text. + if (!(await ensureHuman())) throw new VerificationCancelledError(); if (!effectiveRecipient) throw new Error("no recipient"); // The optimistic bubble goes up only for a call the guard accepted - // otherwise the thread would show a message nothing had been sent for. diff --git a/app/components/inspector-portal/PublishReportModal.tsx b/app/components/inspector-portal/PublishReportModal.tsx index b8c69119e..80b2db0c0 100644 --- a/app/components/inspector-portal/PublishReportModal.tsx +++ b/app/components/inspector-portal/PublishReportModal.tsx @@ -1,4 +1,5 @@ import { Link, type useFetcher } from "react-router"; +import { useHumanGatedSubmit } from "~/components/OutboundVerification"; import { Modal } from "@core/shared-ui"; import type { action } from "~/routes/inspector-portal"; import { m } from "~/paraglide/messages"; @@ -71,6 +72,7 @@ export function PublishReportModal({ error: string | undefined; onClose: () => void; }) { + const gatedSubmit = useHumanGatedSubmit(); return ( } > - + {blockingCount > 0 && (

diff --git a/app/components/inspector-portal/RequestPaymentModal.tsx b/app/components/inspector-portal/RequestPaymentModal.tsx index 5e17acf65..b347f6199 100644 --- a/app/components/inspector-portal/RequestPaymentModal.tsx +++ b/app/components/inspector-portal/RequestPaymentModal.tsx @@ -1,4 +1,5 @@ import type { useFetcher } from "react-router"; +import { useHumanGatedSubmit } from "~/components/OutboundVerification"; import { Modal } from "@core/shared-ui"; import type { action } from "~/routes/inspector-portal"; import { m } from "~/paraglide/messages"; @@ -28,6 +29,7 @@ export function RequestPaymentModal({ error: string | undefined; onClose: () => void; }) { + const gatedSubmit = useHumanGatedSubmit(); const title = resend ? m.hub_payment_title_resend() : m.hub_payment_title(); const submitLabel = resend ? m.hub_payment_submit_resend() : m.hub_payment_submit(); return ( @@ -55,7 +57,12 @@ export function RequestPaymentModal({ } > - +

{m.hub_payment_recipient_label()}

diff --git a/app/components/messaging/MessageThread.tsx b/app/components/messaging/MessageThread.tsx index fa7424d91..d83ee69f5 100644 --- a/app/components/messaging/MessageThread.tsx +++ b/app/components/messaging/MessageThread.tsx @@ -26,6 +26,7 @@ import { useEffect, useRef, useState } from "react"; import { m } from "~/paraglide/messages"; import { useDisplayLocale, useDisplayTimeZone } from "~/hooks/useSessionContext"; import { bucketMessages, dayKeyInZone, dayLabel, type MessageRow } from "~/lib/communication-view"; +import { VerificationCancelledError } from "~/lib/verification-cancelled"; export interface ThreadMessage extends MessageRow { /** Local-only: an optimistic send not yet confirmed, or one that failed. */ @@ -98,8 +99,9 @@ export function MessageThread({ try { await onSend(body); setComposeBody(""); - } catch { - setSendFailed(true); + } catch (err) { + // Closing the human check is not a failed send: keep the draft, say nothing. + if (!(err instanceof VerificationCancelledError)) setSendFailed(true); } finally { setSending(false); } diff --git a/app/components/portal/sections/RepairBuilderSection.tsx b/app/components/portal/sections/RepairBuilderSection.tsx index cd2d121a6..9f2bc56a7 100644 --- a/app/components/portal/sections/RepairBuilderSection.tsx +++ b/app/components/portal/sections/RepairBuilderSection.tsx @@ -22,6 +22,7 @@ import { useState, useCallback, useEffect } from "react"; import { useFetcher } from "react-router"; import { m } from "~/paraglide/messages"; +import { useChallengeOnRefusal } from "~/components/OutboundVerification"; import { RepairDefectRow } from "./repair/RepairDefectRow"; import { useRepairOpQueue } from "./repair/useRepairOpQueue"; import { useRepairItemDrafts } from "./repair/useRepairItemDrafts"; @@ -260,7 +261,7 @@ function RepairBuilderUI({ defects, mine, token, quickPhrases, actionPath }: Rep const [emailSent, setEmailSent] = useState(false); const introFetcher = useFetcher<{ ok?: boolean; error?: string }>(); - const emailFetcher = useFetcher<{ ok?: boolean; error?: string }>(); + const emailFetcher = useFetcher<{ ok?: boolean; error?: string; verifySiteKey?: string }>(); const sorted = sortDefects(defects, sortKey); const phrases = resolveQuickPhrases(quickPhrases, seedQuickPhrases()); @@ -314,9 +315,12 @@ function RepairBuilderUI({ defects, mine, token, quickPhrases, actionPath }: Rep if (emailMsg) fd.append("message", emailMsg); emailFetcher.submit(fd, { method: "post", action: actionPath }); }; + // A company in its first 24 hours is asked for the human check; send again once passed. + const challenge = useChallengeOnRefusal(emailFetcher.data, emailFetcher.state, sendEmail); return (
+ {challenge.dialog} {/* Header */}

@@ -430,7 +434,7 @@ function RepairBuilderUI({ defects, mine, token, quickPhrases, actionPath }: Rep emailMsg={emailMsg} emailSent={emailSent} emailSubmitting={emailFetcher.state === "submitting"} - emailError={emailFetcher.data?.error} + emailError={emailFetcher.data?.verifySiteKey && !challenge.blocked ? undefined : emailFetcher.data?.error} onCopyShareLink={copyShareLink} onEmailToChange={setEmailTo} onEmailMsgChange={setEmailMsg} diff --git a/app/components/settings/ConnectionTestStatus.tsx b/app/components/settings/ConnectionTestStatus.tsx index bf57ce357..3b633d474 100644 --- a/app/components/settings/ConnectionTestStatus.tsx +++ b/app/components/settings/ConnectionTestStatus.tsx @@ -65,7 +65,8 @@ export function ConnectionTestStatus({ const mine = useMemo( () => results - .filter((r) => r.target === target) + // Step rows belong to the stepwise checklist, not the one-line status. + .filter((r) => r.target === target && r.step === null) .sort((a, b) => b.testedAt - a.testedAt), [results, target], ); diff --git a/app/components/settings/EmailSecretsPanel.tsx b/app/components/settings/EmailSecretsPanel.tsx index 78b8b3b61..782cb02ef 100644 --- a/app/components/settings/EmailSecretsPanel.tsx +++ b/app/components/settings/EmailSecretsPanel.tsx @@ -4,6 +4,7 @@ import { RadioCardGroup } from "@core/shared-ui"; import type { useFetcher } from "react-router"; import { SecretField } from "~/components/SecretField"; import { TestConnectionButton } from "~/components/settings/TestConnectionButton"; +import { EmailStepTest } from "~/components/settings/EmailStepTest"; import { ConnectionTestStatus, type ConnectionTestResult } from "~/components/settings/ConnectionTestStatus"; import type { action } from "~/routes/settings-communication"; import { m } from "~/paraglide/messages"; @@ -37,8 +38,6 @@ export function EmailSecretsPanel({ secretFieldError, secretFormError, savingEmailSecrets, - resendTestFetcher, - resendTest, emailValidateFetcher, initialProvider = "resend", webhookBaseUrl = "", @@ -59,8 +58,6 @@ export function EmailSecretsPanel({ secretFieldError: (name: string) => string | undefined; secretFormError: (intent: string) => string | null; savingEmailSecrets: boolean; - resendTestFetcher: ActionFetcher; - resendTest: ActionFetcher["data"]; emailValidateFetcher: ActionFetcher; initialProvider?: EmailByoProvider; /** Origin used to build the deliverability webhook URL hint (e.g. https://app…). */ @@ -241,19 +238,8 @@ export function EmailSecretsPanel({

- {/* Test connection — probes the STORED Resend key; shown for Resend only */} - {provider === "resend" && ( - - {resendTest && "intent" in resendTest && resendTest.intent === "test-resend" && resendTest.test && "domains" in resendTest.test && ( - - {m.settings_emailsecrets_resend_connected({ count: resendTest.test.domains })} - - )} - {resendTest && "intent" in resendTest && resendTest.intent === "test-resend" && "ok" in resendTest && !resendTest.ok && ( - {resendTest.error} - )} - - )} + {/* Test connection for Resend: key, permissions, sender domain, real test email, quota, delivery */} + {provider === "resend" && } {/* Validate credentials — generic probe for sendgrid / postmark / mailgun. The hidden `provider` input is a child of TestConnectionButton so it diff --git a/app/components/settings/EmailStepTest.tsx b/app/components/settings/EmailStepTest.tsx new file mode 100644 index 000000000..3a48e1cfb --- /dev/null +++ b/app/components/settings/EmailStepTest.tsx @@ -0,0 +1,28 @@ +import { StepChecklist } from "~/components/settings/StepChecklist"; +import type { ConnectionTestResult } from "~/lib/connection-test"; +import { EMAIL_TEST_STEPS } from "~/lib/test-steps"; +import { m } from "~/paraglide/messages"; + +const LABEL: Record<(typeof EMAIL_TEST_STEPS)[number], () => string> = { + key_valid: () => m.settings_emailsteps_key_valid(), + key_scope: () => m.settings_emailsteps_key_scope(), + sender_domain_verified: () => m.settings_emailsteps_sender_domain_verified(), + send_test: () => m.settings_emailsteps_send_test(), + quota: () => m.settings_emailsteps_quota(), + delivery: () => m.settings_emailsteps_delivery(), +}; + +/** Resend "Test connection": six steps; the test email always goes to the signed-in user. */ +export function EmailStepTest({ results }: { results: ConnectionTestResult[] }) { + return ( + LABEL[s as keyof typeof LABEL]()} + results={results} + notice={m.settings_emailsteps_notice()} + recheckStep="delivery" + /> + ); +} diff --git a/app/components/settings/StepChecklist.tsx b/app/components/settings/StepChecklist.tsx new file mode 100644 index 000000000..4349105d7 --- /dev/null +++ b/app/components/settings/StepChecklist.tsx @@ -0,0 +1,145 @@ +/** + * Stepwise "Test connection": a checklist that runs one server step per request. + * The server re-derives each step's prerequisites, so the order here only + * decides what the user sees fill in. Used by the Resend panel (six steps) and + * the Stripe panel (three read-only steps). + * + * Each step goes through the route action (`intent`), never a client fetch. A + * failed step ends the run: what follows would only repeat the same cause. + */ +import { useEffect, useRef, useState } from "react"; +import { useGuardedSubmit } from "~/hooks/useGuardedSubmit"; +import { persistedOutcomes, type StepOutcome, type StepStatus } from "~/lib/test-steps"; +import type { ConnectionTestResult } from "~/lib/connection-test"; +import { m } from "~/paraglide/messages"; + +/** What the route action returns for one step (see `app/lib/test-step.server.ts`). */ +interface StepActionData { + intent?: string; + step?: string | null; + outcome?: StepOutcome | null; +} + +const STATUS_LABEL: Record string> = { + pass: () => m.settings_teststeps_status_pass(), + fail: () => m.settings_teststeps_status_fail(), + skipped: () => m.settings_teststeps_status_skipped(), + pending: () => m.settings_teststeps_status_pending(), +}; + +const GLYPH: Record = { pass: "✓", fail: "✗", skipped: "–", pending: "…" }; +const TONE: Record = { + pass: "text-ih-ok-fg", + fail: "text-ih-bad-fg", + skipped: "text-ih-fg-3", + pending: "text-ih-fg-3", +}; + +export function StepChecklist({ + target, + intent, + steps, + label, + results, + notice, + recheckStep, +}: { + target: ConnectionTestResult["target"]; + /** The route action intent that runs ONE step (posted with a `step` field). */ + intent: string; + steps: readonly string[]; + label: (step: string) => string; + results: ConnectionTestResult[]; + /** One line shown beside the button, for a step with a side effect. */ + notice?: string; + /** A step that can stay "waiting" and offers a re-check link. */ + recheckStep?: string; +}) { + const { fetcher, submit, busy: submitting } = useGuardedSubmit(); + const [outcomes, setOutcomes] = useState>(() => persistedOutcomes(results, target, steps)); + const [queue, setQueue] = useState([]); + const [active, setActive] = useState(null); + const [failed, setFailed] = useState(false); + // The last response already applied. A re-run starts with the previous run's + // data still on the fetcher; it must not be mistaken for the new answer. + const handled = useRef(null); + + const start = (run: string[]) => { + const [first, ...rest] = run; + setFailed(false); + setOutcomes((prev) => { + const next = { ...prev }; + for (const s of run) delete next[s]; + return next; + }); + setQueue(rest); + setActive(first); + submit({ intent, step: first }, { method: "post" }); + }; + + useEffect(() => { + if (fetcher.state !== "idle" || !active) return; + const d = fetcher.data; + if (!d || d.intent !== intent || d.step !== active || d === handled.current) return; + handled.current = d; + const outcome = d.outcome; + if (!outcome) { + setFailed(true); + setQueue([]); + setActive(null); + return; + } + setOutcomes((prev) => ({ ...prev, [active]: outcome })); + const [next, ...rest] = outcome.status === "fail" ? [] : queue; + if (next) { + setQueue(rest); + setActive(next); + submit({ intent, step: next }, { method: "post" }); + } else { + setActive(null); + } + }, [fetcher.state, fetcher.data, active, queue, submit, intent]); + + const busy = active !== null || submitting; + const btn = + "h-8 px-3 rounded-md border border-ih-border bg-ih-bg-card text-[12px] font-bold text-ih-fg-2 hover:bg-ih-bg-muted transition-colors disabled:opacity-60"; + + return ( +
+
+ + {notice ? {notice} : null} +
+ +
    + {steps.map((step) => { + const o = outcomes[step]; + const status = o?.status; + return ( +
  • + + {status ? GLYPH[status] : active === step ? "…" : "·"} + + {label(step)} + {status ? {STATUS_LABEL[status]()} : null} + {o?.detail ? ( + — {o.detail} + ) : null} + {step === recheckStep && status === "pending" && !busy ? ( + + ) : null} +
  • + ); + })} +
+ {failed ? {m.settings_teststeps_error()} : null} +
+ ); +} diff --git a/app/components/settings/StripeStepTest.tsx b/app/components/settings/StripeStepTest.tsx new file mode 100644 index 000000000..b087e7039 --- /dev/null +++ b/app/components/settings/StripeStepTest.tsx @@ -0,0 +1,23 @@ +import { StepChecklist } from "~/components/settings/StepChecklist"; +import type { ConnectionTestResult } from "~/lib/connection-test"; +import { STRIPE_TEST_STEPS } from "~/lib/test-steps"; +import { m } from "~/paraglide/messages"; + +const LABEL: Record<(typeof STRIPE_TEST_STEPS)[number], () => string> = { + key_valid: () => m.settings_stripesteps_key_valid(), + account_capabilities: () => m.settings_stripesteps_account_capabilities(), + webhook: () => m.settings_stripesteps_webhook(), +}; + +/** Stripe "Test connection": three read-only steps; nothing is charged or created. */ +export function StripeStepTest({ results }: { results: ConnectionTestResult[] }) { + return ( + LABEL[s as keyof typeof LABEL]()} + results={results} + /> + ); +} diff --git a/app/components/settings/TemplateEditorModal.tsx b/app/components/settings/TemplateEditorModal.tsx index 12ff83980..2d79ed10f 100644 --- a/app/components/settings/TemplateEditorModal.tsx +++ b/app/components/settings/TemplateEditorModal.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useRef } from "react"; import { useFetcher } from "react-router"; import { Button, Pill, Modal } from "@core/shared-ui"; +import { useHumanGatedSubmit } from "~/components/OutboundVerification"; import { m } from "~/paraglide/messages"; import { SUPPORTED_CONTACT_LOCALES } from "../../../server/lib/i18n/contact-locale"; import { localeLabel } from "~/lib/locales"; @@ -79,6 +80,7 @@ export function TemplateEditorModal({ const [subject, setSubject] = useState(template?.subject ?? prefill?.subject ?? ""); const [body, setBody] = useState(template?.body ?? prefill?.body ?? ""); const bodyRef = useRef(null); + const gatedSubmit = useHumanGatedSubmit(); const [testTo, setTestTo] = useState(""); const [testSent, setTestSent] = useState(false); @@ -349,7 +351,10 @@ export function TemplateEditorModal({ className="w-full h-9 px-3 rounded-md border border-ih-border bg-ih-bg-card text-[13px] text-ih-fg-1 placeholder:text-ih-fg-4" />
- + {isEmail && } diff --git a/app/components/settings/connection-test-status.test.ts b/app/components/settings/connection-test-status.test.ts index efd8c4483..53bf9d1d4 100644 --- a/app/components/settings/connection-test-status.test.ts +++ b/app/components/settings/connection-test-status.test.ts @@ -50,7 +50,7 @@ afterEach(() => { function row(over: Partial): ConnectionTestResult { return { target: 'sms', provider: null, ok: true, detail: null, - testedByUserId: null, testedAt: NOW - MIN, ...over, + testedByUserId: null, testedAt: NOW - MIN, step: null, ...over, }; } diff --git a/app/components/settings/integrations/StripePaymentsPanel.tsx b/app/components/settings/integrations/StripePaymentsPanel.tsx index a4a317abc..8a29673f1 100644 --- a/app/components/settings/integrations/StripePaymentsPanel.tsx +++ b/app/components/settings/integrations/StripePaymentsPanel.tsx @@ -1,12 +1,11 @@ import { useState } from "react"; -import { Form, type useFetcher, type useRevalidator } from "react-router"; +import { Form, type useRevalidator } from "react-router"; import { SecretField } from "~/components/SecretField"; -import { TestConnectionButton } from "~/components/settings/TestConnectionButton"; +import { StripeStepTest } from "~/components/settings/StripeStepTest"; import { ConnectionTestStatus, type ConnectionTestResult } from "~/components/settings/ConnectionTestStatus"; import { WebhookStatusBadge } from "./WebhookStatusBadge"; import { useChromeDateTimeFormat, useDisplayTimeZone } from "~/hooks/useSessionContext"; import { formatShapedDateTime } from "~/lib/format-date"; -import type { action } from "~/routes/settings-integrations"; import { m } from "~/paraglide/messages"; type WebhookLogEntry = { ts: string; eventType: string; result: string }; @@ -43,7 +42,6 @@ interface StripePaymentsPanelProps { saving: boolean; serverField: string | null; serverError: string | null | undefined; - testFetcher: ReturnType>; revalidator: ReturnType; /** Persisted "Test connection" history (shared loader list, filtered to stripe). */ testResults?: ConnectionTestResult[]; @@ -56,7 +54,6 @@ export function StripePaymentsPanel({ saving, serverField, serverError, - testFetcher, revalidator, testResults = [], }: StripePaymentsPanelProps) { @@ -144,24 +141,8 @@ export function StripePaymentsPanel({
- {/* Test connection — probes the STORED secret key, no re-entry needed */} - - {testFetcher.data?.intent === "test-stripe" && testFetcher.data.test && ( - - {m.settings_stripe_test_connected_prefix()} {testFetcher.data.test.accountName} - - {testFetcher.data.test.livemode ? m.settings_stripe_livemode_live() : m.settings_stripe_livemode_test()} - - - )} - {testFetcher.data?.intent === "test-stripe" && !testFetcher.data.success && ( - {testFetcher.data.error} - )} - + {/* Test connection — three read-only checks against the STORED secret key */} + {/* Persisted last-tested status + recent history (survives reloads). */} diff --git a/app/hooks/useSessionContext.ts b/app/hooks/useSessionContext.ts index 972c219dc..fda792b07 100644 --- a/app/hooks/useSessionContext.ts +++ b/app/hooks/useSessionContext.ts @@ -96,7 +96,7 @@ export interface SessionContext { * once the window has elapsed, and when the server could not read the * anchor. The client never computes any of that; it renders what it is told. */ - outboundCoolingWindow: { unlockAtMs: number } | null; + outboundCoolingWindow: { unlockAtMs: number; turnstileSiteKey: string | null } | null; } /** diff --git a/app/lib/api-client.server.ts b/app/lib/api-client.server.ts index 33493c1eb..19a2fee64 100644 --- a/app/lib/api-client.server.ts +++ b/app/lib/api-client.server.ts @@ -37,6 +37,7 @@ import type { InspectionTagApi, IntegrationsApi, IntegrationsAiApi, + IntegrationsTestStepsApi, InvoicesApi, MarketplaceApi, McpGrantsApi, @@ -170,6 +171,7 @@ export interface Api { inspectionTag: ReturnType>; integrations: ReturnType>; integrationsAi: ReturnType>; + integrationsTestSteps: ReturnType>; imports: ReturnType>; invoices: ReturnType>; marketplace: ReturnType>; @@ -278,6 +280,7 @@ export function createApi(context: LoadContext, opts: CreateApiOptions = {}): Ap inspectionTag: mk(MOUNT.inspectionTag), integrations: mk(MOUNT.integrations), integrationsAi: mk(MOUNT.integrationsAi), + integrationsTestSteps: mk(MOUNT.integrationsTestSteps), imports: mk(MOUNT.imports), invoices: mk(MOUNT.invoices), marketplace: mk(MOUNT.marketplace), diff --git a/app/lib/api-mounts.ts b/app/lib/api-mounts.ts index 2d01fd9c5..521ccf99d 100644 --- a/app/lib/api-mounts.ts +++ b/app/lib/api-mounts.ts @@ -50,6 +50,7 @@ export const MOUNT: Record = { inspectionTag: "/api/inspections", integrations: "/api/integrations", integrationsAi: "/api/integrations/ai", + integrationsTestSteps: "/api/integrations/test-steps", imports: "/api/imports", invoices: "/api/invoices", marketplace: "/api/templates/marketplace", diff --git a/app/lib/communication-view.test.ts b/app/lib/communication-view.test.ts index 582a49fe7..1c5993d29 100644 --- a/app/lib/communication-view.test.ts +++ b/app/lib/communication-view.test.ts @@ -9,6 +9,7 @@ * - Raw reason strings map to sentences in the UI, with the raw value kept * visible in the fallback — never a bare machine string, never nothing. */ +import { RATE_LIMIT_DEFER_REASON, DAILY_QUOTA_DEFER_REASON, MONTHLY_QUOTA_FAIL_REASON } from "../../server/services/automation/provider-deferral"; import { describe, it, expect } from "vitest"; import { bucketMessages, @@ -103,6 +104,20 @@ describe("groupDeliveries", () => { }); }); +describe("reasonText — the email provider's own limits", () => { + // Asserted against the strings the server actually writes, not a copy of them: + // a reworded reason on the server would otherwise fall through to "Skipped". + it.each([ + ["rate limit", RATE_LIMIT_DEFER_REASON], + ["daily quota", DAILY_QUOTA_DEFER_REASON], + ["monthly quota", MONTHLY_QUOTA_FAIL_REASON], + ])("explains the %s reason instead of calling it skipped", (_name, raw) => { + const text = reasonText(raw)!; + expect(text).not.toContain(raw); + expect(text).not.toMatch(/^Skipped/); + }); +}); + describe("reasonText", () => { it("maps every raw string the send paths write today", () => { for (const raw of [ diff --git a/app/lib/communication-view.ts b/app/lib/communication-view.ts index 14cebe9fe..088cf71fa 100644 --- a/app/lib/communication-view.ts +++ b/app/lib/communication-view.ts @@ -165,6 +165,15 @@ export function reasonText(reasonCode: string | null): string | null { // is the row's own `sendAt`, already on screen beside it. case "waiting — new company, client email unlocks 24h after signup": return m.comm_reason_cooling_window(); + // The email provider's own limits (see the automation provider-deferral module). + // The first two are still owed and carry the same "waiting" meaning; the third + // is final for the month and says so. + case "waiting — email provider rate limit, retrying shortly": + return m.comm_reason_provider_rate_limit(); + case "waiting — daily email sending limit reached, retrying after the next utc midnight": + return m.comm_reason_provider_daily_quota(); + case "monthly email sending limit reached at the email provider; this message was not sent": + return m.comm_reason_provider_monthly_quota(); default: return m.comm_reason_fallback({ raw: reasonCode }); } } diff --git a/app/lib/connection-test.ts b/app/lib/connection-test.ts index 7e4467b21..fc13c852c 100644 --- a/app/lib/connection-test.ts +++ b/app/lib/connection-test.ts @@ -13,6 +13,8 @@ export interface ConnectionTestResult { detail: string | null; testedByUserId: string | null; testedAt: number; // epoch ms + /** Step id of a stepwise test row; null for single-shot probes. */ + step: string | null; } const TARGETS = new Set(["sms", "email", "stripe", "gemini"]); @@ -38,5 +40,6 @@ export async function parseTestResults( detail: typeof r.detail === "string" ? r.detail : null, testedByUserId: typeof r.testedByUserId === "string" ? r.testedByUserId : null, testedAt: typeof r.testedAt === "number" ? r.testedAt : 0, + step: typeof r.step === "string" ? r.step : null, })); } diff --git a/app/lib/human-token.ts b/app/lib/human-token.ts new file mode 100644 index 000000000..36785f5de --- /dev/null +++ b/app/lib/human-token.ts @@ -0,0 +1,16 @@ +/** + * Hands a solved Turnstile token to the next server round trip. + * + * A cookie rather than a form field because every route action would otherwise + * have to forward it by hand; the worker entry (`workers/human-token.ts`) turns + * it into the header the API reads. One-time by construction: Cloudflare + * rejects a token it has already redeemed, so a stale cookie is harmless and + * the 60-second life only keeps it from lingering. + */ +const HUMAN_TOKEN_COOKIE = "oi-human-token"; + +export function setHumanToken(token: string): void { + if (typeof document === "undefined") return; + const secure = typeof location !== "undefined" && location.protocol === "https:" ? "; Secure" : ""; + document.cookie = `${HUMAN_TOKEN_COOKIE}=${encodeURIComponent(token)}; Path=/; Max-Age=60; SameSite=Strict${secure}`; +} diff --git a/app/lib/outbound-refusal.server.test.ts b/app/lib/outbound-refusal.server.test.ts new file mode 100644 index 000000000..75392190e --- /dev/null +++ b/app/lib/outbound-refusal.server.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from "vitest"; +import { humanCheckSiteKey } from "./outbound-refusal.server"; + +const refusal = (status: number, code: string, details: unknown) => + new Response(JSON.stringify({ success: false, error: { code, message: "x", details } }), { status }); + +describe("humanCheckSiteKey", () => { + it("returns the site key for the cooling-window refusal", async () => { + expect(await humanCheckSiteKey(refusal(403, "OUTBOUND_COOLING_WINDOW", { turnstileSiteKey: "k1" }))).toBe("k1"); + }); + it("returns null for any other refusal, status or unreadable body", async () => { + expect(await humanCheckSiteKey(refusal(403, "FORBIDDEN", { turnstileSiteKey: "k1" }))).toBeNull(); + expect(await humanCheckSiteKey(refusal(429, "OUTBOUND_COOLING_WINDOW", { turnstileSiteKey: "k1" }))).toBeNull(); + expect(await humanCheckSiteKey(new Response("not json", { status: 403 }))).toBeNull(); + expect(await humanCheckSiteKey(refusal(403, "OUTBOUND_COOLING_WINDOW", {}))).toBeNull(); + }); +}); diff --git a/app/lib/outbound-refusal.server.ts b/app/lib/outbound-refusal.server.ts new file mode 100644 index 000000000..2958067dc --- /dev/null +++ b/app/lib/outbound-refusal.server.ts @@ -0,0 +1,17 @@ +/** + * A public repair-share email can be refused because the company is inside its + * first-24-hours window. That refusal is not a failure to report: the page asks + * the sender to pass the human check and sends again. This reads the refusal + * off the API response and returns the site key to show the check with. + */ +export async function humanCheckSiteKey(res: { status: number; json(): Promise }): Promise { + if (res.status !== 403) return null; + try { + const body = (await res.json()) as { + error?: { code?: string; details?: { turnstileSiteKey?: string | null } }; + }; + return body.error?.code === "OUTBOUND_COOLING_WINDOW" ? (body.error.details?.turnstileSiteKey ?? null) : null; + } catch { + return null; + } +} diff --git a/app/lib/test-step.server.ts b/app/lib/test-step.server.ts new file mode 100644 index 000000000..5048c48e7 --- /dev/null +++ b/app/lib/test-step.server.ts @@ -0,0 +1,35 @@ +import type { createApi } from "~/lib/api-client.server"; +import { isEmailTestStepId, isStripeTestStepId } from "~/lib/test-steps"; +import { m } from "~/paraglide/messages"; + +type Api = ReturnType; +type Body = { data?: { status: "pass" | "fail" | "skipped" | "pending"; detail?: string | null } } | null; + +/** + * Runs one step of a stepwise connection test for a settings action. A step that + * ran but failed is still `ok` (the outcome says how it went); `outcome: null` + * means the request itself failed (unknown step, rate limit, transport). + */ +function shape(intent: string, step: string | null, data: NonNullable['data']> | undefined) { + return { + intent, + ok: Boolean(data), + error: data ? null : m.settings_teststeps_error(), + step, + outcome: data ? { status: data.status, detail: data.detail ?? null } : null, + }; +} + +export async function runEmailTestStepIntent(api: Api, intent: string, rawStep: unknown) { + if (!isEmailTestStepId(rawStep)) return shape(intent, null, undefined); + const res = await api.integrationsTestSteps.email.$post({ json: { step: rawStep } }); + const body = res.ok ? ((await res.json().catch(() => null)) as Body) : null; + return shape(intent, rawStep, body?.data); +} + +export async function runStripeTestStepIntent(api: Api, intent: string, rawStep: unknown) { + if (!isStripeTestStepId(rawStep)) return shape(intent, null, undefined); + const res = await api.integrationsTestSteps.stripe.$post({ json: { step: rawStep } }); + const body = res.ok ? ((await res.json().catch(() => null)) as Body) : null; + return shape(intent, rawStep, body?.data); +} diff --git a/app/lib/test-steps.test.ts b/app/lib/test-steps.test.ts new file mode 100644 index 000000000..32ea3339a --- /dev/null +++ b/app/lib/test-steps.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from "vitest"; +import { EMAIL_TEST_STEPS, STRIPE_TEST_STEPS, isEmailTestStepId, isStripeTestStepId, persistedOutcomes } from "./test-steps"; + +describe("persistedOutcomes", () => { + const row = (over: Record) => ({ + target: "email", step: "key_valid", ok: true, detail: null, testedAt: 1, ...over, + }) as Parameters[0][number]; + + it("keeps the newest result per step", () => { + const out = persistedOutcomes([ + row({ ok: false, detail: "old", testedAt: 1 }), + row({ ok: true, detail: "new", testedAt: 2 }), + ], "email", EMAIL_TEST_STEPS); + expect(out.key_valid).toEqual({ status: "pass", detail: "new" }); + }); + + it("ignores single-shot rows, other targets and unknown steps", () => { + const out = persistedOutcomes([ + row({ step: null }), + row({ target: "stripe", step: "key_valid" }), + row({ step: "not_a_step" }), + ], "email", EMAIL_TEST_STEPS); + expect(out).toEqual({}); + }); + + it("reads each target's own steps", () => { + const rows = [row({ target: "stripe", step: "webhook", ok: false, detail: "bad" }), row({ step: "webhook" })]; + expect(persistedOutcomes(rows, "stripe", STRIPE_TEST_STEPS).webhook).toEqual({ status: "fail", detail: "bad" }); + // email has no "webhook" step, so the same rows yield nothing for it + expect(persistedOutcomes(rows, "email", EMAIL_TEST_STEPS)).toEqual({}); + }); +}); + +describe("step id guards", () => { + it("accept a known id of their own target and reject everything else", () => { + expect(isEmailTestStepId("send_test")).toBe(true); + expect(isEmailTestStepId("webhook")).toBe(false); + expect(isStripeTestStepId("webhook")).toBe(true); + expect(isStripeTestStepId("send_test")).toBe(false); + expect(isEmailTestStepId("send_test ")).toBe(false); + expect(isEmailTestStepId(null)).toBe(false); + }); +}); diff --git a/app/lib/test-steps.ts b/app/lib/test-steps.ts new file mode 100644 index 000000000..a11c69ad4 --- /dev/null +++ b/app/lib/test-steps.ts @@ -0,0 +1,52 @@ +/** + * Client-safe description of the stepwise connection tests: the ordered step ids + * per target and the shape of one step's outcome. The server owns the logic + * (`server/lib/integration/{email,stripe}-test-steps.ts`); this file only names + * things. + */ +export const EMAIL_TEST_STEPS = [ + "key_valid", + "key_scope", + "sender_domain_verified", + "send_test", + "quota", + "delivery", +] as const; +export const STRIPE_TEST_STEPS = ["key_valid", "account_capabilities", "webhook"] as const; + +export type EmailTestStepId = (typeof EMAIL_TEST_STEPS)[number]; +export type StripeTestStepId = (typeof STRIPE_TEST_STEPS)[number]; + +export type StepStatus = "pass" | "fail" | "skipped" | "pending"; + +export interface StepOutcome { + status: StepStatus; + detail: string | null; +} + +export function isEmailTestStepId(v: unknown): v is EmailTestStepId { + return typeof v === "string" && (EMAIL_TEST_STEPS as readonly string[]).includes(v); +} + +export function isStripeTestStepId(v: unknown): v is StripeTestStepId { + return typeof v === "string" && (STRIPE_TEST_STEPS as readonly string[]).includes(v); +} + +/** + * Outcomes the server already stored for `target` (pass/fail only; skipped and + * pending are never recorded), newest first per step, so the checklist survives + * a reload. + */ +export function persistedOutcomes( + rows: ReadonlyArray<{ target: string; step: string | null; ok: boolean; detail: string | null; testedAt: number }>, + target: string, + steps: readonly string[], +): Record { + const out: Record = {}; + const newestFirst = [...rows].sort((a, b) => b.testedAt - a.testedAt); + for (const r of newestFirst) { + if (r.target !== target || r.step === null || !steps.includes(r.step) || out[r.step]) continue; + out[r.step] = { status: r.ok ? "pass" : "fail", detail: r.detail }; + } + return out; +} diff --git a/app/lib/verification-cancelled.ts b/app/lib/verification-cancelled.ts new file mode 100644 index 000000000..f2e0c0a00 --- /dev/null +++ b/app/lib/verification-cancelled.ts @@ -0,0 +1,7 @@ +/** Thrown by a caller that gave up because the person closed the human check. */ +export class VerificationCancelledError extends Error { + constructor() { + super("verification cancelled"); + this.name = "VerificationCancelledError"; + } +} diff --git a/app/routes/agent/repair-items.tsx b/app/routes/agent/repair-items.tsx index efcd8a34e..1e2ee9c29 100644 --- a/app/routes/agent/repair-items.tsx +++ b/app/routes/agent/repair-items.tsx @@ -11,6 +11,7 @@ import { type AgentRepairRow, } from "~/components/agent/AgentRepairInspectionBlock"; import { m } from "~/paraglide/messages"; +import { humanCheckSiteKey } from "~/lib/outbound-refusal.server"; export function meta() { return [{ title: m.agent_portal_recommendations_meta_title() }]; @@ -135,7 +136,13 @@ export async function action({ request, context }: Route.ActionArgs) { param: { shareToken }, json: { to, message }, }); - if (!res.ok) return { ok: false as const, error: m.repair_builder_error_send_email() }; + if (!res.ok) { + const verifySiteKey = await humanCheckSiteKey(res); + const error = m.repair_builder_error_send_email(); + // `error` rides along so that, if the check cannot be completed, the page says so. + if (verifySiteKey) return { ok: false as const, error, verifySiteKey }; + return { ok: false as const, error }; + } return { ok: true as const }; } diff --git a/app/routes/auth-layout.tsx b/app/routes/auth-layout.tsx index 1474ebb41..dcc112557 100644 --- a/app/routes/auth-layout.tsx +++ b/app/routes/auth-layout.tsx @@ -8,6 +8,7 @@ import { CommandPalette, CommandPaletteProvider } from "~/components/CommandPale import { Sidebar, MobileHeader } from "~/components/Sidebar"; import { RouteSkeleton } from "~/components/RouteSkeleton"; import { OutboundCoolingBanner } from "~/components/OutboundCoolingBanner"; +import { OutboundVerificationProvider } from "~/components/OutboundVerification"; import type { SessionContext } from "~/hooks/useSessionContext"; /** @@ -131,6 +132,10 @@ export default function AuthLayout() { return ( + {/* F4 — Suspension banner */} {context?.branding?.tenantStatus === "suspended" && (
@@ -177,6 +182,7 @@ export default function AuthLayout() { onOpenChange={setPaletteOpen} onNewInspection={() => navigate("/inspections/new")} /> + ); } diff --git a/app/routes/inspection-edit.tsx b/app/routes/inspection-edit.tsx index ae4f6e2e6..9081e0d2b 100644 --- a/app/routes/inspection-edit.tsx +++ b/app/routes/inspection-edit.tsx @@ -7,6 +7,7 @@ import { useInspectionState, type InspectionSchema, type ItemFilter } from "~/ho import { findingKey } from "~/hooks/findings/shared"; import { useDisplayLocale } from "~/hooks/useSessionContext"; import { useGuardedSubmit } from "~/hooks/useGuardedSubmit"; +import { useEnsureHuman } from "~/components/OutboundVerification"; import { useFindings, type AttachedRepairItem } from "~/hooks/useFindings"; import { usePhotoOps } from "~/hooks/usePhotoOps"; import { useScopeLoader } from "~/hooks/useScopeLoader"; @@ -163,6 +164,7 @@ export default function InspectionEditPage() { // message is surfaced inline in the publish modal instead. // #106 — publishing is the tier-1 surface: it mints a report version, signs // it and notifies the client, and a double click would do all three twice. + const ensureHuman = useEnsureHuman(); const { fetcher: publishFetcher, submit: submitPublish, busy: publishBusy } = useGuardedSubmit<{ ok: boolean; intent?: string; error?: string }>(); const [publishError, setPublishError] = useState(null); @@ -1875,7 +1877,8 @@ export default function InspectionEditPage() { publishError={publishError} isSubmitting={publishBusy} isAmendment={loaderData.nextPublishIsAmendment} onClose={() => { setPublishError(null); state.setShowPublishModal(false); }} - onPublish={(markComplete: boolean, extras: PublishExtras) => { + onPublish={async (markComplete: boolean, extras: PublishExtras) => { + if (!(await ensureHuman())) return; // Keep the modal open: the publish-result effect closes it on success // and shows the real server reason inline on failure. `markComplete` // also closes the order axis first (advisory — never blocks publish); diff --git a/app/routes/inspector-portal.tsx b/app/routes/inspector-portal.tsx index 81ee9e89c..525e5eec0 100644 --- a/app/routes/inspector-portal.tsx +++ b/app/routes/inspector-portal.tsx @@ -75,6 +75,7 @@ import { isAdminRole } from "~/lib/access"; import { m } from "~/paraglide/messages"; import { getCloudflareEnv } from "~/lib/load-context"; import { useModalFetcher } from "~/hooks/useModalFetcher"; +import { useEnsureHuman } from "~/components/OutboundVerification"; export function meta() { return [{ title: m.inspections_hub_meta_title() }]; @@ -723,7 +724,9 @@ export default function InspectionHubPage() { // IA-65 — signer management on the inspection. The send modal serializes its // signer rows into the action; the pre-sign modal is keyed by envelope id // (which row's "Sign now" was pressed) and closes itself on success. - const submitSendAgreement = (payload: SendAgreementPayload) => { + const ensureHuman = useEnsureHuman(); + const submitSendAgreement = async (payload: SendAgreementPayload) => { + if (!(await ensureHuman())) return; agreementModal.fetcher.submit( { intent: "send-agreement", diff --git a/app/routes/login.tsx b/app/routes/login.tsx index 3db76710e..381e87398 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -73,7 +73,13 @@ async function completeTwoFactor( try { const api = createApi(context); - const res = await api.auth.login["2fa"].$post({ json: { challengeToken, code } }); + // Same BFF signal as the password step below: this client does not forward + // the API's Set-Cookie, so without the header the session token never + // reaches this action and a correct code reads as a failure. + const res = await api.auth.login["2fa"].$post( + { json: { challengeToken, code } }, + { headers: { "x-token-relay": "1" } }, + ); if (!res.ok) { // 401 covers both a wrong code and an expired challenge, and the two need // different advice: retyping helps with one and cannot help with the other. diff --git a/app/routes/login.twofactor.server.test.ts b/app/routes/login.twofactor.server.test.ts index fb8beed73..ad1d7e04d 100644 --- a/app/routes/login.twofactor.server.test.ts +++ b/app/routes/login.twofactor.server.test.ts @@ -45,6 +45,7 @@ vi.mock("../../server/lib/deployment-profile", () => ({ import { action } from "./login"; import { routeArgs } from "../../tests/helpers/route-args"; +import { Login2faResponseSchema, LoginResponseSchema } from "../../server/lib/validations/auth.schema"; const CONTEXT = {} as Parameters[0]["context"]; const json = (body: unknown, status = 200) => @@ -59,6 +60,47 @@ function post(fields: Record) { )); } +/** + * What `POST /api/auth/login/2fa` answers a successful code with, built through + * the REAL response schema rather than typed out here. A hand-written + * `{ data: { token } }` is how this file used to pass while the page could not + * sign anyone in: the server never sent a token, and nothing here could notice. + * Parsing strips any field the schema does not declare, and the token is only + * present when the caller identified itself as the BFF — the same rule the + * handler applies (see tests/unit/auth/login-2fa-token-relay.spec.ts, which + * holds the handler to it). + */ +function twoFaSuccess(token: string) { + return (_args: unknown, options?: { headers?: Record }) => { + const relay = options?.headers?.["x-token-relay"] === "1"; + return Promise.resolve(json(Login2faResponseSchema.parse({ + success: true, + data: { redirect: "/inspections", ...(relay ? { token } : {}) }, + }))); + }; +} + +/** + * What `POST /api/auth/login` answers a correct password with, built the same + * way: through `LoginResponseSchema`, carrying the session token only when the + * call identified itself as the BFF (the handler's rule, held to it by + * tests/unit/auth/login-2fa-token-relay.spec.ts). A page that stopped sending the + * header would get no token here, exactly as it would get none from the API. + */ +function loginSuccess(token: string) { + return (_args: unknown, options?: { headers?: Record }) => { + const relay = options?.headers?.["x-token-relay"] === "1"; + return Promise.resolve(json(LoginResponseSchema.parse({ + success: true, + data: { redirect: "/inspections", ...(relay ? { token } : {}) }, + }))); + }; +} + +/** The password step's answer for a 2FA account, through the same schema. */ +const challengeReply = (challengeToken: string) => + json(LoginResponseSchema.parse({ success: true, data: { requires2fa: true, challengeToken } })); + const CHALLENGE = "challenge.jwt.value"; const GOOD = { email: "a@b.test", password: "hunter2hunter2" }; @@ -69,14 +111,15 @@ beforeEach(() => { describe("login — a 2FA account is handed a challenge, not a refusal", () => { it("CONTROL — a non-2FA login still mints a session, so the branch below is the only change", async () => { - loginPost.mockResolvedValue(json({ data: { token: "session.jwt" } })); + loginPost.mockImplementation(loginSuccess("session.jwt")); await post(GOOD); expect(createSession).toHaveBeenCalledTimes(1); + expect(createSession).toHaveBeenCalledWith(CONTEXT, "session.jwt", "/inspections"); expect(twoFaPost).not.toHaveBeenCalled(); }); it("returns the challenge token instead of an error", async () => { - loginPost.mockResolvedValue(json({ data: { requires2fa: true, challengeToken: CHALLENGE } })); + loginPost.mockResolvedValue(challengeReply(CHALLENGE)); const out = await post(GOOD); // Not a session: the second factor has not been supplied yet. expect(createSession).not.toHaveBeenCalled(); @@ -87,7 +130,7 @@ describe("login — a 2FA account is handed a challenge, not a refusal", () => { // The OAuth consent bounce resumes on the far side of the code form. A // version that dropped it would send 2FA users somewhere else than // everyone else, only on the flow that has a destination. - loginPost.mockResolvedValue(json({ data: { requires2fa: true, challengeToken: CHALLENGE } })); + loginPost.mockResolvedValue(challengeReply(CHALLENGE)); const out = await post({ ...GOOD, returnTo: "/oauth/authorize?x=1" }); expect(out).toMatchObject({ returnTo: "/oauth/authorize?x=1" }); }); @@ -95,7 +138,7 @@ describe("login — a 2FA account is handed a challenge, not a refusal", () => { describe("login — answering the challenge", () => { it("posts the code to /login/2fa and never re-posts credentials", async () => { - twoFaPost.mockResolvedValue(json({ data: { token: "session.jwt" } })); + twoFaPost.mockImplementation(twoFaSuccess("session.jwt")); await post({ challengeToken: CHALLENGE, code: "123456" }); expect(loginPost).not.toHaveBeenCalled(); @@ -104,8 +147,18 @@ describe("login — answering the challenge", () => { .toEqual({ challengeToken: CHALLENGE, code: "123456" }); }); + it("identifies itself as the BFF, so the API hands back the session token", async () => { + // The BFF client does not forward the API's Set-Cookie. Without this + // header the API keeps the JWT out of the body, the action has nothing + // to open a session with, and a correct code reads as a failure. + twoFaPost.mockImplementation(twoFaSuccess("session.jwt")); + await post({ challengeToken: CHALLENGE, code: "123456" }); + const options = twoFaPost.mock.calls[0][1] as { headers?: Record } | undefined; + expect(options?.headers?.["x-token-relay"]).toBe("1"); + }); + it("creates the session at the requested destination on success", async () => { - twoFaPost.mockResolvedValue(json({ data: { token: "session.jwt" } })); + twoFaPost.mockImplementation(twoFaSuccess("session.jwt")); await post({ challengeToken: CHALLENGE, code: "123456", returnTo: "/oauth/authorize?x=1" }); expect(createSession).toHaveBeenCalledWith(CONTEXT, "session.jwt", "/oauth/authorize?x=1"); }); @@ -132,7 +185,7 @@ describe("login — answering the challenge", () => { // A 2FA submission carries no email or password. Parsed as a login it // would answer "email is required" to somebody who has already proved // their password — the branch order in the action is what prevents it. - twoFaPost.mockResolvedValue(json({ data: { token: "session.jwt" } })); + twoFaPost.mockImplementation(twoFaSuccess("session.jwt")); const out = await post({ challengeToken: CHALLENGE, code: "123456" }); expect(out).toBeInstanceOf(Response); }); diff --git a/app/routes/messages.tsx b/app/routes/messages.tsx index 97b37e3f8..944f50d90 100644 --- a/app/routes/messages.tsx +++ b/app/routes/messages.tsx @@ -21,6 +21,8 @@ import { requireToken } from "~/lib/session.server"; import { createApi } from "~/lib/api-client.server"; import { PageHeader, Card, Avatar } from "@core/shared-ui"; import { MessageThread, type ThreadMessage } from "~/components/messaging/MessageThread"; +import { useEnsureHuman } from "~/components/OutboundVerification"; +import { VerificationCancelledError } from "~/lib/verification-cancelled"; import { useDisplayLocale, useDisplayTimeZone } from "~/hooks/useSessionContext"; import { useGuardedSubmit } from "~/hooks/useGuardedSubmit"; import { m } from "~/paraglide/messages"; @@ -144,6 +146,7 @@ function threadTime(ms: number, locale: string, timeZone: string): string { export default function MessagesPage() { const { threads, thread, selected } = useLoaderData(); const navigate = useNavigate(); + const ensureHuman = useEnsureHuman(); // #106 - a send delivers a real message to a real person. // MessageThread owns the pending affordance and will not fire `onSend` again // until the previous one resolves. @@ -176,6 +179,8 @@ export default function MessagesPage() { })); async function handleSend(body: string) { + // Throw (not return) on cancel so MessageThread keeps the draft text. + if (!(await ensureHuman())) throw new VerificationCancelledError(); if (!selected) throw new Error("no thread"); submitSend( { contactId: selected, body, inspectionId: mentionId }, diff --git a/app/routes/public/repair-builder.$tenant.$id.tsx b/app/routes/public/repair-builder.$tenant.$id.tsx index 9914c261a..9574a8a4d 100644 --- a/app/routes/public/repair-builder.$tenant.$id.tsx +++ b/app/routes/public/repair-builder.$tenant.$id.tsx @@ -27,6 +27,7 @@ import { type LoaderResult, } from "~/components/portal/sections/RepairBuilderSection"; import { m } from "~/paraglide/messages"; +import { humanCheckSiteKey } from "~/lib/outbound-refusal.server"; export function meta() { return [{ title: m.repair_builder_meta_title() }]; @@ -224,7 +225,13 @@ export async function action({ param: { shareToken }, json: { to, message }, }); - if (!res.ok) return { ok: false as const, error: m.repair_builder_error_send_email() }; + if (!res.ok) { + const verifySiteKey = await humanCheckSiteKey(res); + const error = m.repair_builder_error_send_email(); + // `error` rides along so that, if the check cannot be completed, the page says so. + if (verifySiteKey) return { ok: false as const, error, verifySiteKey }; + return { ok: false as const, error }; + } return { ok: true as const }; } diff --git a/app/routes/settings-communication.tsx b/app/routes/settings-communication.tsx index 7954f24aa..05906498f 100644 --- a/app/routes/settings-communication.tsx +++ b/app/routes/settings-communication.tsx @@ -16,6 +16,7 @@ import { requireAdminLoader } from "~/lib/access.server"; import { AccessDenied } from "~/components/AccessDenied"; import { EmailDeliveryPanel } from "~/components/settings/EmailDeliveryPanel"; import { EmailSecretsPanel } from "~/components/settings/EmailSecretsPanel"; +import { runEmailTestStepIntent } from "~/lib/test-step.server"; import { SmsDeliveryPanel, type SmsModeValue } from "~/components/settings/SmsDeliveryPanel"; import { GoogleCalendarPanel } from "~/components/settings/GoogleCalendarPanel"; import { ManagedComplianceWizard, type ManagedComplianceData } from "~/components/settings/ManagedComplianceWizard"; @@ -254,22 +255,7 @@ export async function action({ request, context }: Route.ActionArgs) { return saveSecrets(api, intent, body, m.settings_comms_email_secrets_save_error()); } - if (intent === "test-resend") { - const res = await api.integrations.resend.test.$post(); - const body = (await res.json().catch(() => null)) as - | { data?: { domains: number }; error?: { message?: string } } - | null; - if (!res.ok || !body?.data) { - return { - intent, - ok: false, - error: body?.error?.message ?? m.settings_connection_test_failed(), - field: null, - test: null, - }; - } - return { intent, ok: true, error: null, field: null, test: body.data }; - } + if (intent === "test-email-step") return runEmailTestStepIntent(api, intent, form.get("step")); if (intent === "validate-email-provider") { const VALID_PROVIDERS = ["resend", "sendgrid", "postmark", "mailgun"] as const; @@ -498,7 +484,6 @@ export default function SettingsCommunication() { : loaderResult.compliance; const actionData = useActionData(); const nav = useNavigation(); - const resendTestFetcher = useFetcher(); const emailValidateFetcher = useFetcher(); const smsTestFetcher = useFetcher(); const session = useSessionContext(); @@ -604,7 +589,6 @@ export default function SettingsCommunication() { return null; }; - const resendTest = resendTestFetcher.data; // Whether the tenant's OWN selected email provider has its credentials saved // (drives the Email delivery panel's provider-aware guardrail + status copy). @@ -669,8 +653,6 @@ export default function SettingsCommunication() { secretFieldError={secretFieldError} secretFormError={secretFormError} savingEmailSecrets={savingEmailSecrets} - resendTestFetcher={resendTestFetcher} - resendTest={resendTest} emailValidateFetcher={emailValidateFetcher} initialProvider={emailByoProvider} webhookBaseUrl={typeof window !== "undefined" ? window.location.origin : ""} diff --git a/app/routes/settings-integrations.tsx b/app/routes/settings-integrations.tsx index 357b5d409..11a520bdc 100644 --- a/app/routes/settings-integrations.tsx +++ b/app/routes/settings-integrations.tsx @@ -3,12 +3,12 @@ import { useActionData, useNavigation, useRevalidator, - useFetcher, } from "react-router"; import { SettingsCrumb } from "~/components/SettingsCrumb"; import type { Route } from "./+types/settings-integrations"; import { requireToken } from "~/lib/session.server"; import { createApi } from "~/lib/api-client.server"; +import { runStripeTestStepIntent } from "~/lib/test-step.server"; import { useFlash } from "~/hooks/useFlash"; import { useSessionContext } from "~/hooks/useSessionContext"; import { requireAdminLoader } from "~/lib/access.server"; @@ -109,22 +109,10 @@ export async function action({ request, context }: Route.ActionArgs) { return { intent, success: true, error: null, field: null, test: null }; } - if (intent === "test-stripe") { - const api = createApi(context, { token }); - const res = await api.integrations.stripe.test.$post(); - const body = (await res.json().catch(() => null)) as - | { data?: { accountName: string; livemode: boolean }; error?: { message?: string } } - | null; - if (!res.ok || !body?.data) { - return { - intent, - success: false, - error: body?.error?.message ?? m.settings_connection_test_failed(), - field: null, - test: null, - }; - } - return { intent, success: true, error: null, field: null, test: body.data }; + if (intent === "test-stripe-step") { + // This route's other results carry `success` and `field`; keep the union uniform. + const r = await runStripeTestStepIntent(createApi(context, { token }), "test-stripe-step", fd.get("step")); + return { ...r, success: r.ok, field: null }; } if (intent === "save-video") { @@ -233,7 +221,6 @@ export default function SettingsIntegrations() { const actionData = useActionData(); const nav = useNavigation(); const revalidator = useRevalidator(); - const testFetcher = useFetcher(); const ctx = useSessionContext(); const { flashVisible } = useFlash( @@ -300,7 +287,6 @@ export default function SettingsIntegrations() { saving={saving} serverField={serverField} serverError={actionData?.error} - testFetcher={testFetcher} revalidator={revalidator} testResults={testResults} /> diff --git a/docs/reference/api.md b/docs/reference/api.md index d365e4a6c..2f650c31b 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -195,16 +195,20 @@ The JWT is delivered **only** as the `__Host-inspector_token` HttpOnly cookie. It is not in the body, and browser JavaScript must never store a token — `CLAUDE.md` § JWT & Auth Security Rules makes both of those rules. The one exception is the server-side BFF: a caller sending `x-token-relay: 1` also gets -`token` in `data`, because Workers `fetch()` may strip `Set-Cookie` on a -server-to-server hop and the BFF has to put the token in its own session cookie. -The browser never sends that header. +`token` in `data`, because the in-process BFF client does not forward the API's +`Set-Cookie` to the browser, and the login page has to put the token in its own +session cookie. The browser never sends that header. Only login (this step and +`POST /api/auth/login/2fa`) honours it; join and setup never return the token in +the body (see below). **If the user has 2FA enabled, no session is granted here.** The response is `{ "success": true, "data": { "requires2fa": true, "challengeToken": "…" } }` with **no** `Set-Cookie` — challenge tokens travel JSON-only, so a stolen session cookie alone can never bypass the second factor. The challenge is valid for five minutes; post it back with the TOTP code to `POST /api/auth/login/2fa` to get a -session. +session. That response follows the same delivery rule as this one: the session +arrives as the cookie, and only a caller sending `x-token-relay: 1` also gets +`token` in `data`. Returns `401` if credentials are invalid. @@ -229,8 +233,11 @@ Accept a team invite token, create the user account, and set the `__Host-inspect { "success": true, "data": { "redirect": "/inspections" } } ``` -Sets the `__Host-inspector_token` HttpOnly cookie. As with login, the JWT is not -returned to a browser and must not be stored in `localStorage`. +Sets the `__Host-inspector_token` HttpOnly cookie. The JWT is never in the +body — unlike login, this route ignores `x-token-relay`, and so does +`POST /api/auth/setup`. Their pages open a session by reading the JWT out of the +`Set-Cookie` header on the in-process API response. It must not be stored in +`localStorage`. Returns `400` if the token is expired or already used. diff --git a/docs/reference/database-schema.md b/docs/reference/database-schema.md index fbb0cbd7c..911581063 100644 --- a/docs/reference/database-schema.md +++ b/docs/reference/database-schema.md @@ -10,10 +10,10 @@ from the Drizzle definitions in `server/lib/db/schema/` — the two that | | | |---|---| | Tables | 109 | -| Columns | 1263 | +| Columns | 1265 | | Indexes (excluding primary keys) | 183 | | Database foreign keys (all legacy, frozen) | 51 | -| Columns carrying a source comment | 610 (48%) | +| Columns carrying a source comment | 612 (48%) | **Tables without `tenant_id`.** Every table holding tenant data must carry it — `npm run lint:tenant-scope` is the gate. These are the tables that are not *about* @@ -24,7 +24,7 @@ a tenant, which is the only reason to be missing it: That is 13 of 109. If a table you just added appears here, that is the bug, not the list. -**Timestamps.** 205 column(s) use `integer(..., { mode: 'timestamp_ms' })` — +**Timestamps.** 206 column(s) use `integer(..., { mode: 'timestamp_ms' })` — epoch MILLISECONDS, with no legacy `mode: 'timestamp'` columns left. Seconds and milliseconds are one multiplication apart and the mistake reads as a date tens of thousands of years out, so the Schema Rules allow only the former for @@ -269,7 +269,7 @@ neither is left blank. `[more]` marks a column whose source comment runs past ## `automation_logs` -server/lib/db/schema/inspection/automation.ts · 17 columns · primary key `id` +server/lib/db/schema/inspection/automation.ts · 18 columns · primary key `id` | Column | Type | Flags | Default | Values | Description | |---|---|---|---|---|---| @@ -290,6 +290,7 @@ neither is left blank. `[more]` marks a column whose source comment runs past | `attempts` | integer | NN | `0` | | How many times flush() has PICKED THIS ROW UP. Stamped when the batch is claimed — before anything is dispatched — and that ordering is the entire point. **[more]** | | `last_attempt_at` | integer | | | | When the last claim happened — `attempts` says how many, this says how stale. A row stuck at attempts=6 tells you nothing without it. | | `sender_identity` | text | | | | WHO SENT IT, and ON WHOSE BEHALF — recorded, not inferred. The owner of the number is not automatically the legal sender. **[more]** | +| `human_verified_at` | integer | | | | When a person passed the human check on the request that queued this row (publishing a report in a new company's first 24 hours). | **Indexes** @@ -1426,7 +1427,7 @@ neither is left blank. `[more]` marks a column whose source comment runs past ## `integration_test_results` -server/lib/db/schema/tenant/integration.ts · 8 columns · primary key `id` +server/lib/db/schema/tenant/integration.ts · 9 columns · primary key `id` > Settings "Test connection" history. Each on-demand provider probe (SMS, email, Stripe, Gemini) appends one row so the settings panels can show the LAST tested time + outcome without re-probing on every page load — and a short recent history (the helper prunes to the newest N per (tenant, target)). @@ -1440,6 +1441,7 @@ neither is left blank. `[more]` marks a column whose source comment runs past | `detail` | text | | | | Non-sensitive outcome summary (success blurb or provider error message). | | `tested_by_user_id` | text | | | | User who ran the probe (JWT sub); NULL if unknown. | | `tested_at` | integer | NN IX | | | *Timestamp, epoch milliseconds. NULL means it has not happened.* | +| `step` | text | | | | Which step of a stepwise test this row records (e.g. 'key_valid', 'send_test'); NULL for the single-shot probes. | **Indexes** diff --git a/messages/en/communication.json b/messages/en/communication.json index d53ae2a28..88c763d53 100644 --- a/messages/en/communication.json +++ b/messages/en/communication.json @@ -31,6 +31,9 @@ "comm_reason_no_email_template": "Skipped — the rule has no email template.", "comm_reason_managed_not_approved": "Skipped — the sending number is still awaiting carrier approval.", "comm_reason_cooling_window": "Waiting — client email on our shared sender unlocks in your company's first 24 hours. This sends itself at the time shown; connect your own email provider to send now.", + "comm_reason_provider_rate_limit": "Waiting — the email provider is rate limiting sends. This sends itself shortly.", + "comm_reason_provider_daily_quota": "Waiting — the daily sending limit at the email provider was reached. This sends itself after the next UTC midnight.", + "comm_reason_provider_monthly_quota": "Not sent — the monthly sending limit at the email provider was reached.", "comm_reason_fallback": "Skipped — {raw}", "comm_status_sent": "Delivered", "comm_status_failed": "Failed", diff --git a/messages/en/editor-2.json b/messages/en/editor-2.json index 864825232..dedcaa50d 100644 --- a/messages/en/editor-2.json +++ b/messages/en/editor-2.json @@ -188,7 +188,7 @@ "editor_publish_publishing": "Publishing…", "editor_publish_now": "Publish Now", "editor_publish_body": "Publishing will finalize this inspection and make the report available to clients.", - "editor_publish_cooling_notice": "Your company is less than a day old, so the client's report email waits until {unlockAt} and then sends itself — nothing to redo. The report goes live now.", + "editor_publish_cooling_notice": "Your company is less than a day old, so we will ask you to confirm you are a person before the client's report email goes out. This applies until {unlockAt}. The report goes live now.", "editor_publish_warning": "Warning: Only {rated} of {total} items have been rated ({pct}% complete).", "editor_publish_stat_items_rated": "Items rated", "editor_publish_stat_completion": "Completion", diff --git a/messages/en/misc.json b/messages/en/misc.json index 2da3bb95e..f6e1c5a65 100644 --- a/messages/en/misc.json +++ b/messages/en/misc.json @@ -254,6 +254,10 @@ "load_failed_generic": "This could not be loaded, so it may be incomplete. Reload before treating it as empty.", "media_gallery_unavailable": "Photos could not be loaded.", "load_failed_named": "{what} could not be loaded, so this may be incomplete. Reload before treating it as empty.", - "outbound_cooling_notice": "Your company is less than a day old, so client email on our shared sender — reports, agreements, invoices and automations — unlocks {unlockAt}. Everything else works now: inspections, reports, scheduling, sign-in email and team invitations.", - "outbound_cooling_notice_action": "Use your own email provider" + "outbound_cooling_notice": "Your company is less than a day old, so before client email goes out on our shared sender — reports, agreements, invoices and automations — we ask you to confirm you are a person. This applies until {unlockAt}. Everything else works now: inspections, reports, scheduling, sign-in email and team invitations. Connecting your own email provider removes the check.", + "outbound_cooling_notice_action": "Use your own email provider", + "outbound_verify_title": "Confirm you are a person", + "outbound_verify_body": "Your company is new, so we check once before each client email goes out on our shared sender. It only takes a moment.", + "outbound_verify_cancel": "Cancel", + "outbound_verify_load_failed": "The check could not load. Check your connection and try again." } diff --git a/messages/en/settings-components.json b/messages/en/settings-components.json index 6b066d3f2..df14ef888 100644 --- a/messages/en/settings-components.json +++ b/messages/en/settings-components.json @@ -286,9 +286,6 @@ "settings_stripe_webhook_secret_label": "Webhook Signing Secret", "settings_stripe_webhook_secret_hint": "Verifies payment notifications. Found after you add the webhook endpoint below (starts with whsec_).", "settings_stripe_save": "Save Stripe keys", - "settings_stripe_test_connected_prefix": "Connected:", - "settings_stripe_livemode_live": "Live", - "settings_stripe_livemode_test": "Test", "settings_stripe_webhook_endpoint": "Webhook endpoint", "settings_stripe_webhook_desc_prefix": "In Stripe → Developers → Webhooks, add an endpoint for the", "settings_stripe_webhook_desc_suffix": "event pointing at this URL, then paste its signing secret above. If you registered an endpoint before, re-point it to this URL — the signing secret stays the same.", @@ -426,7 +423,6 @@ "settings_emailsecrets_deliverability_desc": "Configure {provider} to send bounce & spam-complaint events here so suppressed addresses are never emailed again.", "settings_emailsecrets_webhook_url_label": "Webhook URL", "settings_emailsecrets_save": "Save {provider} key(s)", - "settings_emailsecrets_resend_connected": "Connected — {count} verified domain(s)", "settings_emailsecrets_validate_idle": "Validate credentials", "settings_emailsecrets_validate_busy": "Validating…", "settings_emailsecrets_creds_verified": "Credentials verified.", @@ -696,5 +692,21 @@ "settings_isn_keys_hint": "In ISN: Settings → My Access Keys. The keys carry the full permissions of the ISN user who owns them.", "settings_isn_access_key_label": "Access key", "settings_isn_secret_key_label": "Secret access key", - "settings_isn_save_error": "Could not save the ISN settings." + "settings_isn_save_error": "Could not save the ISN settings.", + "settings_emailsteps_notice": "Sends one test email to your own address.", + "settings_teststeps_recheck": "Check delivery again", + "settings_teststeps_error": "The test could not be run. Try again in a minute.", + "settings_emailsteps_key_valid": "API key accepted", + "settings_emailsteps_key_scope": "Key permissions", + "settings_emailsteps_sender_domain_verified": "Sender domain verified", + "settings_emailsteps_send_test": "Test email sent to you", + "settings_emailsteps_quota": "Sending quota", + "settings_emailsteps_delivery": "Delivery confirmed", + "settings_teststeps_status_pass": "Passed", + "settings_teststeps_status_fail": "Failed", + "settings_teststeps_status_skipped": "Skipped", + "settings_stripesteps_key_valid": "Secret key accepted", + "settings_stripesteps_account_capabilities": "Account can take payments", + "settings_stripesteps_webhook": "Webhook delivery verified", + "settings_teststeps_status_pending": "Waiting" } diff --git a/messages/es-419/communication.json b/messages/es-419/communication.json index dc3fb81c2..68e8c0c8e 100644 --- a/messages/es-419/communication.json +++ b/messages/es-419/communication.json @@ -31,6 +31,9 @@ "comm_reason_no_email_template": "Omitido: la regla no tiene una plantilla de correo electrónico.", "comm_reason_managed_not_approved": "Omitido: el número remitente todavía está en espera de la aprobación del operador.", "comm_reason_cooling_window": "En espera: el correo a clientes desde nuestro remitente compartido se habilita durante las primeras 24 horas de su empresa. Se enviará solo a la hora indicada; conecte su propio proveedor de correo para enviarlo ahora.", + "comm_reason_provider_rate_limit": "En espera: el proveedor de correo está limitando los envíos. Se enviará automáticamente en breve.", + "comm_reason_provider_daily_quota": "En espera: se alcanzó el límite diario de envíos del proveedor de correo. Se enviará automáticamente después de la próxima medianoche UTC.", + "comm_reason_provider_monthly_quota": "No enviado: se alcanzó el límite mensual de envíos del proveedor de correo.", "comm_reason_fallback": "Omitido: {raw}", "comm_status_sent": "Entregado", "comm_status_failed": "Fallido", diff --git a/messages/es-419/editor-2.json b/messages/es-419/editor-2.json index 3a96c94c9..907ddaaeb 100644 --- a/messages/es-419/editor-2.json +++ b/messages/es-419/editor-2.json @@ -188,7 +188,7 @@ "editor_publish_publishing": "Publicando…", "editor_publish_now": "Publicar ahora", "editor_publish_body": "Publicar finalizará esta inspección y pondrá el informe a disposición de los clientes.", - "editor_publish_cooling_notice": "Su empresa tiene menos de un día, así que el correo del informe al cliente esperará hasta {unlockAt} y luego se enviará solo; no hay que repetir nada. El informe queda activo ahora.", + "editor_publish_cooling_notice": "Su empresa tiene menos de un día, así que le pediremos confirmar que es una persona antes de enviar el correo del informe al cliente. Esto aplica hasta el {unlockAt}. El informe se publica ahora.", "editor_publish_warning": "Advertencia: solo se han calificado {rated} de {total} elementos ({pct}% completo).", "editor_publish_stat_items_rated": "Elementos calificados", "editor_publish_stat_completion": "Avance", diff --git a/messages/es-419/misc.json b/messages/es-419/misc.json index 73f6e0d68..c4bcde95b 100644 --- a/messages/es-419/misc.json +++ b/messages/es-419/misc.json @@ -254,6 +254,10 @@ "load_failed_generic": "Esto no se pudo cargar, así que puede estar incompleto. Vuelva a cargar antes de darlo por vacío.", "media_gallery_unavailable": "No se pudieron cargar las fotos.", "load_failed_named": "No se pudo cargar {what}, así que esto puede estar incompleto. Vuelva a cargar antes de darlo por vacío.", - "outbound_cooling_notice": "Su empresa tiene menos de un día, así que el correo a clientes desde nuestro remitente compartido — informes, acuerdos, facturas y automatizaciones — se habilita el {unlockAt}. Todo lo demás ya funciona: inspecciones, informes, agenda, correo de inicio de sesión e invitaciones de equipo.", - "outbound_cooling_notice_action": "Use su propio proveedor de correo" + "outbound_cooling_notice": "Su empresa tiene menos de un día, así que antes de enviar correo a clientes desde nuestro remitente compartido — informes, acuerdos, facturas y automatizaciones — le pedimos confirmar que es una persona. Esto aplica hasta el {unlockAt}. Todo lo demás ya funciona: inspecciones, informes, agenda, correo de inicio de sesión e invitaciones de equipo. Conectar su propio proveedor de correo elimina la verificación.", + "outbound_cooling_notice_action": "Use su propio proveedor de correo", + "outbound_verify_title": "Confirme que es una persona", + "outbound_verify_body": "Su empresa es nueva, así que verificamos una vez antes de cada correo a clientes desde nuestro remitente compartido. Solo toma un momento.", + "outbound_verify_cancel": "Cancelar", + "outbound_verify_load_failed": "No se pudo cargar la verificación. Revise su conexión e inténtelo de nuevo." } diff --git a/messages/es-419/settings-components.json b/messages/es-419/settings-components.json index cc5cc80e9..7486effca 100644 --- a/messages/es-419/settings-components.json +++ b/messages/es-419/settings-components.json @@ -286,9 +286,6 @@ "settings_stripe_webhook_secret_label": "Secreto de firma del webhook", "settings_stripe_webhook_secret_hint": "Verifica las notificaciones de pago. Se obtiene después de agregar el endpoint del webhook de abajo (comienza con whsec_).", "settings_stripe_save": "Guardar las claves de Stripe", - "settings_stripe_test_connected_prefix": "Conectado:", - "settings_stripe_livemode_live": "Producción", - "settings_stripe_livemode_test": "Prueba", "settings_stripe_webhook_endpoint": "Endpoint del webhook", "settings_stripe_webhook_desc_prefix": "En Stripe → Developers → Webhooks, agregue un endpoint para el evento", "settings_stripe_webhook_desc_suffix": "que apunte a esta URL y luego pegue arriba su secreto de firma. Si ya registró un endpoint antes, vuelva a apuntarlo a esta URL: el secreto de firma no cambia.", @@ -426,7 +423,6 @@ "settings_emailsecrets_deliverability_desc": "Configure {provider} para que envíe aquí los eventos de rebote y de queja por spam, de modo que nunca se vuelva a escribir a las direcciones suprimidas.", "settings_emailsecrets_webhook_url_label": "URL del webhook", "settings_emailsecrets_save": "Guardar la(s) clave(s) de {provider}", - "settings_emailsecrets_resend_connected": "Conectado — {count} dominio(s) verificado(s)", "settings_emailsecrets_validate_idle": "Validar las credenciales", "settings_emailsecrets_validate_busy": "Validando…", "settings_emailsecrets_creds_verified": "Credenciales verificadas.", @@ -696,5 +692,21 @@ "settings_isn_keys_hint": "En ISN: Settings → My Access Keys. Las claves tienen todos los permisos del usuario de ISN al que pertenecen.", "settings_isn_access_key_label": "Clave de acceso", "settings_isn_secret_key_label": "Clave de acceso secreta", - "settings_isn_save_error": "No se pudo guardar la configuración de ISN." + "settings_isn_save_error": "No se pudo guardar la configuración de ISN.", + "settings_emailsteps_notice": "Envíe un correo de prueba a su propia dirección.", + "settings_teststeps_recheck": "Volver a comprobar la entrega", + "settings_teststeps_error": "No se pudo ejecutar la prueba. Inténtalo de nuevo en un minuto.", + "settings_emailsteps_key_valid": "Clave de API aceptada", + "settings_emailsteps_key_scope": "Permisos de la clave", + "settings_emailsteps_sender_domain_verified": "Dominio del remitente verificado", + "settings_emailsteps_send_test": "Correo de prueba enviado a usted", + "settings_emailsteps_quota": "Cuota de envío", + "settings_emailsteps_delivery": "Entrega confirmada", + "settings_teststeps_status_pass": "Correcto", + "settings_teststeps_status_fail": "Fallido", + "settings_teststeps_status_skipped": "Omitido", + "settings_stripesteps_key_valid": "Clave secreta aceptada", + "settings_stripesteps_account_capabilities": "La cuenta puede cobrar pagos", + "settings_stripesteps_webhook": "Entrega de webhook verificada", + "settings_teststeps_status_pending": "En espera" } diff --git a/migrations/0001_integration_test_step.sql b/migrations/0001_integration_test_step.sql new file mode 100644 index 000000000..1d171e871 --- /dev/null +++ b/migrations/0001_integration_test_step.sql @@ -0,0 +1 @@ +ALTER TABLE `integration_test_results` ADD `step` text; \ No newline at end of file diff --git a/migrations/0002_automation_log_human_verified.sql b/migrations/0002_automation_log_human_verified.sql new file mode 100644 index 000000000..5e4903308 --- /dev/null +++ b/migrations/0002_automation_log_human_verified.sql @@ -0,0 +1 @@ +ALTER TABLE `automation_logs` ADD `human_verified_at` integer; \ No newline at end of file diff --git a/migrations/meta/0001_snapshot.json b/migrations/meta/0001_snapshot.json new file mode 100644 index 000000000..31742ebce --- /dev/null +++ b/migrations/meta/0001_snapshot.json @@ -0,0 +1,12450 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "174ac276-45d1-4f0b-85a8-90b5d4a62a47", + "prevId": "38205577-a1ac-4d65-8836-5721fa63a0a9", + "tables": { + "agent_terms_acceptances": { + "name": "agent_terms_acceptances", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "doc": { + "name": "doc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_agent_terms_acceptances_user": { + "name": "idx_agent_terms_acceptances_user", + "columns": [ + "user_id", + "accepted_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agreement_requests": { + "name": "agreement_requests", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agreement_id": { + "name": "agreement_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_email": { + "name": "client_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "signed_at": { + "name": "signed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "viewed_at": { + "name": "viewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sent_at": { + "name": "sent_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspector_signature_base64": { + "name": "inspector_signature_base64", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspector_signed_at": { + "name": "inspector_signed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspector_user_id": { + "name": "inspector_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_snapshot": { + "name": "content_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_policy": { + "name": "completion_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'all'" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signer_legal_name": { + "name": "signer_legal_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signer_company_name": { + "name": "signer_company_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_agreement_requests_verify_token": { + "name": "idx_agreement_requests_verify_token", + "columns": [ + "verification_token" + ], + "isUnique": true + }, + "idx_agreement_requests_tenant": { + "name": "idx_agreement_requests_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_agreement_requests_inspection": { + "name": "idx_agreement_requests_inspection", + "columns": [ + "inspection_id" + ], + "isUnique": false + }, + "idx_agreement_requests_token_hash": { + "name": "idx_agreement_requests_token_hash", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "agreement_requests_tenant_id_tenants_id_fk": { + "name": "agreement_requests_tenant_id_tenants_id_fk", + "tableFrom": "agreement_requests", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agreement_requests_inspection_id_inspections_id_fk": { + "name": "agreement_requests_inspection_id_inspections_id_fk", + "tableFrom": "agreement_requests", + "tableTo": "inspections", + "columnsFrom": [ + "inspection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agreement_requests_agreement_id_agreements_id_fk": { + "name": "agreement_requests_agreement_id_agreements_id_fk", + "tableFrom": "agreement_requests", + "tableTo": "agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agreement_requests_inspector_user_id_users_id_fk": { + "name": "agreement_requests_inspector_user_id_users_id_fk", + "tableFrom": "agreement_requests", + "tableTo": "users", + "columnsFrom": [ + "inspector_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agreement_signers": { + "name": "agreement_signers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client'" + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_enc": { + "name": "token_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "signature_base64": { + "name": "signature_base64", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signed_at": { + "name": "signed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "viewed_at": { + "name": "viewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "on_behalf_of": { + "name": "on_behalf_of", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "on_behalf_disclaimer": { + "name": "on_behalf_disclaimer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_reminded_at": { + "name": "last_reminded_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language_disclosure_version": { + "name": "language_disclosure_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attribution_basis": { + "name": "attribution_basis", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attribution_source": { + "name": "attribution_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributed_at": { + "name": "attributed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_agreement_signers_tenant_request": { + "name": "idx_agreement_signers_tenant_request", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": false + }, + "idx_agreement_signers_request_email": { + "name": "idx_agreement_signers_request_email", + "columns": [ + "request_id", + "email" + ], + "isUnique": true + }, + "idx_agreement_signers_token_hash": { + "name": "idx_agreement_signers_token_hash", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agreements": { + "name": "agreements", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_agreements_tenant": { + "name": "idx_agreements_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "agreements_tenant_id_tenants_id_fk": { + "name": "agreements_tenant_id_tenants_id_fk", + "tableFrom": "agreements", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_call_provenance": { + "name": "ai_call_provenance", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt_version": { + "name": "prompt_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_ai_call_provenance_tenant_created": { + "name": "idx_ai_call_provenance_tenant_created", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_content_reviews": { + "name": "ai_content_reviews", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "artifact_type": { + "name": "artifact_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ai_call_id": { + "name": "ai_call_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_ai_content_reviews_ai_call": { + "name": "idx_ai_content_reviews_ai_call", + "columns": [ + "ai_call_id" + ], + "isUnique": false + }, + "uq_ai_content_reviews_person_call": { + "name": "uq_ai_content_reviews_person_call", + "columns": [ + "tenant_id", + "artifact_type", + "artifact_id", + "ai_call_id", + "reviewed_by" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_logs": { + "name": "automation_logs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient": { + "name": "recipient", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_role_key": { + "name": "recipient_role_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'email'" + }, + "send_at": { + "name": "send_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_contact_id": { + "name": "recipient_contact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notice_id": { + "name": "notice_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sender_identity": { + "name": "sender_identity", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_logs_pending": { + "name": "idx_automation_logs_pending", + "columns": [ + "tenant_id", + "status", + "send_at" + ], + "isUnique": false + }, + "idx_automation_logs_insp": { + "name": "idx_automation_logs_insp", + "columns": [ + "inspection_id" + ], + "isUnique": false + }, + "uq_automation_logs_event": { + "name": "uq_automation_logs_event", + "columns": [ + "automation_id", + "inspection_id", + "event_id", + "channel", + "recipient" + ], + "isUnique": true, + "where": "event_id IS NOT NULL" + } + }, + "foreignKeys": { + "automation_logs_tenant_id_tenants_id_fk": { + "name": "automation_logs_tenant_id_tenants_id_fk", + "tableFrom": "automation_logs", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automations": { + "name": "automations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_kind": { + "name": "recipient_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_role_profile_id": { + "name": "recipient_role_profile_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delay_minutes": { + "name": "delay_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "email_template_id": { + "name": "email_template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "conditions": { + "name": "conditions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channels": { + "name": "channels", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[\"email\"]'" + }, + "sms_template_id": { + "name": "sms_template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "in_app_template_id": { + "name": "in_app_template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automations_tenant": { + "name": "idx_automations_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automations_tenant_id_tenants_id_fk": { + "name": "automations_tenant_id_tenants_id_fk", + "tableFrom": "automations", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "availability": { + "name": "availability", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspector_id": { + "name": "inspector_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "day_of_week": { + "name": "day_of_week", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_time": { + "name": "start_time", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_time": { + "name": "end_time", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_availability_window_unique": { + "name": "idx_availability_window_unique", + "columns": [ + "inspector_id", + "day_of_week", + "start_time" + ], + "isUnique": true + } + }, + "foreignKeys": { + "availability_tenant_id_tenants_id_fk": { + "name": "availability_tenant_id_tenants_id_fk", + "tableFrom": "availability", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "availability_inspector_id_users_id_fk": { + "name": "availability_inspector_id_users_id_fk", + "tableFrom": "availability", + "tableTo": "users", + "columnsFrom": [ + "inspector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "availability_overrides": { + "name": "availability_overrides", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspector_id": { + "name": "inspector_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_available": { + "name": "is_available", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "start_time": { + "name": "start_time", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_time": { + "name": "end_time", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "transparency": { + "name": "transparency", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_avail_overrides_block_unique": { + "name": "idx_avail_overrides_block_unique", + "columns": [ + "inspector_id", + "date" + ], + "isUnique": true, + "where": "is_available = 0 AND source IS NULL" + }, + "uq_avail_overrides_external": { + "name": "uq_avail_overrides_external", + "columns": [ + "inspector_id", + "source", + "external_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "availability_overrides_tenant_id_tenants_id_fk": { + "name": "availability_overrides_tenant_id_tenants_id_fk", + "tableFrom": "availability_overrides", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "availability_overrides_inspector_id_users_id_fk": { + "name": "availability_overrides_inspector_id_users_id_fk", + "tableFrom": "availability_overrides", + "tableTo": "users", + "columnsFrom": [ + "inspector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "calendar_blocks": { + "name": "calendar_blocks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_time": { + "name": "start_time", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_time": { + "name": "end_time", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_all_day": { + "name": "is_all_day", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_calendar_blocks_tenant_user_date": { + "name": "idx_calendar_blocks_tenant_user_date", + "columns": [ + "tenant_id", + "user_id", + "date" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "calendar_connection_read_calendars": { + "name": "calendar_connection_read_calendars", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_calendar_id": { + "name": "external_calendar_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_role": { + "name": "access_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_conn_read_cal": { + "name": "uq_conn_read_cal", + "columns": [ + "connection_id", + "external_calendar_id" + ], + "isUnique": true + }, + "idx_conn_read_cal_tenant": { + "name": "idx_conn_read_cal_tenant", + "columns": [ + "tenant_id", + "connection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "calendar_connections": { + "name": "calendar_connections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentials_enc": { + "name": "credentials_enc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentials_dek_enc": { + "name": "credentials_dek_enc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "calendar_id": { + "name": "calendar_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connected_at": { + "name": "connected_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_calendar_connections_user_provider": { + "name": "uq_calendar_connections_user_provider", + "columns": [ + "user_id", + "provider" + ], + "isUnique": true + }, + "idx_calendar_connections_tenant_user": { + "name": "idx_calendar_connections_tenant_user", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "calendar_external_links": { + "name": "calendar_external_links", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_calendar_external_links_entity": { + "name": "uq_calendar_external_links_entity", + "columns": [ + "tenant_id", + "provider", + "entity_type", + "entity_id" + ], + "isUnique": true + }, + "idx_calendar_external_links_user": { + "name": "idx_calendar_external_links_user", + "columns": [ + "tenant_id", + "user_id", + "provider" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "comment_usage": { + "name": "comment_usage", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "comment_id": { + "name": "comment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_count": { + "name": "use_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_comment_usage_user_last_used": { + "name": "idx_comment_usage_user_last_used", + "columns": [ + "tenant_id", + "user_id", + "last_used_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "comment_usage_comment_id_comments_id_fk": { + "name": "comment_usage_comment_id_comments_id_fk", + "tableFrom": "comment_usage", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "comment_usage_tenant_id_user_id_comment_id_pk": { + "columns": [ + "tenant_id", + "user_id", + "comment_id" + ], + "name": "comment_usage_tenant_id_user_id_comment_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "comments": { + "name": "comments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "section": { + "name": "section", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "library_id": { + "name": "library_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "section_ids": { + "name": "section_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "item_labels": { + "name": "item_labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trigger_code": { + "name": "trigger_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "search_keywords": { + "name": "search_keywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "item_label": { + "name": "item_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repair_summary": { + "name": "repair_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recommended_contractor_type_id": { + "name": "recommended_contractor_type_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "edited_at": { + "name": "edited_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "import_hash": { + "name": "import_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_comments_tenant": { + "name": "idx_comments_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_comments_library_id": { + "name": "idx_comments_library_id", + "columns": [ + "library_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "comments_tenant_id_tenants_id_fk": { + "name": "comments_tenant_id_tenants_id_fk", + "tableFrom": "comments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "concierge_confirm_tokens": { + "name": "concierge_confirm_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_email": { + "name": "client_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_concierge_tokens_expiry": { + "name": "idx_concierge_tokens_expiry", + "columns": [ + "expires_at" + ], + "isUnique": false + }, + "idx_concierge_confirm_token_hash": { + "name": "idx_concierge_confirm_token_hash", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "contact_role_profiles": { + "name": "contact_role_profiles", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_template_id": { + "name": "email_template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sms_template_id": { + "name": "sms_template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability_overrides": { + "name": "capability_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_crp_tenant": { + "name": "idx_crp_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "uq_crp_tenant_key": { + "name": "uq_crp_tenant_key", + "columns": [ + "tenant_id", + "key" + ], + "isUnique": true, + "where": "is_active = 1" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "contacts": { + "name": "contacts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agency": { + "name": "agency", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_user_id": { + "name": "agent_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_linked_at": { + "name": "agent_linked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_revoked_at": { + "name": "agent_revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_contacts_type": { + "name": "idx_contacts_type", + "columns": [ + "tenant_id", + "type" + ], + "isUnique": false + }, + "uq_contacts_tenant_email": { + "name": "uq_contacts_tenant_email", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true, + "where": "email IS NOT NULL AND archived_at IS NULL" + }, + "uq_contacts_tenant_agent_user": { + "name": "uq_contacts_tenant_agent_user", + "columns": [ + "tenant_id", + "agent_user_id" + ], + "isUnique": true, + "where": "agent_user_id IS NOT NULL AND archived_at IS NULL" + }, + "idx_contacts_agent_user": { + "name": "idx_contacts_agent_user", + "columns": [ + "agent_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "contacts_tenant_id_tenants_id_fk": { + "name": "contacts_tenant_id_tenants_id_fk", + "tableFrom": "contacts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "contractor_types": { + "name": "contractor_types", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trade_slug": { + "name": "trade_slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_contractor_types_tenant": { + "name": "idx_contractor_types_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "uq_contractor_types_tenant_trade": { + "name": "uq_contractor_types_tenant_trade", + "columns": [ + "tenant_id", + "trade_slug" + ], + "isUnique": true, + "where": "trade_slug IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cost_items": { + "name": "cost_items", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "building_id": { + "name": "building_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "instance_index": { + "name": "instance_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit_id": { + "name": "unit_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "finding_key": { + "name": "finding_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "system": { + "name": "system", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "component": { + "name": "component", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cost_method": { + "name": "cost_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uom": { + "name": "uom", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit_cost_cents": { + "name": "unit_cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lump_sum_cents": { + "name": "lump_sum_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "eul": { + "name": "eul", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "eff_age": { + "name": "eff_age", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rul": { + "name": "rul", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "suggested_remedy": { + "name": "suggested_remedy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_ref": { + "name": "section_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "photo_ref": { + "name": "photo_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_cost_items_tenant_inspection": { + "name": "idx_cost_items_tenant_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + }, + "idx_cost_items_finding_key": { + "name": "idx_cost_items_finding_key", + "columns": [ + "finding_key" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "defect_categories": { + "name": "defect_categories", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#6b7280'" + }, + "is_summary_driver": { + "name": "is_summary_driver", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_seed": { + "name": "is_seed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_defect_categories_tenant": { + "name": "idx_defect_categories_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deployment_legal_versions": { + "name": "deployment_legal_versions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "doc": { + "name": "doc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body_snapshot": { + "name": "body_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_deployment_legal_versions_doc_hash": { + "name": "uq_deployment_legal_versions_doc_hash", + "columns": [ + "doc", + "content_hash" + ], + "isUnique": true + }, + "idx_deployment_legal_versions_latest": { + "name": "idx_deployment_legal_versions_latest", + "columns": [ + "doc", + "published_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discount_codes": { + "name": "discount_codes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uses_count": { + "name": "uses_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_discount_codes_tenant": { + "name": "idx_discount_codes_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "uq_discount_codes_code_tenant": { + "name": "uq_discount_codes_code_tenant", + "columns": [ + "upper(code)", + "tenant_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "discount_codes_tenant_id_tenants_id_fk": { + "name": "discount_codes_tenant_id_tenants_id_fk", + "tableFrom": "discount_codes", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "document_review_items": { + "name": "document_review_items", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "document_key": { + "name": "document_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_requested": { + "name": "is_requested", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_received": { + "name": "is_received", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_reviewed": { + "name": "is_reviewed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_na": { + "name": "is_na", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_doc_review_inspection": { + "name": "idx_doc_review_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + }, + "uq_doc_review_item": { + "name": "uq_doc_review_item", + "columns": [ + "inspection_id", + "document_key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_suppressions": { + "name": "email_suppressions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_provider": { + "name": "source_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_event_id": { + "name": "provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_email_suppressions_email": { + "name": "idx_email_suppressions_email", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "erasure_log": { + "name": "erasure_log", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_email": { + "name": "subject_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identity_basis": { + "name": "identity_basis", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "decisions_json": { + "name": "decisions_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retained_count": { + "name": "retained_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "anonymized_count": { + "name": "anonymized_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "deleted_count": { + "name": "deleted_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "response_note": { + "name": "response_note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_erasure_log_tenant": { + "name": "idx_erasure_log_tenant", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "esign_audit_logs": { + "name": "esign_audit_logs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prev_hash": { + "name": "prev_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_fingerprint": { + "name": "key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_esign_audit_logs_request": { + "name": "idx_esign_audit_logs_request", + "columns": [ + "tenant_id", + "request_id", + "created_at" + ], + "isUnique": false + }, + "idx_esign_audit_logs_event_dedup": { + "name": "idx_esign_audit_logs_event_dedup", + "columns": [ + "tenant_id", + "request_id", + "event" + ], + "isUnique": true, + "where": "event NOT LIKE 'signer.%'" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "event_types": { + "name": "event_types", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_duration_min": { + "name": "default_duration_min", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "default_price_cents": { + "name": "default_price_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#6366f1'" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "follow_up_delay_hours": { + "name": "follow_up_delay_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 72 + } + }, + "indexes": { + "uq_event_types_tenant_slug": { + "name": "uq_event_types_tenant_slug", + "columns": [ + "tenant_id", + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "event_types_tenant_id_tenants_id_fk": { + "name": "event_types_tenant_id_tenants_id_fk", + "tableFrom": "event_types", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "idempotency_keys": { + "name": "idempotency_keys", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'in_flight'" + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_idempotency_expires": { + "name": "idx_idempotency_expires", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "idempotency_keys_tenant_id_key_pk": { + "columns": [ + "tenant_id", + "key" + ], + "name": "idempotency_keys_tenant_id_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_access_tokens": { + "name": "inspection_access_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_enc": { + "name": "token_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "view_tracking_objected_at": { + "name": "view_tracking_objected_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_iat_inspection": { + "name": "idx_iat_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + }, + "idx_iat_recipient": { + "name": "idx_iat_recipient", + "columns": [ + "inspection_id", + "recipient_email" + ], + "isUnique": true + }, + "idx_iat_token_hash": { + "name": "idx_iat_token_hash", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "inspection_access_tokens_tenant_id_tenants_id_fk": { + "name": "inspection_access_tokens_tenant_id_tenants_id_fk", + "tableFrom": "inspection_access_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_events": { + "name": "inspection_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspector_id": { + "name": "inspector_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration_min": { + "name": "duration_min", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'scheduled'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "results_received_at": { + "name": "results_received_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_inspection_events_scheduled": { + "name": "idx_inspection_events_scheduled", + "columns": [ + "tenant_id", + "scheduled_at" + ], + "isUnique": false + }, + "idx_inspection_events_inspection": { + "name": "idx_inspection_events_inspection", + "columns": [ + "inspection_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "inspection_events_tenant_id_tenants_id_fk": { + "name": "inspection_events_tenant_id_tenants_id_fk", + "tableFrom": "inspection_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspection_events_inspection_id_inspections_id_fk": { + "name": "inspection_events_inspection_id_inspections_id_fk", + "tableFrom": "inspection_events", + "tableTo": "inspections", + "columnsFrom": [ + "inspection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inspection_events_event_type_id_event_types_id_fk": { + "name": "inspection_events_event_type_id_event_types_id_fk", + "tableFrom": "inspection_events", + "tableTo": "event_types", + "columnsFrom": [ + "event_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspection_events_inspector_id_users_id_fk": { + "name": "inspection_events_inspector_id_users_id_fk", + "tableFrom": "inspection_events", + "tableTo": "users", + "columnsFrom": [ + "inspector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_inspectors": { + "name": "inspection_inspectors", + "columns": { + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'lead'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_insp_inspectors_tenant_user": { + "name": "idx_insp_inspectors_tenant_user", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "idx_insp_inspectors_user": { + "name": "idx_insp_inspectors_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "inspection_inspectors_inspection_id_user_id_pk": { + "columns": [ + "inspection_id", + "user_id" + ], + "name": "inspection_inspectors_inspection_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_item_tag_links": { + "name": "inspection_item_tag_links", + "columns": { + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_tag_links_tenant": { + "name": "idx_tag_links_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_tag_links_tag": { + "name": "idx_tag_links_tag", + "columns": [ + "tag_id" + ], + "isUnique": false + }, + "idx_tag_links_inspection_item": { + "name": "idx_tag_links_inspection_item", + "columns": [ + "inspection_id", + "item_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "inspection_item_tag_links_inspection_id_item_id_tag_id_pk": { + "columns": [ + "inspection_id", + "item_id", + "tag_id" + ], + "name": "inspection_item_tag_links_inspection_id_item_id_tag_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_media_pool": { + "name": "inspection_media_pool", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exif_data": { + "name": "exif_data", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "annotations": { + "name": "annotations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "caption": { + "name": "caption", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'photo'" + }, + "stream_uid": { + "name": "stream_uid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "poster_pct": { + "name": "poster_pct", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_sec": { + "name": "duration_sec", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'stream'" + }, + "poster_key": { + "name": "poster_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_media_pool_tenant": { + "name": "idx_media_pool_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_media_pool_inspection": { + "name": "idx_media_pool_inspection", + "columns": [ + "inspection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_messages": { + "name": "inspection_messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "from_role": { + "name": "from_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attachments": { + "name": "attachments", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read_at": { + "name": "read_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from_user_id": { + "name": "from_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_msg_inspection": { + "name": "idx_msg_inspection", + "columns": [ + "inspection_id", + "created_at" + ], + "isUnique": false + }, + "idx_msg_contact": { + "name": "idx_msg_contact", + "columns": [ + "tenant_id", + "contact_id", + "created_at" + ], + "isUnique": false + }, + "idx_msg_unread": { + "name": "idx_msg_unread", + "columns": [ + "tenant_id", + "contact_id", + "from_role" + ], + "isUnique": false, + "where": "\"inspection_messages\".\"read_at\" IS NULL" + } + }, + "foreignKeys": { + "inspection_messages_tenant_id_tenants_id_fk": { + "name": "inspection_messages_tenant_id_tenants_id_fk", + "tableFrom": "inspection_messages", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspection_messages_inspection_id_inspections_id_fk": { + "name": "inspection_messages_inspection_id_inspections_id_fk", + "tableFrom": "inspection_messages", + "tableTo": "inspections", + "columnsFrom": [ + "inspection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_people": { + "name": "inspection_people", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_profile_id": { + "name": "role_profile_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_ip_tenant": { + "name": "idx_ip_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "uq_ip_insp_contact_role": { + "name": "uq_ip_insp_contact_role", + "columns": [ + "inspection_id", + "contact_id", + "role_profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_requests": { + "name": "inspection_requests", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_email": { + "name": "client_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_phone": { + "name": "client_phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "property_address": { + "name": "property_address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "property_city": { + "name": "property_city", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "property_state": { + "name": "property_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "property_zip": { + "name": "property_zip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_inspection_requests_tenant": { + "name": "idx_inspection_requests_tenant", + "columns": [ + "tenant_id", + "status", + "scheduled_at" + ], + "isUnique": false + }, + "idx_inspection_requests_email": { + "name": "idx_inspection_requests_email", + "columns": [ + "tenant_id", + "client_email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "inspection_requests_tenant_id_tenants_id_fk": { + "name": "inspection_requests_tenant_id_tenants_id_fk", + "tableFrom": "inspection_requests", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_results": { + "name": "inspection_results", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ydoc_state": { + "name": "ydoc_state", + "type": "blob", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rating_system_id": { + "name": "rating_system_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rating_system_snapshot": { + "name": "rating_system_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_id": { + "name": "report_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_results_tenant": { + "name": "idx_results_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_results_inspection": { + "name": "idx_results_inspection", + "columns": [ + "inspection_id" + ], + "isUnique": false + }, + "uq_results_report": { + "name": "uq_results_report", + "columns": [ + "report_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "inspection_results_tenant_id_tenants_id_fk": { + "name": "inspection_results_tenant_id_tenants_id_fk", + "tableFrom": "inspection_results", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspection_results_inspection_id_inspections_id_fk": { + "name": "inspection_results_inspection_id_inspections_id_fk", + "tableFrom": "inspection_results", + "tableTo": "inspections", + "columnsFrom": [ + "inspection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_service_pay_splits": { + "name": "inspection_service_pay_splits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_service_id": { + "name": "inspection_service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "locked_at": { + "name": "locked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "corrects_split_id": { + "name": "corrects_split_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_pay_split_line_user": { + "name": "uq_pay_split_line_user", + "columns": [ + "tenant_id", + "inspection_service_id", + "user_id" + ], + "isUnique": true, + "where": "corrects_split_id IS NULL" + }, + "idx_pay_split_user": { + "name": "idx_pay_split_user", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "idx_pay_split_line": { + "name": "idx_pay_split_line", + "columns": [ + "tenant_id", + "inspection_service_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_services": { + "name": "inspection_services", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_override_cents": { + "name": "price_override_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_snapshot": { + "name": "name_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_snapshot_cents": { + "name": "price_snapshot_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_insp_services_tenant": { + "name": "idx_insp_services_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_insp_services_insp": { + "name": "idx_insp_services_insp", + "columns": [ + "inspection_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "inspection_services_tenant_id_tenants_id_fk": { + "name": "inspection_services_tenant_id_tenants_id_fk", + "tableFrom": "inspection_services", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspection_services_inspection_id_inspections_id_fk": { + "name": "inspection_services_inspection_id_inspections_id_fk", + "tableFrom": "inspection_services", + "tableTo": "inspections", + "columnsFrom": [ + "inspection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inspection_services_service_id_services_id_fk": { + "name": "inspection_services_service_id_services_id_fk", + "tableFrom": "inspection_services", + "tableTo": "services", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_types": { + "name": "inspection_types", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "based_on": { + "name": "based_on", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_inspection_types_tenant_name": { + "name": "idx_inspection_types_tenant_name", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_units": { + "name": "inspection_units", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_unit_id": { + "name": "parent_unit_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unit'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "attrs": { + "name": "attrs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_inspection_units_tenant_inspection": { + "name": "idx_inspection_units_tenant_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + }, + "idx_inspection_units_parent": { + "name": "idx_inspection_units_parent", + "columns": [ + "parent_unit_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspections": { + "name": "inspections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspector_id": { + "name": "inspector_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "property_address": { + "name": "property_address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "address_place_id": { + "name": "address_place_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_city": { + "name": "address_city", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_state": { + "name": "address_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_zip": { + "name": "address_zip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_county": { + "name": "address_county", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_lat": { + "name": "address_lat", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_lng": { + "name": "address_lng", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_geocoded_at": { + "name": "address_geocoded_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'requested'" + }, + "report_status": { + "name": "report_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'in_progress'" + }, + "payment_status": { + "name": "payment_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unpaid'" + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancel_reason": { + "name": "cancel_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancel_notes": { + "name": "cancel_notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_payment_required": { + "name": "is_payment_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_agreement_required": { + "name": "is_agreement_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_auto_sign_on_publish": { + "name": "is_auto_sign_on_publish", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "discount_code_id": { + "name": "discount_code_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discount_amount_cents": { + "name": "discount_amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "closing_date": { + "name": "closing_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "referral_source": { + "name": "referral_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_number": { + "name": "reference_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "internal_notes": { + "name": "internal_notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "year_built": { + "name": "year_built", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sqft": { + "name": "sqft", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "foundation_type": { + "name": "foundation_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bedrooms": { + "name": "bedrooms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bathrooms": { + "name": "bathrooms", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lot_size": { + "name": "lot_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "property_facts": { + "name": "property_facts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cover_photo_id": { + "name": "cover_photo_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cover_crop": { + "name": "cover_crop", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cover_image_key": { + "name": "cover_image_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commercial_subtype": { + "name": "commercial_subtype", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_tier": { + "name": "report_tier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "county": { + "name": "county", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_automations_disabled": { + "name": "is_automations_disabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "template_snapshot": { + "name": "template_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "template_snapshot_version": { + "name": "template_snapshot_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 1 + }, + "profile_override": { + "name": "profile_override", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "require_defect_fields_override": { + "name": "require_defect_fields_override", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "concierge_status": { + "name": "concierge_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_team_mode": { + "name": "is_team_mode", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "source_inspection_id": { + "name": "source_inspection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "root_inspection_id": { + "name": "root_inspection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reinspection_round": { + "name": "reinspection_round", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit_inspection_mode": { + "name": "unit_inspection_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'tagged'" + }, + "location_options": { + "name": "location_options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sampling_declaration": { + "name": "sampling_declaration", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pca_narrative": { + "name": "pca_narrative", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deviations": { + "name": "deviations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_photo_mode": { + "name": "report_photo_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scheduled_start_ms": { + "name": "scheduled_start_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scheduled_end_ms": { + "name": "scheduled_end_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_min": { + "name": "duration_min", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "badge_layout_override": { + "name": "badge_layout_override", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_photo_columns": { + "name": "report_photo_columns", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "referred_by_contact_id": { + "name": "referred_by_contact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unlocked_at": { + "name": "unlocked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unlocked_by": { + "name": "unlocked_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unlock_reason": { + "name": "unlock_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reports_generated_at": { + "name": "reports_generated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deposit_required_cents": { + "name": "deposit_required_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_deposit_overridden": { + "name": "is_deposit_overridden", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_inspections_request": { + "name": "idx_inspections_request", + "columns": [ + "request_id" + ], + "isUnique": false + }, + "idx_inspections_tenant_status": { + "name": "idx_inspections_tenant_status", + "columns": [ + "tenant_id", + "status" + ], + "isUnique": false + }, + "idx_inspections_tenant_date": { + "name": "idx_inspections_tenant_date", + "columns": [ + "tenant_id", + "date" + ], + "isUnique": false + }, + "idx_inspections_inspector_date": { + "name": "idx_inspections_inspector_date", + "columns": [ + "inspector_id", + "date" + ], + "isUnique": false + }, + "idx_inspections_root": { + "name": "idx_inspections_root", + "columns": [ + "root_inspection_id" + ], + "isUnique": false + }, + "idx_inspections_tenant_created": { + "name": "idx_inspections_tenant_created", + "columns": [ + "tenant_id", + "created_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "inspections_tenant_id_tenants_id_fk": { + "name": "inspections_tenant_id_tenants_id_fk", + "tableFrom": "inspections", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspections_inspector_id_users_id_fk": { + "name": "inspections_inspector_id_users_id_fk", + "tableFrom": "inspections", + "tableTo": "users", + "columnsFrom": [ + "inspector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspections_template_id_templates_id_fk": { + "name": "inspections_template_id_templates_id_fk", + "tableFrom": "inspections", + "tableTo": "templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspections_discount_code_id_discount_codes_id_fk": { + "name": "inspections_discount_code_id_discount_codes_id_fk", + "tableFrom": "inspections", + "tableTo": "discount_codes", + "columnsFrom": [ + "discount_code_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspections_request_id_inspection_requests_id_fk": { + "name": "inspections_request_id_inspection_requests_id_fk", + "tableFrom": "inspections", + "tableTo": "inspection_requests", + "columnsFrom": [ + "request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspector_credentials": { + "name": "inspector_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "member_number": { + "name": "member_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_r2_key": { + "name": "image_r2_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_inspector_credentials_tenant": { + "name": "idx_inspector_credentials_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_inspector_credentials_user": { + "name": "idx_inspector_credentials_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspector_service_areas": { + "name": "inspector_service_areas", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "zip_prefix": { + "name": "zip_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_inspector_service_areas": { + "name": "uq_inspector_service_areas", + "columns": [ + "tenant_id", + "user_id", + "zip_prefix" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invoices": { + "name": "invoices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_email": { + "name": "client_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "line_items": { + "name": "line_items", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sent_at": { + "name": "sent_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "paid_at": { + "name": "paid_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method": { + "name": "payment_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "partial_paid_at": { + "name": "partial_paid_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voided_at": { + "name": "voided_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "qbo_sync_status": { + "name": "qbo_sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'USD'" + }, + "amount_paid_cents": { + "name": "amount_paid_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_number": { + "name": "invoice_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_invoices_inspection": { + "name": "idx_invoices_inspection", + "columns": [ + "inspection_id" + ], + "isUnique": false + }, + "idx_invoices_contact": { + "name": "idx_invoices_contact", + "columns": [ + "tenant_id", + "contact_id" + ], + "isUnique": false + }, + "uq_invoices_tenant_number": { + "name": "uq_invoices_tenant_number", + "columns": [ + "tenant_id", + "invoice_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invoices_tenant_id_tenants_id_fk": { + "name": "invoices_tenant_id_tenants_id_fk", + "tableFrom": "invoices", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invoices_inspection_id_inspections_id_fk": { + "name": "invoices_inspection_id_inspections_id_fk", + "tableFrom": "invoices", + "tableTo": "inspections", + "columnsFrom": [ + "inspection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invoices_contact_id_contacts_id_fk": { + "name": "invoices_contact_id_contacts_id_fk", + "tableFrom": "invoices", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "marketplace_libraries": { + "name": "marketplace_libraries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "semver": { + "name": "semver", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema": { + "name": "schema", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "changelog": { + "name": "changelog", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "download_count": { + "name": "download_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_featured": { + "name": "is_featured", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jurisdiction": { + "name": "jurisdiction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspection_kind": { + "name": "inspection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delisted_at": { + "name": "delisted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_marketplace_libraries_kind_featured": { + "name": "idx_marketplace_libraries_kind_featured", + "columns": [ + "kind", + "is_featured" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "message_templates": { + "name": "message_templates", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "variables": { + "name": "variables", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_seeded": { + "name": "is_seeded", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + } + }, + "indexes": { + "idx_message_templates_tenant_channel": { + "name": "idx_message_templates_tenant_channel", + "columns": [ + "tenant_id", + "channel" + ], + "isUnique": false + }, + "idx_message_templates_variant": { + "name": "idx_message_templates_variant", + "columns": [ + "tenant_id", + "name", + "channel", + "locale" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messaging_compliance": { + "name": "messaging_compliance", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'own'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_profile_sid": { + "name": "customer_profile_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_profile_status": { + "name": "customer_profile_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "brand_sid": { + "name": "brand_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "brand_status": { + "name": "brand_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "campaign_sid": { + "name": "campaign_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "campaign_status": { + "name": "campaign_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tfv_sid": { + "name": "tfv_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tfv_status": { + "name": "tfv_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "messaging_resource_sid": { + "name": "messaging_resource_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_meta": { + "name": "provider_meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provisioned_number": { + "name": "provisioned_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provisioned_number_sid": { + "name": "provisioned_number_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "has_sender_attached": { + "name": "has_sender_attached", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "compliance_status": { + "name": "compliance_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'not_started'" + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "migration_batches": { + "name": "migration_batches", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "intent": { + "name": "intent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vendor": { + "name": "vendor", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "adapter_name": { + "name": "adapter_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "adapter_version": { + "name": "adapter_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "manifest": { + "name": "manifest", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "conflict_policy": { + "name": "conflict_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'staged'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reverted_at": { + "name": "reverted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "upload_authorized_by": { + "name": "upload_authorized_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "upload_authorized_at": { + "name": "upload_authorized_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "upload_authorization_version": { + "name": "upload_authorization_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "staff_access_authorized_by": { + "name": "staff_access_authorized_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "staff_access_authorized_at": { + "name": "staff_access_authorized_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "staff_access_authorization_version": { + "name": "staff_access_authorization_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_migration_batches_tenant_created": { + "name": "idx_migration_batches_tenant_created", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "idx_migration_batches_expires": { + "name": "idx_migration_batches_expires", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "migration_rows": { + "name": "migration_rows", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "batch_id": { + "name": "batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity": { + "name": "entity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "conflict_with": { + "name": "conflict_with", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_id": { + "name": "created_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prior_state": { + "name": "prior_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_migration_rows_batch_status": { + "name": "idx_migration_rows_batch_status", + "columns": [ + "batch_id", + "status" + ], + "isUnique": false + }, + "idx_migration_rows_tenant": { + "name": "idx_migration_rows_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_preferences": { + "name": "notification_preferences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "class_id": { + "name": "class_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_notification_prefs_unique": { + "name": "idx_notification_prefs_unique", + "columns": [ + "tenant_id", + "subject_kind", + "subject_id", + "class_id", + "channel" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "order_payments": { + "name": "order_payments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recorded_by": { + "name": "recorded_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refunds_id": { + "name": "refunds_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_order_payments_inspection": { + "name": "idx_order_payments_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + }, + "idx_order_payments_invoice": { + "name": "idx_order_payments_invoice", + "columns": [ + "tenant_id", + "invoice_id" + ], + "isUnique": false + }, + "uq_order_payments_provider_ref": { + "name": "uq_order_payments_provider_ref", + "columns": [ + "tenant_id", + "provider", + "provider_ref" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "orphaned_media": { + "name": "orphaned_media", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_orphaned_media_key": { + "name": "idx_orphaned_media_key", + "columns": [ + "tenant_id", + "r2_key" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "processed_webhook_events": { + "name": "processed_webhook_events", + "columns": { + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "received_at": { + "name": "received_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "psq_responses": { + "name": "psq_responses", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "responses": { + "name": "responses", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sent'" + }, + "share_token": { + "name": "share_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sent_at": { + "name": "sent_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "received_at": { + "name": "received_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_psq_inspection": { + "name": "uq_psq_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": true + }, + "idx_psq_share_token": { + "name": "idx_psq_share_token", + "columns": [ + "share_token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "qbo_connections": { + "name": "qbo_connections", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "realm_id": { + "name": "realm_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_enc": { + "name": "access_token_enc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token_enc": { + "name": "refresh_token_enc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_expires_at": { + "name": "token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_sync_enabled": { + "name": "is_sync_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "default_item_id": { + "name": "default_item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'1'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_qbo_connections_realm": { + "name": "uq_qbo_connections_realm", + "columns": [ + "realm_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "qbo_entity_map": { + "name": "qbo_entity_map", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oi_type": { + "name": "oi_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oi_id": { + "name": "oi_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "qbo_type": { + "name": "qbo_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "qbo_id": { + "name": "qbo_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "qbo_sync_token": { + "name": "qbo_sync_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synced_at": { + "name": "synced_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_qbo_entity_map_qbo": { + "name": "idx_qbo_entity_map_qbo", + "columns": [ + "tenant_id", + "qbo_type", + "qbo_id" + ], + "isUnique": true + }, + "idx_qbo_entity_map_oi": { + "name": "idx_qbo_entity_map_oi", + "columns": [ + "tenant_id", + "oi_type", + "oi_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "qbo_sync_errors": { + "name": "qbo_sync_errors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oi_type": { + "name": "oi_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oi_id": { + "name": "oi_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_msg": { + "name": "error_msg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_resolved": { + "name": "is_resolved", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rating_systems": { + "name": "rating_systems", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "levels": { + "name": "levels", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_seed": { + "name": "is_seed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_rating_systems_tenant_slug": { + "name": "idx_rating_systems_tenant_slug", + "columns": [ + "tenant_id", + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rating_systems_tenant_id_tenants_id_fk": { + "name": "rating_systems_tenant_id_tenants_id_fk", + "tableFrom": "rating_systems", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repair_request_items": { + "name": "repair_request_items", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repair_request_id": { + "name": "repair_request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "finding_key": { + "name": "finding_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_label": { + "name": "item_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "comment_snapshot": { + "name": "comment_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "requested_credit_cents": { + "name": "requested_credit_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "defect_title_snapshot": { + "name": "defect_title_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "location_snapshot": { + "name": "location_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_snapshot": { + "name": "category_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trade_snapshot": { + "name": "trade_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repair_action_tag": { + "name": "repair_action_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_repair_request_items_rr": { + "name": "idx_repair_request_items_rr", + "columns": [ + "repair_request_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repair_requests": { + "name": "repair_requests", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_ref": { + "name": "created_by_ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "custom_intro": { + "name": "custom_intro", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_token": { + "name": "share_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_repair_requests_inspection": { + "name": "idx_repair_requests_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + }, + "idx_repair_requests_share_token": { + "name": "idx_repair_requests_share_token", + "columns": [ + "share_token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "report_exports": { + "name": "report_exports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_report_exports_inspection": { + "name": "idx_report_exports_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "report_pdfs": { + "name": "report_pdfs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rendered_at": { + "name": "rendered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_version": { + "name": "source_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ready'" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_report_pdfs_inspection_type": { + "name": "uq_report_pdfs_inspection_type", + "columns": [ + "inspection_id", + "type", + "version_number" + ], + "isUnique": true + }, + "idx_report_pdfs_tenant": { + "name": "idx_report_pdfs_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_report_pdfs_status": { + "name": "idx_report_pdfs_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_report_pdfs_content_hash": { + "name": "idx_report_pdfs_content_hash", + "columns": [ + "inspection_id", + "type", + "content_hash" + ], + "isUnique": false + } + }, + "foreignKeys": { + "report_pdfs_tenant_id_tenants_id_fk": { + "name": "report_pdfs_tenant_id_tenants_id_fk", + "tableFrom": "report_pdfs", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "report_signoff": { + "name": "report_signoff", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "person_id": { + "name": "person_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "license": { + "name": "license", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "qualifications_ref": { + "name": "qualifications_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signed_at": { + "name": "signed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signature_ref": { + "name": "signature_ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_dual_role": { + "name": "is_dual_role", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_report_signoff_inspection": { + "name": "idx_report_signoff_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + }, + "uq_report_signoff_role": { + "name": "uq_report_signoff_role", + "columns": [ + "inspection_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "report_translations": { + "name": "report_translations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "report_id": { + "name": "report_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "english_hash": { + "name": "english_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "translated_hash": { + "name": "translated_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notice_version": { + "name": "notice_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ai_call_id": { + "name": "ai_call_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generated_at": { + "name": "generated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_report_translations_report_locale": { + "name": "uq_report_translations_report_locale", + "columns": [ + "tenant_id", + "report_id", + "locale" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "report_versions": { + "name": "report_versions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_json": { + "name": "snapshot_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prev_hash": { + "name": "prev_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_fingerprint": { + "name": "key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_amendment": { + "name": "is_amendment", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "report_id": { + "name": "report_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_report_versions_report_version": { + "name": "uq_report_versions_report_version", + "columns": [ + "report_id", + "version_number" + ], + "isUnique": true + }, + "idx_report_versions_verify_token": { + "name": "idx_report_versions_verify_token", + "columns": [ + "verification_token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "report_views": { + "name": "report_views", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token_id": { + "name": "access_token_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_viewed_at": { + "name": "first_viewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_viewed_at": { + "name": "last_viewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "view_count": { + "name": "view_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_report_views_scope": { + "name": "idx_report_views_scope", + "columns": [ + "tenant_id", + "inspection_id", + "access_token_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_service_id": { + "name": "inspection_service_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'in_progress'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notified_at": { + "name": "notified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "inspector_narrative": { + "name": "inspector_narrative", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_reports_inspection": { + "name": "idx_reports_inspection", + "columns": [ + "inspection_id" + ], + "isUnique": false + }, + "idx_reports_tenant": { + "name": "idx_reports_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "uq_reports_primary": { + "name": "uq_reports_primary", + "columns": [ + "inspection_id" + ], + "isUnique": true, + "where": "kind = 'primary'" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_inspectors": { + "name": "service_inspectors", + "columns": { + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_service_inspectors_tenant": { + "name": "idx_service_inspectors_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "service_inspectors_service_id_user_id_pk": { + "columns": [ + "service_id", + "user_id" + ], + "name": "service_inspectors_service_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_pay_rules": { + "name": "service_pay_rules", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deduction_cents": { + "name": "deduction_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_service_pay_rules_user": { + "name": "uq_service_pay_rules_user", + "columns": [ + "tenant_id", + "service_id", + "user_id" + ], + "isUnique": true, + "where": "user_id IS NOT NULL" + }, + "uq_service_pay_rules_default": { + "name": "uq_service_pay_rules_default", + "columns": [ + "tenant_id", + "service_id" + ], + "isUnique": true, + "where": "user_id IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "services": { + "name": "services", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agreement_id": { + "name": "agreement_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_event_type_slugs": { + "name": "default_event_type_slugs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deposit_policy": { + "name": "deposit_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_services_tenant": { + "name": "idx_services_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "services_tenant_id_tenants_id_fk": { + "name": "services_tenant_id_tenants_id_fk", + "tableFrom": "services", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "services_template_id_templates_id_fk": { + "name": "services_template_id_templates_id_fk", + "tableFrom": "services", + "tableTo": "templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "services_agreement_id_agreements_id_fk": { + "name": "services_agreement_id_agreements_id_fk", + "tableFrom": "services", + "tableTo": "agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key_enc": { + "name": "private_key_enc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key_iv": { + "name": "private_key_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Ed25519'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retired_at": { + "name": "retired_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_signing_keys_tenant_fingerprint": { + "name": "uq_signing_keys_tenant_fingerprint", + "columns": [ + "tenant_id", + "fingerprint" + ], + "isUnique": true + }, + "uq_signing_keys_tenant_active": { + "name": "uq_signing_keys_tenant_active", + "columns": [ + "tenant_id" + ], + "isUnique": true, + "where": "retired_at IS NULL" + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sms_consent_log": { + "name": "sms_consent_log", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_type": { + "name": "recipient_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "disclosure_version": { + "name": "disclosure_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "captured_via": { + "name": "captured_via", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'contact'" + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "disclosure_content_hash": { + "name": "disclosure_content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_sms_consent_contact": { + "name": "idx_sms_consent_contact", + "columns": [ + "tenant_id", + "contact_id", + "created_at" + ], + "isUnique": false + }, + "idx_sms_consent_subject": { + "name": "idx_sms_consent_subject", + "columns": [ + "tenant_id", + "subject_kind", + "subject_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sms_delivery_status": { + "name": "sms_delivery_status", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_sms_delivery_status_msg": { + "name": "idx_sms_delivery_status_msg", + "columns": [ + "tenant_id", + "provider_message_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sms_disclosure_versions": { + "name": "sms_disclosure_versions", + "columns": { + "version": { + "name": "version", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "statutory_form_productions": { + "name": "statutory_form_productions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "form_id": { + "name": "form_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "produced_by": { + "name": "produced_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "produced_at": { + "name": "produced_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_statutory_productions_form_version": { + "name": "idx_statutory_productions_form_version", + "columns": [ + "form_id", + "version" + ], + "isUnique": false + }, + "idx_statutory_productions_inspection": { + "name": "idx_statutory_productions_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "statutory_form_sightings": { + "name": "statutory_form_sightings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "form_id": { + "name": "form_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observed_hash": { + "name": "observed_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_statutory_form_sightings_seen": { + "name": "uq_statutory_form_sightings_seen", + "columns": [ + "form_id", + "source_url", + "observed_hash" + ], + "isUnique": true + }, + "idx_statutory_form_sightings_form": { + "name": "idx_statutory_form_sightings_form", + "columns": [ + "form_id", + "last_seen_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "statutory_form_versions": { + "name": "statutory_form_versions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "form_id": { + "name": "form_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "effective_from": { + "name": "effective_from", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mandatory_from": { + "name": "mandatory_from", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "effective_until": { + "name": "effective_until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_statutory_form_versions_form_version": { + "name": "uq_statutory_form_versions_form_version", + "columns": [ + "form_id", + "version" + ], + "isUnique": true + }, + "idx_statutory_form_versions_form": { + "name": "idx_statutory_form_versions_form", + "columns": [ + "form_id", + "effective_from" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tags": { + "name": "tags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_seed": { + "name": "is_seed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_tags_tenant_name": { + "name": "idx_tags_tenant_name", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "templates": { + "name": "templates", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "schema": { + "name": "schema", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rating_system_id": { + "name": "rating_system_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commercial_subtype": { + "name": "commercial_subtype", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_featured": { + "name": "is_featured", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_profile_id": { + "name": "default_profile_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retired_at": { + "name": "retired_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_templates_tenant": { + "name": "idx_templates_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_templates_rating_system": { + "name": "idx_templates_rating_system", + "columns": [ + "rating_system_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "templates_tenant_id_tenants_id_fk": { + "name": "templates_tenant_id_tenants_id_fk", + "tableFrom": "templates", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_custom_holidays": { + "name": "tenant_custom_holidays", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_tenant_custom_holidays_tenant_date": { + "name": "uq_tenant_custom_holidays_tenant_date", + "columns": [ + "tenant_id", + "date" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_library_imports": { + "name": "tenant_library_imports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "library_id": { + "name": "library_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imported_semver": { + "name": "imported_semver", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imported_at": { + "name": "imported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "local_entity_id": { + "name": "local_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uninstalled_at": { + "name": "uninstalled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_tenant_library_import": { + "name": "uq_tenant_library_import", + "columns": [ + "tenant_id", + "library_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_marketplace_import_history": { + "name": "tenant_marketplace_import_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "library_id": { + "name": "library_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_version": { + "name": "source_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_version": { + "name": "target_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rows_affected": { + "name": "rows_affected", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_marketplace_history_tenant": { + "name": "idx_marketplace_history_tenant", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "idx_marketplace_history_template": { + "name": "idx_marketplace_history_template", + "columns": [ + "template_id" + ], + "isUnique": false + }, + "idx_marketplace_history_library": { + "name": "idx_marketplace_history_library", + "columns": [ + "library_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_counters": { + "name": "usage_counters", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "period_key": { + "name": "period_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_usage_counters_tenant": { + "name": "idx_usage_counters_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_counters_tenant_id_metric_period_key_pk": { + "columns": [ + "tenant_id", + "metric", + "period_key" + ], + "name": "usage_counters_tenant_id_metric_period_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_configs": { + "name": "tenant_configs", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "support_email": { + "name": "support_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "company_address": { + "name": "company_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_pdf_footer_shown": { + "name": "is_pdf_footer_shown", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "is_pdf_page_numbers_shown": { + "name": "is_pdf_page_numbers_shown", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "is_pdf_license_shown": { + "name": "is_pdf_license_shown", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sender_email": { + "name": "sender_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reply_to": { + "name": "reply_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_mode": { + "name": "email_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'platform'" + }, + "video_mode": { + "name": "video_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'r2'" + }, + "sms_mode": { + "name": "sms_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'platform'" + }, + "sender_display_name": { + "name": "sender_display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "point_of_contact": { + "name": "point_of_contact", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'company'" + }, + "billing_url": { + "name": "billing_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "review_url": { + "name": "review_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "company_phone": { + "name": "company_phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "integration_config": { + "name": "integration_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secrets_enc": { + "name": "secrets_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dek_enc": { + "name": "dek_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ics_token": { + "name": "ics_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "widget_allowed_origins": { + "name": "widget_allowed_origins", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_profile_id": { + "name": "default_profile_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'signature'" + }, + "attention_thresholds": { + "name": "attention_thresholds", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"agreement_unsigned_h\":72,\"invoice_overdue_h\":72,\"report_unpublished_h\":72}'" + }, + "inspection_prefs": { + "name": "inspection_prefs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_repair_list_enabled": { + "name": "is_repair_list_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_customer_repair_export_enabled": { + "name": "is_customer_repair_export_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_unpaid_blocked": { + "name": "is_unpaid_blocked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_unsigned_agreement_blocked": { + "name": "is_unsigned_agreement_blocked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "custom_referral_sources": { + "name": "custom_referral_sources", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dashboard_column_prefs": { + "name": "dashboard_column_prefs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_concierge_review_required": { + "name": "is_concierge_review_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_inspector_choice_allowed": { + "name": "is_inspector_choice_allowed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_pdf_pipeline_enabled": { + "name": "is_pdf_pipeline_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_team_mode_default": { + "name": "is_team_mode_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "require_defect_fields": { + "name": "require_defect_fields", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "agreement_retention_years": { + "name": "agreement_retention_years", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 6 + }, + "reinspection_statuses": { + "name": "reinspection_statuses", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_collab_editing_enabled": { + "name": "is_collab_editing_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sms_byo_provider": { + "name": "sms_byo_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_byo_provider": { + "name": "email_byo_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'resend'" + }, + "is_managed_eligible": { + "name": "is_managed_eligible", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "managed_provider": { + "name": "managed_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'twilio'" + }, + "is_reserve_schedule_enabled": { + "name": "is_reserve_schedule_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "reserve_term_years": { + "name": "reserve_term_years", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 12 + }, + "inflation_rate_bps": { + "name": "inflation_rate_bps", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_timezone": { + "name": "default_timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'UTC'" + }, + "booking_slot_mode": { + "name": "booking_slot_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fixed'" + }, + "booking_slot_interval_min": { + "name": "booking_slot_interval_min", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "holiday_region": { + "name": "holiday_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "holiday_public_policy": { + "name": "holiday_public_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'open'" + }, + "holiday_internal_policy": { + "name": "holiday_internal_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'advisory'" + }, + "default_locale": { + "name": "default_locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en-US'" + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'USD'" + }, + "is_archive_revoking_access": { + "name": "is_archive_revoking_access", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "legal_mode": { + "name": "legal_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'hosted'" + }, + "custom_privacy_url": { + "name": "custom_privacy_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_terms_url": { + "name": "custom_terms_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "privacy_body": { + "name": "privacy_body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terms_body": { + "name": "terms_body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date_format": { + "name": "date_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'us'" + }, + "time_format": { + "name": "time_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'12h'" + }, + "booking_conflict_policy": { + "name": "booking_conflict_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'advisory'" + }, + "cancellation_policy": { + "name": "cancellation_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancellation_clause_agreement_id": { + "name": "cancellation_clause_agreement_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancellation_clause_version": { + "name": "cancellation_clause_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancellation_clause_attested_at": { + "name": "cancellation_clause_attested_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deposit_policy": { + "name": "deposit_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "booking_routing_strategy": { + "name": "booking_routing_strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'first_available'" + }, + "booking_min_lead_hours": { + "name": "booking_min_lead_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "booking_same_day_cutoff_time": { + "name": "booking_same_day_cutoff_time", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "company_lat": { + "name": "company_lat", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "company_lng": { + "name": "company_lng", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "company_geocoded_at": { + "name": "company_geocoded_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repair_quick_phrases": { + "name": "repair_quick_phrases", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "legal_name": { + "name": "legal_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_seq": { + "name": "invoice_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1000 + }, + "report_pdf_retention_years": { + "name": "report_pdf_retention_years", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 7 + }, + "is_report_view_counting_enabled": { + "name": "is_report_view_counting_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "tenant_configs_tenant_id_tenants_id_fk": { + "name": "tenant_configs_tenant_id_tenants_id_fk", + "tableFrom": "tenant_configs", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'free'" + }, + "stripe_connect_account_id": { + "name": "stripe_connect_account_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "max_users": { + "name": "max_users", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 5 + }, + "deployment_mode": { + "name": "deployment_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'shared'" + }, + "applied_cmd_seq": { + "name": "applied_cmd_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "applied_cred_seq": { + "name": "applied_cred_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_version": { + "name": "content_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "tenants_slug_unique": { + "name": "tenants_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discovery_objections": { + "name": "discovery_objections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email_hash": { + "name": "email_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "proved_by": { + "name": "proved_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "withdrawn_at": { + "name": "withdrawn_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_discovery_objections_email_hash": { + "name": "uq_discovery_objections_email_hash", + "columns": [ + "email_hash" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_ai_attestations": { + "name": "tenant_ai_attestations", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_owner": { + "name": "account_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "terms_version": { + "name": "terms_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attested_at": { + "name": "attested_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "policy_version": { + "name": "policy_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_ai_configs": { + "name": "tenant_ai_configs", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_courtesy_translation_enabled": { + "name": "is_courtesy_translation_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_templates": { + "name": "email_templates", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "blocks": { + "name": "blocks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "email_templates_tenant_id_tenants_id_fk": { + "name": "email_templates_tenant_id_tenants_id_fk", + "tableFrom": "email_templates", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "email_templates_tenant_id_trigger_pk": { + "columns": [ + "tenant_id", + "trigger" + ], + "name": "email_templates_tenant_id_trigger_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_slug_history": { + "name": "tenant_slug_history", + "columns": { + "old_slug": { + "name": "old_slug", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "changed_at": { + "name": "changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retired_until": { + "name": "retired_until", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_tenant_slug_history_tenant": { + "name": "idx_tenant_slug_history_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_invites": { + "name": "tenant_invites", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'inspector'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_overrides": { + "name": "permission_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_invites_tenant": { + "name": "idx_invites_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "uq_tenant_invites_pending_email": { + "name": "uq_tenant_invites_pending_email", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true, + "where": "status = 'pending'" + } + }, + "foreignKeys": { + "tenant_invites_tenant_id_tenants_id_fk": { + "name": "tenant_invites_tenant_id_tenants_id_fk", + "tableFrom": "tenant_invites", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "photo_url": { + "name": "photo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_signature_base64": { + "name": "default_signature_base64", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_signature_enabled": { + "name": "is_signature_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manager'" + }, + "onboarding_state": { + "name": "onboarding_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_totp_enabled": { + "name": "is_totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_recovery_codes": { + "name": "totp_recovery_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_verified_at": { + "name": "totp_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terms_accepted": { + "name": "terms_accepted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_overrides": { + "name": "permission_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date_format": { + "name": "date_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_format": { + "name": "time_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "service_origin_address": { + "name": "service_origin_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "service_origin_lat": { + "name": "service_origin_lat", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "service_origin_lng": { + "name": "service_origin_lng", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "statutory_license_type": { + "name": "statutory_license_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "statutory_qualification": { + "name": "statutory_qualification", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_users_deleted_at": { + "name": "idx_users_deleted_at", + "columns": [ + "deleted_at" + ], + "isUnique": false + }, + "uq_users_tenant_email": { + "name": "uq_users_tenant_email", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true, + "where": "deleted_at IS NULL" + }, + "idx_users_slug_per_tenant": { + "name": "idx_users_slug_per_tenant", + "columns": [ + "tenant_id", + "slug" + ], + "isUnique": true + }, + "idx_users_email": { + "name": "idx_users_email", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspector_slug": { + "name": "inspector_slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_kind": { + "name": "actor_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'tenant_user'" + }, + "platform_actor_id": { + "name": "platform_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_audit_tenant_created": { + "name": "idx_audit_tenant_created", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "idx_audit_entity": { + "name": "idx_audit_entity", + "columns": [ + "entity_type", + "entity_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_tenant_id_tenants_id_fk": { + "name": "audit_logs_tenant_id_tenants_id_fk", + "tableFrom": "audit_logs", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration_test_results": { + "name": "integration_test_results", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_ok": { + "name": "is_ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tested_by_user_id": { + "name": "tested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tested_at": { + "name": "tested_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step": { + "name": "step", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_integration_test_tenant_target": { + "name": "idx_integration_test_tenant_target", + "columns": [ + "tenant_id", + "target", + "tested_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notifications": { + "name": "notifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read_at": { + "name": "read_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_notifications_tenant_user_created": { + "name": "idx_notifications_tenant_user_created", + "columns": [ + "tenant_id", + "user_id", + "created_at" + ], + "isUnique": false + }, + "idx_notifications_tenant_user_unread": { + "name": "idx_notifications_tenant_user_unread", + "columns": [ + "tenant_id", + "user_id", + "read_at" + ], + "isUnique": false + }, + "idx_notifications_tenant_contact_created": { + "name": "idx_notifications_tenant_contact_created", + "columns": [ + "tenant_id", + "contact_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "notifications_tenant_id_tenants_id_fk": { + "name": "notifications_tenant_id_tenants_id_fk", + "tableFrom": "notifications", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "parked_cmd_events": { + "name": "parked_cmd_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "envelope": { + "name": "envelope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "received_at": { + "name": "received_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_parked_cmd_events_received_at": { + "name": "idx_parked_cmd_events_received_at", + "columns": [ + "received_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "processed_cmd_events": { + "name": "processed_cmd_events", + "columns": { + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "cmd_type": { + "name": "cmd_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "processed_at": { + "name": "processed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "slug_reservations": { + "name": "slug_reservations", + "columns": { + "slug": { + "name": "slug", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_outbox": { + "name": "sync_outbox", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_tried_at": { + "name": "last_tried_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_sync_outbox_status_created": { + "name": "idx_sync_outbox_status_created", + "columns": [ + "status", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_destruction_records": { + "name": "tenant_destruction_records", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_slug": { + "name": "tenant_slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rows_deleted": { + "name": "rows_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "r2_objects": { + "name": "r2_objects", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "r2_bytes": { + "name": "r2_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "kv_keys": { + "name": "kv_keys", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "destroyed_at": { + "name": "destroyed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'completed'" + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "record_version": { + "name": "record_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "stores_measured": { + "name": "stores_measured", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "store_results": { + "name": "store_results", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "incomplete_notified_at": { + "name": "incomplete_notified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_destruction_tenant": { + "name": "idx_destruction_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_destruction_destroyed_at": { + "name": "idx_destruction_destroyed_at", + "columns": [ + "destroyed_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_legal_versions": { + "name": "tenant_legal_versions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "doc": { + "name": "doc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body_snapshot": { + "name": "body_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_material": { + "name": "is_material", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_by_user_id": { + "name": "published_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_tenant_legal_versions_doc_version": { + "name": "idx_tenant_legal_versions_doc_version", + "columns": [ + "tenant_id", + "doc", + "version" + ], + "isUnique": true + }, + "idx_tenant_legal_versions_latest": { + "name": "idx_tenant_legal_versions_latest", + "columns": [ + "tenant_id", + "doc", + "published_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account_acceptances": { + "name": "account_acceptances", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_identity_ref": { + "name": "actor_identity_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "doc": { + "name": "doc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authority_basis": { + "name": "authority_basis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_account_acceptances_user_doc_version": { + "name": "uq_account_acceptances_user_doc_version", + "columns": [ + "user_id", + "doc", + "version" + ], + "isUnique": true + }, + "idx_account_acceptances_tenant": { + "name": "idx_account_acceptances_tenant", + "columns": [ + "tenant_id", + "accepted_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "legal_holds": { + "name": "legal_holds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "matter": { + "name": "matter", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "placed_by": { + "name": "placed_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "placed_at": { + "name": "placed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "released_at": { + "name": "released_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "released_by": { + "name": "released_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "release_reason": { + "name": "release_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_legal_holds_tenant_active": { + "name": "idx_legal_holds_tenant_active", + "columns": [ + "tenant_id", + "released_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "client_uploads": { + "name": "client_uploads", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uploaded_by_kind": { + "name": "uploaded_by_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uploaded_by_ref": { + "name": "uploaded_by_ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uploaded_by_name": { + "name": "uploaded_by_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_client_uploads_inspection": { + "name": "idx_client_uploads_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "statutory_form_entries": { + "name": "statutory_form_entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "form_id": { + "name": "form_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "values": { + "name": "values", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_statutory_form_entries_subject": { + "name": "uq_statutory_form_entries_subject", + "columns": [ + "tenant_id", + "inspection_id", + "form_id" + ], + "isUnique": true + }, + "idx_statutory_form_entries_inspection": { + "name": "idx_statutory_form_entries_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "statutory_inspection_details": { + "name": "statutory_inspection_details", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspector_signature_date": { + "name": "inspector_signature_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "employee_printed_name": { + "name": "employee_printed_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_name": { + "name": "owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_email": { + "name": "owner_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_mailing_address": { + "name": "owner_mailing_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_home_phone": { + "name": "owner_home_phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_work_phone": { + "name": "owner_work_phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_cell_phone": { + "name": "owner_cell_phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_statutory_inspection_details_subject": { + "name": "uq_statutory_inspection_details_subject", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "uq_discount_codes_code_tenant": { + "columns": { + "upper(code)": { + "isExpression": true + } + } + } + } + } +} \ No newline at end of file diff --git a/migrations/meta/0002_snapshot.json b/migrations/meta/0002_snapshot.json new file mode 100644 index 000000000..68dd1a7de --- /dev/null +++ b/migrations/meta/0002_snapshot.json @@ -0,0 +1,12457 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "1be93fd4-f203-45db-b32f-015bb94fe122", + "prevId": "174ac276-45d1-4f0b-85a8-90b5d4a62a47", + "tables": { + "agent_terms_acceptances": { + "name": "agent_terms_acceptances", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "doc": { + "name": "doc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_agent_terms_acceptances_user": { + "name": "idx_agent_terms_acceptances_user", + "columns": [ + "user_id", + "accepted_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agreement_requests": { + "name": "agreement_requests", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agreement_id": { + "name": "agreement_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_email": { + "name": "client_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "signed_at": { + "name": "signed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "viewed_at": { + "name": "viewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sent_at": { + "name": "sent_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspector_signature_base64": { + "name": "inspector_signature_base64", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspector_signed_at": { + "name": "inspector_signed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspector_user_id": { + "name": "inspector_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_snapshot": { + "name": "content_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_policy": { + "name": "completion_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'all'" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signer_legal_name": { + "name": "signer_legal_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signer_company_name": { + "name": "signer_company_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_agreement_requests_verify_token": { + "name": "idx_agreement_requests_verify_token", + "columns": [ + "verification_token" + ], + "isUnique": true + }, + "idx_agreement_requests_tenant": { + "name": "idx_agreement_requests_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_agreement_requests_inspection": { + "name": "idx_agreement_requests_inspection", + "columns": [ + "inspection_id" + ], + "isUnique": false + }, + "idx_agreement_requests_token_hash": { + "name": "idx_agreement_requests_token_hash", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "agreement_requests_tenant_id_tenants_id_fk": { + "name": "agreement_requests_tenant_id_tenants_id_fk", + "tableFrom": "agreement_requests", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agreement_requests_inspection_id_inspections_id_fk": { + "name": "agreement_requests_inspection_id_inspections_id_fk", + "tableFrom": "agreement_requests", + "tableTo": "inspections", + "columnsFrom": [ + "inspection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agreement_requests_agreement_id_agreements_id_fk": { + "name": "agreement_requests_agreement_id_agreements_id_fk", + "tableFrom": "agreement_requests", + "tableTo": "agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agreement_requests_inspector_user_id_users_id_fk": { + "name": "agreement_requests_inspector_user_id_users_id_fk", + "tableFrom": "agreement_requests", + "tableTo": "users", + "columnsFrom": [ + "inspector_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agreement_signers": { + "name": "agreement_signers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client'" + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_enc": { + "name": "token_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "signature_base64": { + "name": "signature_base64", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signed_at": { + "name": "signed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "viewed_at": { + "name": "viewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "on_behalf_of": { + "name": "on_behalf_of", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "on_behalf_disclaimer": { + "name": "on_behalf_disclaimer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_reminded_at": { + "name": "last_reminded_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language_disclosure_version": { + "name": "language_disclosure_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attribution_basis": { + "name": "attribution_basis", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attribution_source": { + "name": "attribution_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributed_at": { + "name": "attributed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_agreement_signers_tenant_request": { + "name": "idx_agreement_signers_tenant_request", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": false + }, + "idx_agreement_signers_request_email": { + "name": "idx_agreement_signers_request_email", + "columns": [ + "request_id", + "email" + ], + "isUnique": true + }, + "idx_agreement_signers_token_hash": { + "name": "idx_agreement_signers_token_hash", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agreements": { + "name": "agreements", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_agreements_tenant": { + "name": "idx_agreements_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "agreements_tenant_id_tenants_id_fk": { + "name": "agreements_tenant_id_tenants_id_fk", + "tableFrom": "agreements", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_call_provenance": { + "name": "ai_call_provenance", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt_version": { + "name": "prompt_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_ai_call_provenance_tenant_created": { + "name": "idx_ai_call_provenance_tenant_created", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_content_reviews": { + "name": "ai_content_reviews", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "artifact_type": { + "name": "artifact_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ai_call_id": { + "name": "ai_call_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_ai_content_reviews_ai_call": { + "name": "idx_ai_content_reviews_ai_call", + "columns": [ + "ai_call_id" + ], + "isUnique": false + }, + "uq_ai_content_reviews_person_call": { + "name": "uq_ai_content_reviews_person_call", + "columns": [ + "tenant_id", + "artifact_type", + "artifact_id", + "ai_call_id", + "reviewed_by" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_logs": { + "name": "automation_logs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient": { + "name": "recipient", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_role_key": { + "name": "recipient_role_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'email'" + }, + "send_at": { + "name": "send_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_contact_id": { + "name": "recipient_contact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notice_id": { + "name": "notice_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sender_identity": { + "name": "sender_identity", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "human_verified_at": { + "name": "human_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_logs_pending": { + "name": "idx_automation_logs_pending", + "columns": [ + "tenant_id", + "status", + "send_at" + ], + "isUnique": false + }, + "idx_automation_logs_insp": { + "name": "idx_automation_logs_insp", + "columns": [ + "inspection_id" + ], + "isUnique": false + }, + "uq_automation_logs_event": { + "name": "uq_automation_logs_event", + "columns": [ + "automation_id", + "inspection_id", + "event_id", + "channel", + "recipient" + ], + "isUnique": true, + "where": "event_id IS NOT NULL" + } + }, + "foreignKeys": { + "automation_logs_tenant_id_tenants_id_fk": { + "name": "automation_logs_tenant_id_tenants_id_fk", + "tableFrom": "automation_logs", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automations": { + "name": "automations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_kind": { + "name": "recipient_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_role_profile_id": { + "name": "recipient_role_profile_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delay_minutes": { + "name": "delay_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "email_template_id": { + "name": "email_template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "conditions": { + "name": "conditions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channels": { + "name": "channels", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[\"email\"]'" + }, + "sms_template_id": { + "name": "sms_template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "in_app_template_id": { + "name": "in_app_template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automations_tenant": { + "name": "idx_automations_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automations_tenant_id_tenants_id_fk": { + "name": "automations_tenant_id_tenants_id_fk", + "tableFrom": "automations", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "availability": { + "name": "availability", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspector_id": { + "name": "inspector_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "day_of_week": { + "name": "day_of_week", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_time": { + "name": "start_time", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_time": { + "name": "end_time", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_availability_window_unique": { + "name": "idx_availability_window_unique", + "columns": [ + "inspector_id", + "day_of_week", + "start_time" + ], + "isUnique": true + } + }, + "foreignKeys": { + "availability_tenant_id_tenants_id_fk": { + "name": "availability_tenant_id_tenants_id_fk", + "tableFrom": "availability", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "availability_inspector_id_users_id_fk": { + "name": "availability_inspector_id_users_id_fk", + "tableFrom": "availability", + "tableTo": "users", + "columnsFrom": [ + "inspector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "availability_overrides": { + "name": "availability_overrides", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspector_id": { + "name": "inspector_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_available": { + "name": "is_available", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "start_time": { + "name": "start_time", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_time": { + "name": "end_time", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "transparency": { + "name": "transparency", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_avail_overrides_block_unique": { + "name": "idx_avail_overrides_block_unique", + "columns": [ + "inspector_id", + "date" + ], + "isUnique": true, + "where": "is_available = 0 AND source IS NULL" + }, + "uq_avail_overrides_external": { + "name": "uq_avail_overrides_external", + "columns": [ + "inspector_id", + "source", + "external_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "availability_overrides_tenant_id_tenants_id_fk": { + "name": "availability_overrides_tenant_id_tenants_id_fk", + "tableFrom": "availability_overrides", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "availability_overrides_inspector_id_users_id_fk": { + "name": "availability_overrides_inspector_id_users_id_fk", + "tableFrom": "availability_overrides", + "tableTo": "users", + "columnsFrom": [ + "inspector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "calendar_blocks": { + "name": "calendar_blocks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_time": { + "name": "start_time", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_time": { + "name": "end_time", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_all_day": { + "name": "is_all_day", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_calendar_blocks_tenant_user_date": { + "name": "idx_calendar_blocks_tenant_user_date", + "columns": [ + "tenant_id", + "user_id", + "date" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "calendar_connection_read_calendars": { + "name": "calendar_connection_read_calendars", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_calendar_id": { + "name": "external_calendar_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_role": { + "name": "access_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_conn_read_cal": { + "name": "uq_conn_read_cal", + "columns": [ + "connection_id", + "external_calendar_id" + ], + "isUnique": true + }, + "idx_conn_read_cal_tenant": { + "name": "idx_conn_read_cal_tenant", + "columns": [ + "tenant_id", + "connection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "calendar_connections": { + "name": "calendar_connections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentials_enc": { + "name": "credentials_enc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentials_dek_enc": { + "name": "credentials_dek_enc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "calendar_id": { + "name": "calendar_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connected_at": { + "name": "connected_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_calendar_connections_user_provider": { + "name": "uq_calendar_connections_user_provider", + "columns": [ + "user_id", + "provider" + ], + "isUnique": true + }, + "idx_calendar_connections_tenant_user": { + "name": "idx_calendar_connections_tenant_user", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "calendar_external_links": { + "name": "calendar_external_links", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_calendar_external_links_entity": { + "name": "uq_calendar_external_links_entity", + "columns": [ + "tenant_id", + "provider", + "entity_type", + "entity_id" + ], + "isUnique": true + }, + "idx_calendar_external_links_user": { + "name": "idx_calendar_external_links_user", + "columns": [ + "tenant_id", + "user_id", + "provider" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "comment_usage": { + "name": "comment_usage", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "comment_id": { + "name": "comment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_count": { + "name": "use_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_comment_usage_user_last_used": { + "name": "idx_comment_usage_user_last_used", + "columns": [ + "tenant_id", + "user_id", + "last_used_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "comment_usage_comment_id_comments_id_fk": { + "name": "comment_usage_comment_id_comments_id_fk", + "tableFrom": "comment_usage", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "comment_usage_tenant_id_user_id_comment_id_pk": { + "columns": [ + "tenant_id", + "user_id", + "comment_id" + ], + "name": "comment_usage_tenant_id_user_id_comment_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "comments": { + "name": "comments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "section": { + "name": "section", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "library_id": { + "name": "library_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "section_ids": { + "name": "section_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "item_labels": { + "name": "item_labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trigger_code": { + "name": "trigger_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "search_keywords": { + "name": "search_keywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "item_label": { + "name": "item_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repair_summary": { + "name": "repair_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recommended_contractor_type_id": { + "name": "recommended_contractor_type_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "edited_at": { + "name": "edited_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "import_hash": { + "name": "import_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_comments_tenant": { + "name": "idx_comments_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_comments_library_id": { + "name": "idx_comments_library_id", + "columns": [ + "library_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "comments_tenant_id_tenants_id_fk": { + "name": "comments_tenant_id_tenants_id_fk", + "tableFrom": "comments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "concierge_confirm_tokens": { + "name": "concierge_confirm_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_email": { + "name": "client_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_concierge_tokens_expiry": { + "name": "idx_concierge_tokens_expiry", + "columns": [ + "expires_at" + ], + "isUnique": false + }, + "idx_concierge_confirm_token_hash": { + "name": "idx_concierge_confirm_token_hash", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "contact_role_profiles": { + "name": "contact_role_profiles", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_template_id": { + "name": "email_template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sms_template_id": { + "name": "sms_template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability_overrides": { + "name": "capability_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_crp_tenant": { + "name": "idx_crp_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "uq_crp_tenant_key": { + "name": "uq_crp_tenant_key", + "columns": [ + "tenant_id", + "key" + ], + "isUnique": true, + "where": "is_active = 1" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "contacts": { + "name": "contacts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agency": { + "name": "agency", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_user_id": { + "name": "agent_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_linked_at": { + "name": "agent_linked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_revoked_at": { + "name": "agent_revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_contacts_type": { + "name": "idx_contacts_type", + "columns": [ + "tenant_id", + "type" + ], + "isUnique": false + }, + "uq_contacts_tenant_email": { + "name": "uq_contacts_tenant_email", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true, + "where": "email IS NOT NULL AND archived_at IS NULL" + }, + "uq_contacts_tenant_agent_user": { + "name": "uq_contacts_tenant_agent_user", + "columns": [ + "tenant_id", + "agent_user_id" + ], + "isUnique": true, + "where": "agent_user_id IS NOT NULL AND archived_at IS NULL" + }, + "idx_contacts_agent_user": { + "name": "idx_contacts_agent_user", + "columns": [ + "agent_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "contacts_tenant_id_tenants_id_fk": { + "name": "contacts_tenant_id_tenants_id_fk", + "tableFrom": "contacts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "contractor_types": { + "name": "contractor_types", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trade_slug": { + "name": "trade_slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_contractor_types_tenant": { + "name": "idx_contractor_types_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "uq_contractor_types_tenant_trade": { + "name": "uq_contractor_types_tenant_trade", + "columns": [ + "tenant_id", + "trade_slug" + ], + "isUnique": true, + "where": "trade_slug IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cost_items": { + "name": "cost_items", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "building_id": { + "name": "building_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "instance_index": { + "name": "instance_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit_id": { + "name": "unit_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "finding_key": { + "name": "finding_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "system": { + "name": "system", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "component": { + "name": "component", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cost_method": { + "name": "cost_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uom": { + "name": "uom", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit_cost_cents": { + "name": "unit_cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lump_sum_cents": { + "name": "lump_sum_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "eul": { + "name": "eul", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "eff_age": { + "name": "eff_age", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rul": { + "name": "rul", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "suggested_remedy": { + "name": "suggested_remedy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_ref": { + "name": "section_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "photo_ref": { + "name": "photo_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_cost_items_tenant_inspection": { + "name": "idx_cost_items_tenant_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + }, + "idx_cost_items_finding_key": { + "name": "idx_cost_items_finding_key", + "columns": [ + "finding_key" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "defect_categories": { + "name": "defect_categories", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#6b7280'" + }, + "is_summary_driver": { + "name": "is_summary_driver", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_seed": { + "name": "is_seed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_defect_categories_tenant": { + "name": "idx_defect_categories_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deployment_legal_versions": { + "name": "deployment_legal_versions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "doc": { + "name": "doc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body_snapshot": { + "name": "body_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_deployment_legal_versions_doc_hash": { + "name": "uq_deployment_legal_versions_doc_hash", + "columns": [ + "doc", + "content_hash" + ], + "isUnique": true + }, + "idx_deployment_legal_versions_latest": { + "name": "idx_deployment_legal_versions_latest", + "columns": [ + "doc", + "published_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discount_codes": { + "name": "discount_codes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uses_count": { + "name": "uses_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_discount_codes_tenant": { + "name": "idx_discount_codes_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "uq_discount_codes_code_tenant": { + "name": "uq_discount_codes_code_tenant", + "columns": [ + "upper(code)", + "tenant_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "discount_codes_tenant_id_tenants_id_fk": { + "name": "discount_codes_tenant_id_tenants_id_fk", + "tableFrom": "discount_codes", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "document_review_items": { + "name": "document_review_items", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "document_key": { + "name": "document_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_requested": { + "name": "is_requested", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_received": { + "name": "is_received", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_reviewed": { + "name": "is_reviewed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_na": { + "name": "is_na", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_doc_review_inspection": { + "name": "idx_doc_review_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + }, + "uq_doc_review_item": { + "name": "uq_doc_review_item", + "columns": [ + "inspection_id", + "document_key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_suppressions": { + "name": "email_suppressions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_provider": { + "name": "source_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_event_id": { + "name": "provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_email_suppressions_email": { + "name": "idx_email_suppressions_email", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "erasure_log": { + "name": "erasure_log", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_email": { + "name": "subject_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identity_basis": { + "name": "identity_basis", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "decisions_json": { + "name": "decisions_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retained_count": { + "name": "retained_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "anonymized_count": { + "name": "anonymized_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "deleted_count": { + "name": "deleted_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "response_note": { + "name": "response_note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_erasure_log_tenant": { + "name": "idx_erasure_log_tenant", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "esign_audit_logs": { + "name": "esign_audit_logs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prev_hash": { + "name": "prev_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_fingerprint": { + "name": "key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_esign_audit_logs_request": { + "name": "idx_esign_audit_logs_request", + "columns": [ + "tenant_id", + "request_id", + "created_at" + ], + "isUnique": false + }, + "idx_esign_audit_logs_event_dedup": { + "name": "idx_esign_audit_logs_event_dedup", + "columns": [ + "tenant_id", + "request_id", + "event" + ], + "isUnique": true, + "where": "event NOT LIKE 'signer.%'" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "event_types": { + "name": "event_types", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_duration_min": { + "name": "default_duration_min", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "default_price_cents": { + "name": "default_price_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#6366f1'" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "follow_up_delay_hours": { + "name": "follow_up_delay_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 72 + } + }, + "indexes": { + "uq_event_types_tenant_slug": { + "name": "uq_event_types_tenant_slug", + "columns": [ + "tenant_id", + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "event_types_tenant_id_tenants_id_fk": { + "name": "event_types_tenant_id_tenants_id_fk", + "tableFrom": "event_types", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "idempotency_keys": { + "name": "idempotency_keys", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'in_flight'" + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_idempotency_expires": { + "name": "idx_idempotency_expires", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "idempotency_keys_tenant_id_key_pk": { + "columns": [ + "tenant_id", + "key" + ], + "name": "idempotency_keys_tenant_id_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_access_tokens": { + "name": "inspection_access_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_enc": { + "name": "token_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "view_tracking_objected_at": { + "name": "view_tracking_objected_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_iat_inspection": { + "name": "idx_iat_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + }, + "idx_iat_recipient": { + "name": "idx_iat_recipient", + "columns": [ + "inspection_id", + "recipient_email" + ], + "isUnique": true + }, + "idx_iat_token_hash": { + "name": "idx_iat_token_hash", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "inspection_access_tokens_tenant_id_tenants_id_fk": { + "name": "inspection_access_tokens_tenant_id_tenants_id_fk", + "tableFrom": "inspection_access_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_events": { + "name": "inspection_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspector_id": { + "name": "inspector_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration_min": { + "name": "duration_min", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'scheduled'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "results_received_at": { + "name": "results_received_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_inspection_events_scheduled": { + "name": "idx_inspection_events_scheduled", + "columns": [ + "tenant_id", + "scheduled_at" + ], + "isUnique": false + }, + "idx_inspection_events_inspection": { + "name": "idx_inspection_events_inspection", + "columns": [ + "inspection_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "inspection_events_tenant_id_tenants_id_fk": { + "name": "inspection_events_tenant_id_tenants_id_fk", + "tableFrom": "inspection_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspection_events_inspection_id_inspections_id_fk": { + "name": "inspection_events_inspection_id_inspections_id_fk", + "tableFrom": "inspection_events", + "tableTo": "inspections", + "columnsFrom": [ + "inspection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inspection_events_event_type_id_event_types_id_fk": { + "name": "inspection_events_event_type_id_event_types_id_fk", + "tableFrom": "inspection_events", + "tableTo": "event_types", + "columnsFrom": [ + "event_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspection_events_inspector_id_users_id_fk": { + "name": "inspection_events_inspector_id_users_id_fk", + "tableFrom": "inspection_events", + "tableTo": "users", + "columnsFrom": [ + "inspector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_inspectors": { + "name": "inspection_inspectors", + "columns": { + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'lead'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_insp_inspectors_tenant_user": { + "name": "idx_insp_inspectors_tenant_user", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "idx_insp_inspectors_user": { + "name": "idx_insp_inspectors_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "inspection_inspectors_inspection_id_user_id_pk": { + "columns": [ + "inspection_id", + "user_id" + ], + "name": "inspection_inspectors_inspection_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_item_tag_links": { + "name": "inspection_item_tag_links", + "columns": { + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_tag_links_tenant": { + "name": "idx_tag_links_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_tag_links_tag": { + "name": "idx_tag_links_tag", + "columns": [ + "tag_id" + ], + "isUnique": false + }, + "idx_tag_links_inspection_item": { + "name": "idx_tag_links_inspection_item", + "columns": [ + "inspection_id", + "item_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "inspection_item_tag_links_inspection_id_item_id_tag_id_pk": { + "columns": [ + "inspection_id", + "item_id", + "tag_id" + ], + "name": "inspection_item_tag_links_inspection_id_item_id_tag_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_media_pool": { + "name": "inspection_media_pool", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exif_data": { + "name": "exif_data", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "annotations": { + "name": "annotations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "caption": { + "name": "caption", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'photo'" + }, + "stream_uid": { + "name": "stream_uid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "poster_pct": { + "name": "poster_pct", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_sec": { + "name": "duration_sec", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'stream'" + }, + "poster_key": { + "name": "poster_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_media_pool_tenant": { + "name": "idx_media_pool_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_media_pool_inspection": { + "name": "idx_media_pool_inspection", + "columns": [ + "inspection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_messages": { + "name": "inspection_messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "from_role": { + "name": "from_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attachments": { + "name": "attachments", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read_at": { + "name": "read_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from_user_id": { + "name": "from_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_msg_inspection": { + "name": "idx_msg_inspection", + "columns": [ + "inspection_id", + "created_at" + ], + "isUnique": false + }, + "idx_msg_contact": { + "name": "idx_msg_contact", + "columns": [ + "tenant_id", + "contact_id", + "created_at" + ], + "isUnique": false + }, + "idx_msg_unread": { + "name": "idx_msg_unread", + "columns": [ + "tenant_id", + "contact_id", + "from_role" + ], + "isUnique": false, + "where": "\"inspection_messages\".\"read_at\" IS NULL" + } + }, + "foreignKeys": { + "inspection_messages_tenant_id_tenants_id_fk": { + "name": "inspection_messages_tenant_id_tenants_id_fk", + "tableFrom": "inspection_messages", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspection_messages_inspection_id_inspections_id_fk": { + "name": "inspection_messages_inspection_id_inspections_id_fk", + "tableFrom": "inspection_messages", + "tableTo": "inspections", + "columnsFrom": [ + "inspection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_people": { + "name": "inspection_people", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_profile_id": { + "name": "role_profile_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_ip_tenant": { + "name": "idx_ip_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "uq_ip_insp_contact_role": { + "name": "uq_ip_insp_contact_role", + "columns": [ + "inspection_id", + "contact_id", + "role_profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_requests": { + "name": "inspection_requests", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_email": { + "name": "client_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_phone": { + "name": "client_phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "property_address": { + "name": "property_address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "property_city": { + "name": "property_city", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "property_state": { + "name": "property_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "property_zip": { + "name": "property_zip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_inspection_requests_tenant": { + "name": "idx_inspection_requests_tenant", + "columns": [ + "tenant_id", + "status", + "scheduled_at" + ], + "isUnique": false + }, + "idx_inspection_requests_email": { + "name": "idx_inspection_requests_email", + "columns": [ + "tenant_id", + "client_email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "inspection_requests_tenant_id_tenants_id_fk": { + "name": "inspection_requests_tenant_id_tenants_id_fk", + "tableFrom": "inspection_requests", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_results": { + "name": "inspection_results", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ydoc_state": { + "name": "ydoc_state", + "type": "blob", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rating_system_id": { + "name": "rating_system_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rating_system_snapshot": { + "name": "rating_system_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_id": { + "name": "report_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_results_tenant": { + "name": "idx_results_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_results_inspection": { + "name": "idx_results_inspection", + "columns": [ + "inspection_id" + ], + "isUnique": false + }, + "uq_results_report": { + "name": "uq_results_report", + "columns": [ + "report_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "inspection_results_tenant_id_tenants_id_fk": { + "name": "inspection_results_tenant_id_tenants_id_fk", + "tableFrom": "inspection_results", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspection_results_inspection_id_inspections_id_fk": { + "name": "inspection_results_inspection_id_inspections_id_fk", + "tableFrom": "inspection_results", + "tableTo": "inspections", + "columnsFrom": [ + "inspection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_service_pay_splits": { + "name": "inspection_service_pay_splits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_service_id": { + "name": "inspection_service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "locked_at": { + "name": "locked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "corrects_split_id": { + "name": "corrects_split_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_pay_split_line_user": { + "name": "uq_pay_split_line_user", + "columns": [ + "tenant_id", + "inspection_service_id", + "user_id" + ], + "isUnique": true, + "where": "corrects_split_id IS NULL" + }, + "idx_pay_split_user": { + "name": "idx_pay_split_user", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "idx_pay_split_line": { + "name": "idx_pay_split_line", + "columns": [ + "tenant_id", + "inspection_service_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_services": { + "name": "inspection_services", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_override_cents": { + "name": "price_override_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_snapshot": { + "name": "name_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "price_snapshot_cents": { + "name": "price_snapshot_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_insp_services_tenant": { + "name": "idx_insp_services_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_insp_services_insp": { + "name": "idx_insp_services_insp", + "columns": [ + "inspection_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "inspection_services_tenant_id_tenants_id_fk": { + "name": "inspection_services_tenant_id_tenants_id_fk", + "tableFrom": "inspection_services", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspection_services_inspection_id_inspections_id_fk": { + "name": "inspection_services_inspection_id_inspections_id_fk", + "tableFrom": "inspection_services", + "tableTo": "inspections", + "columnsFrom": [ + "inspection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inspection_services_service_id_services_id_fk": { + "name": "inspection_services_service_id_services_id_fk", + "tableFrom": "inspection_services", + "tableTo": "services", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_types": { + "name": "inspection_types", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "based_on": { + "name": "based_on", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_inspection_types_tenant_name": { + "name": "idx_inspection_types_tenant_name", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspection_units": { + "name": "inspection_units", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_unit_id": { + "name": "parent_unit_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unit'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "attrs": { + "name": "attrs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_inspection_units_tenant_inspection": { + "name": "idx_inspection_units_tenant_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + }, + "idx_inspection_units_parent": { + "name": "idx_inspection_units_parent", + "columns": [ + "parent_unit_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspections": { + "name": "inspections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspector_id": { + "name": "inspector_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "property_address": { + "name": "property_address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "address_place_id": { + "name": "address_place_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_city": { + "name": "address_city", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_state": { + "name": "address_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_zip": { + "name": "address_zip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_county": { + "name": "address_county", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_lat": { + "name": "address_lat", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_lng": { + "name": "address_lng", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_geocoded_at": { + "name": "address_geocoded_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'requested'" + }, + "report_status": { + "name": "report_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'in_progress'" + }, + "payment_status": { + "name": "payment_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unpaid'" + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancel_reason": { + "name": "cancel_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancel_notes": { + "name": "cancel_notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_payment_required": { + "name": "is_payment_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_agreement_required": { + "name": "is_agreement_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_auto_sign_on_publish": { + "name": "is_auto_sign_on_publish", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "discount_code_id": { + "name": "discount_code_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discount_amount_cents": { + "name": "discount_amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "closing_date": { + "name": "closing_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "referral_source": { + "name": "referral_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_number": { + "name": "reference_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "internal_notes": { + "name": "internal_notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "year_built": { + "name": "year_built", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sqft": { + "name": "sqft", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "foundation_type": { + "name": "foundation_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bedrooms": { + "name": "bedrooms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bathrooms": { + "name": "bathrooms", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lot_size": { + "name": "lot_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "property_facts": { + "name": "property_facts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cover_photo_id": { + "name": "cover_photo_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cover_crop": { + "name": "cover_crop", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cover_image_key": { + "name": "cover_image_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commercial_subtype": { + "name": "commercial_subtype", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_tier": { + "name": "report_tier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "county": { + "name": "county", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_automations_disabled": { + "name": "is_automations_disabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "template_snapshot": { + "name": "template_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "template_snapshot_version": { + "name": "template_snapshot_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 1 + }, + "profile_override": { + "name": "profile_override", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "require_defect_fields_override": { + "name": "require_defect_fields_override", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "concierge_status": { + "name": "concierge_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_team_mode": { + "name": "is_team_mode", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "source_inspection_id": { + "name": "source_inspection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "root_inspection_id": { + "name": "root_inspection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reinspection_round": { + "name": "reinspection_round", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit_inspection_mode": { + "name": "unit_inspection_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'tagged'" + }, + "location_options": { + "name": "location_options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sampling_declaration": { + "name": "sampling_declaration", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pca_narrative": { + "name": "pca_narrative", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deviations": { + "name": "deviations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_photo_mode": { + "name": "report_photo_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scheduled_start_ms": { + "name": "scheduled_start_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scheduled_end_ms": { + "name": "scheduled_end_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_min": { + "name": "duration_min", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "badge_layout_override": { + "name": "badge_layout_override", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_photo_columns": { + "name": "report_photo_columns", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "referred_by_contact_id": { + "name": "referred_by_contact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unlocked_at": { + "name": "unlocked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unlocked_by": { + "name": "unlocked_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unlock_reason": { + "name": "unlock_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reports_generated_at": { + "name": "reports_generated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deposit_required_cents": { + "name": "deposit_required_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_deposit_overridden": { + "name": "is_deposit_overridden", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_inspections_request": { + "name": "idx_inspections_request", + "columns": [ + "request_id" + ], + "isUnique": false + }, + "idx_inspections_tenant_status": { + "name": "idx_inspections_tenant_status", + "columns": [ + "tenant_id", + "status" + ], + "isUnique": false + }, + "idx_inspections_tenant_date": { + "name": "idx_inspections_tenant_date", + "columns": [ + "tenant_id", + "date" + ], + "isUnique": false + }, + "idx_inspections_inspector_date": { + "name": "idx_inspections_inspector_date", + "columns": [ + "inspector_id", + "date" + ], + "isUnique": false + }, + "idx_inspections_root": { + "name": "idx_inspections_root", + "columns": [ + "root_inspection_id" + ], + "isUnique": false + }, + "idx_inspections_tenant_created": { + "name": "idx_inspections_tenant_created", + "columns": [ + "tenant_id", + "created_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "inspections_tenant_id_tenants_id_fk": { + "name": "inspections_tenant_id_tenants_id_fk", + "tableFrom": "inspections", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspections_inspector_id_users_id_fk": { + "name": "inspections_inspector_id_users_id_fk", + "tableFrom": "inspections", + "tableTo": "users", + "columnsFrom": [ + "inspector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspections_template_id_templates_id_fk": { + "name": "inspections_template_id_templates_id_fk", + "tableFrom": "inspections", + "tableTo": "templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspections_discount_code_id_discount_codes_id_fk": { + "name": "inspections_discount_code_id_discount_codes_id_fk", + "tableFrom": "inspections", + "tableTo": "discount_codes", + "columnsFrom": [ + "discount_code_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inspections_request_id_inspection_requests_id_fk": { + "name": "inspections_request_id_inspection_requests_id_fk", + "tableFrom": "inspections", + "tableTo": "inspection_requests", + "columnsFrom": [ + "request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspector_credentials": { + "name": "inspector_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "member_number": { + "name": "member_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_r2_key": { + "name": "image_r2_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_inspector_credentials_tenant": { + "name": "idx_inspector_credentials_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_inspector_credentials_user": { + "name": "idx_inspector_credentials_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inspector_service_areas": { + "name": "inspector_service_areas", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "zip_prefix": { + "name": "zip_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_inspector_service_areas": { + "name": "uq_inspector_service_areas", + "columns": [ + "tenant_id", + "user_id", + "zip_prefix" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invoices": { + "name": "invoices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_email": { + "name": "client_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "line_items": { + "name": "line_items", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sent_at": { + "name": "sent_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "paid_at": { + "name": "paid_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method": { + "name": "payment_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "partial_paid_at": { + "name": "partial_paid_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voided_at": { + "name": "voided_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "qbo_sync_status": { + "name": "qbo_sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'USD'" + }, + "amount_paid_cents": { + "name": "amount_paid_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_number": { + "name": "invoice_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_invoices_inspection": { + "name": "idx_invoices_inspection", + "columns": [ + "inspection_id" + ], + "isUnique": false + }, + "idx_invoices_contact": { + "name": "idx_invoices_contact", + "columns": [ + "tenant_id", + "contact_id" + ], + "isUnique": false + }, + "uq_invoices_tenant_number": { + "name": "uq_invoices_tenant_number", + "columns": [ + "tenant_id", + "invoice_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invoices_tenant_id_tenants_id_fk": { + "name": "invoices_tenant_id_tenants_id_fk", + "tableFrom": "invoices", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invoices_inspection_id_inspections_id_fk": { + "name": "invoices_inspection_id_inspections_id_fk", + "tableFrom": "invoices", + "tableTo": "inspections", + "columnsFrom": [ + "inspection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invoices_contact_id_contacts_id_fk": { + "name": "invoices_contact_id_contacts_id_fk", + "tableFrom": "invoices", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "marketplace_libraries": { + "name": "marketplace_libraries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "semver": { + "name": "semver", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema": { + "name": "schema", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "changelog": { + "name": "changelog", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "download_count": { + "name": "download_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_featured": { + "name": "is_featured", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jurisdiction": { + "name": "jurisdiction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspection_kind": { + "name": "inspection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delisted_at": { + "name": "delisted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_marketplace_libraries_kind_featured": { + "name": "idx_marketplace_libraries_kind_featured", + "columns": [ + "kind", + "is_featured" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "message_templates": { + "name": "message_templates", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "variables": { + "name": "variables", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_seeded": { + "name": "is_seeded", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + } + }, + "indexes": { + "idx_message_templates_tenant_channel": { + "name": "idx_message_templates_tenant_channel", + "columns": [ + "tenant_id", + "channel" + ], + "isUnique": false + }, + "idx_message_templates_variant": { + "name": "idx_message_templates_variant", + "columns": [ + "tenant_id", + "name", + "channel", + "locale" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messaging_compliance": { + "name": "messaging_compliance", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'own'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_profile_sid": { + "name": "customer_profile_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_profile_status": { + "name": "customer_profile_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "brand_sid": { + "name": "brand_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "brand_status": { + "name": "brand_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "campaign_sid": { + "name": "campaign_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "campaign_status": { + "name": "campaign_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tfv_sid": { + "name": "tfv_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tfv_status": { + "name": "tfv_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "messaging_resource_sid": { + "name": "messaging_resource_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_meta": { + "name": "provider_meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provisioned_number": { + "name": "provisioned_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provisioned_number_sid": { + "name": "provisioned_number_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "has_sender_attached": { + "name": "has_sender_attached", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "compliance_status": { + "name": "compliance_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'not_started'" + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "migration_batches": { + "name": "migration_batches", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "intent": { + "name": "intent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vendor": { + "name": "vendor", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "adapter_name": { + "name": "adapter_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "adapter_version": { + "name": "adapter_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "manifest": { + "name": "manifest", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "conflict_policy": { + "name": "conflict_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'staged'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reverted_at": { + "name": "reverted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "upload_authorized_by": { + "name": "upload_authorized_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "upload_authorized_at": { + "name": "upload_authorized_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "upload_authorization_version": { + "name": "upload_authorization_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "staff_access_authorized_by": { + "name": "staff_access_authorized_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "staff_access_authorized_at": { + "name": "staff_access_authorized_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "staff_access_authorization_version": { + "name": "staff_access_authorization_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_migration_batches_tenant_created": { + "name": "idx_migration_batches_tenant_created", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "idx_migration_batches_expires": { + "name": "idx_migration_batches_expires", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "migration_rows": { + "name": "migration_rows", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "batch_id": { + "name": "batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity": { + "name": "entity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "conflict_with": { + "name": "conflict_with", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_id": { + "name": "created_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prior_state": { + "name": "prior_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_migration_rows_batch_status": { + "name": "idx_migration_rows_batch_status", + "columns": [ + "batch_id", + "status" + ], + "isUnique": false + }, + "idx_migration_rows_tenant": { + "name": "idx_migration_rows_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_preferences": { + "name": "notification_preferences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "class_id": { + "name": "class_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_notification_prefs_unique": { + "name": "idx_notification_prefs_unique", + "columns": [ + "tenant_id", + "subject_kind", + "subject_id", + "class_id", + "channel" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "order_payments": { + "name": "order_payments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recorded_by": { + "name": "recorded_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refunds_id": { + "name": "refunds_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_order_payments_inspection": { + "name": "idx_order_payments_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + }, + "idx_order_payments_invoice": { + "name": "idx_order_payments_invoice", + "columns": [ + "tenant_id", + "invoice_id" + ], + "isUnique": false + }, + "uq_order_payments_provider_ref": { + "name": "uq_order_payments_provider_ref", + "columns": [ + "tenant_id", + "provider", + "provider_ref" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "orphaned_media": { + "name": "orphaned_media", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_orphaned_media_key": { + "name": "idx_orphaned_media_key", + "columns": [ + "tenant_id", + "r2_key" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "processed_webhook_events": { + "name": "processed_webhook_events", + "columns": { + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "received_at": { + "name": "received_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "psq_responses": { + "name": "psq_responses", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "responses": { + "name": "responses", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sent'" + }, + "share_token": { + "name": "share_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sent_at": { + "name": "sent_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "received_at": { + "name": "received_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_psq_inspection": { + "name": "uq_psq_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": true + }, + "idx_psq_share_token": { + "name": "idx_psq_share_token", + "columns": [ + "share_token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "qbo_connections": { + "name": "qbo_connections", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "realm_id": { + "name": "realm_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_enc": { + "name": "access_token_enc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token_enc": { + "name": "refresh_token_enc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_expires_at": { + "name": "token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_sync_enabled": { + "name": "is_sync_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "default_item_id": { + "name": "default_item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'1'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_qbo_connections_realm": { + "name": "uq_qbo_connections_realm", + "columns": [ + "realm_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "qbo_entity_map": { + "name": "qbo_entity_map", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oi_type": { + "name": "oi_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oi_id": { + "name": "oi_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "qbo_type": { + "name": "qbo_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "qbo_id": { + "name": "qbo_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "qbo_sync_token": { + "name": "qbo_sync_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synced_at": { + "name": "synced_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_qbo_entity_map_qbo": { + "name": "idx_qbo_entity_map_qbo", + "columns": [ + "tenant_id", + "qbo_type", + "qbo_id" + ], + "isUnique": true + }, + "idx_qbo_entity_map_oi": { + "name": "idx_qbo_entity_map_oi", + "columns": [ + "tenant_id", + "oi_type", + "oi_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "qbo_sync_errors": { + "name": "qbo_sync_errors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oi_type": { + "name": "oi_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oi_id": { + "name": "oi_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_msg": { + "name": "error_msg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_resolved": { + "name": "is_resolved", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rating_systems": { + "name": "rating_systems", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "levels": { + "name": "levels", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_seed": { + "name": "is_seed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_rating_systems_tenant_slug": { + "name": "idx_rating_systems_tenant_slug", + "columns": [ + "tenant_id", + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rating_systems_tenant_id_tenants_id_fk": { + "name": "rating_systems_tenant_id_tenants_id_fk", + "tableFrom": "rating_systems", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repair_request_items": { + "name": "repair_request_items", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repair_request_id": { + "name": "repair_request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "finding_key": { + "name": "finding_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_label": { + "name": "item_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "comment_snapshot": { + "name": "comment_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "requested_credit_cents": { + "name": "requested_credit_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "defect_title_snapshot": { + "name": "defect_title_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "location_snapshot": { + "name": "location_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_snapshot": { + "name": "category_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trade_snapshot": { + "name": "trade_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repair_action_tag": { + "name": "repair_action_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_repair_request_items_rr": { + "name": "idx_repair_request_items_rr", + "columns": [ + "repair_request_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repair_requests": { + "name": "repair_requests", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_ref": { + "name": "created_by_ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "custom_intro": { + "name": "custom_intro", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_token": { + "name": "share_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_repair_requests_inspection": { + "name": "idx_repair_requests_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + }, + "idx_repair_requests_share_token": { + "name": "idx_repair_requests_share_token", + "columns": [ + "share_token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "report_exports": { + "name": "report_exports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_report_exports_inspection": { + "name": "idx_report_exports_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "report_pdfs": { + "name": "report_pdfs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rendered_at": { + "name": "rendered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_version": { + "name": "source_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ready'" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_report_pdfs_inspection_type": { + "name": "uq_report_pdfs_inspection_type", + "columns": [ + "inspection_id", + "type", + "version_number" + ], + "isUnique": true + }, + "idx_report_pdfs_tenant": { + "name": "idx_report_pdfs_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_report_pdfs_status": { + "name": "idx_report_pdfs_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_report_pdfs_content_hash": { + "name": "idx_report_pdfs_content_hash", + "columns": [ + "inspection_id", + "type", + "content_hash" + ], + "isUnique": false + } + }, + "foreignKeys": { + "report_pdfs_tenant_id_tenants_id_fk": { + "name": "report_pdfs_tenant_id_tenants_id_fk", + "tableFrom": "report_pdfs", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "report_signoff": { + "name": "report_signoff", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "person_id": { + "name": "person_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "license": { + "name": "license", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "qualifications_ref": { + "name": "qualifications_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signed_at": { + "name": "signed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signature_ref": { + "name": "signature_ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_dual_role": { + "name": "is_dual_role", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_report_signoff_inspection": { + "name": "idx_report_signoff_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + }, + "uq_report_signoff_role": { + "name": "uq_report_signoff_role", + "columns": [ + "inspection_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "report_translations": { + "name": "report_translations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "report_id": { + "name": "report_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "english_hash": { + "name": "english_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "translated_hash": { + "name": "translated_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notice_version": { + "name": "notice_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ai_call_id": { + "name": "ai_call_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generated_at": { + "name": "generated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_report_translations_report_locale": { + "name": "uq_report_translations_report_locale", + "columns": [ + "tenant_id", + "report_id", + "locale" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "report_versions": { + "name": "report_versions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_json": { + "name": "snapshot_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prev_hash": { + "name": "prev_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_fingerprint": { + "name": "key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_amendment": { + "name": "is_amendment", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "report_id": { + "name": "report_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_report_versions_report_version": { + "name": "uq_report_versions_report_version", + "columns": [ + "report_id", + "version_number" + ], + "isUnique": true + }, + "idx_report_versions_verify_token": { + "name": "idx_report_versions_verify_token", + "columns": [ + "verification_token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "report_views": { + "name": "report_views", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token_id": { + "name": "access_token_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_viewed_at": { + "name": "first_viewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_viewed_at": { + "name": "last_viewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "view_count": { + "name": "view_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_report_views_scope": { + "name": "idx_report_views_scope", + "columns": [ + "tenant_id", + "inspection_id", + "access_token_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_service_id": { + "name": "inspection_service_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'in_progress'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notified_at": { + "name": "notified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "inspector_narrative": { + "name": "inspector_narrative", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_reports_inspection": { + "name": "idx_reports_inspection", + "columns": [ + "inspection_id" + ], + "isUnique": false + }, + "idx_reports_tenant": { + "name": "idx_reports_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "uq_reports_primary": { + "name": "uq_reports_primary", + "columns": [ + "inspection_id" + ], + "isUnique": true, + "where": "kind = 'primary'" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_inspectors": { + "name": "service_inspectors", + "columns": { + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_service_inspectors_tenant": { + "name": "idx_service_inspectors_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "service_inspectors_service_id_user_id_pk": { + "columns": [ + "service_id", + "user_id" + ], + "name": "service_inspectors_service_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_pay_rules": { + "name": "service_pay_rules", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deduction_cents": { + "name": "deduction_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_service_pay_rules_user": { + "name": "uq_service_pay_rules_user", + "columns": [ + "tenant_id", + "service_id", + "user_id" + ], + "isUnique": true, + "where": "user_id IS NOT NULL" + }, + "uq_service_pay_rules_default": { + "name": "uq_service_pay_rules_default", + "columns": [ + "tenant_id", + "service_id" + ], + "isUnique": true, + "where": "user_id IS NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "services": { + "name": "services", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agreement_id": { + "name": "agreement_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_event_type_slugs": { + "name": "default_event_type_slugs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deposit_policy": { + "name": "deposit_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_services_tenant": { + "name": "idx_services_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "services_tenant_id_tenants_id_fk": { + "name": "services_tenant_id_tenants_id_fk", + "tableFrom": "services", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "services_template_id_templates_id_fk": { + "name": "services_template_id_templates_id_fk", + "tableFrom": "services", + "tableTo": "templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "services_agreement_id_agreements_id_fk": { + "name": "services_agreement_id_agreements_id_fk", + "tableFrom": "services", + "tableTo": "agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key_enc": { + "name": "private_key_enc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key_iv": { + "name": "private_key_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Ed25519'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retired_at": { + "name": "retired_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_signing_keys_tenant_fingerprint": { + "name": "uq_signing_keys_tenant_fingerprint", + "columns": [ + "tenant_id", + "fingerprint" + ], + "isUnique": true + }, + "uq_signing_keys_tenant_active": { + "name": "uq_signing_keys_tenant_active", + "columns": [ + "tenant_id" + ], + "isUnique": true, + "where": "retired_at IS NULL" + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sms_consent_log": { + "name": "sms_consent_log", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_type": { + "name": "recipient_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "disclosure_version": { + "name": "disclosure_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "captured_via": { + "name": "captured_via", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'contact'" + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "disclosure_content_hash": { + "name": "disclosure_content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_sms_consent_contact": { + "name": "idx_sms_consent_contact", + "columns": [ + "tenant_id", + "contact_id", + "created_at" + ], + "isUnique": false + }, + "idx_sms_consent_subject": { + "name": "idx_sms_consent_subject", + "columns": [ + "tenant_id", + "subject_kind", + "subject_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sms_delivery_status": { + "name": "sms_delivery_status", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_sms_delivery_status_msg": { + "name": "idx_sms_delivery_status_msg", + "columns": [ + "tenant_id", + "provider_message_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sms_disclosure_versions": { + "name": "sms_disclosure_versions", + "columns": { + "version": { + "name": "version", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "statutory_form_productions": { + "name": "statutory_form_productions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "form_id": { + "name": "form_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "produced_by": { + "name": "produced_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "produced_at": { + "name": "produced_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_statutory_productions_form_version": { + "name": "idx_statutory_productions_form_version", + "columns": [ + "form_id", + "version" + ], + "isUnique": false + }, + "idx_statutory_productions_inspection": { + "name": "idx_statutory_productions_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "statutory_form_sightings": { + "name": "statutory_form_sightings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "form_id": { + "name": "form_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observed_hash": { + "name": "observed_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_statutory_form_sightings_seen": { + "name": "uq_statutory_form_sightings_seen", + "columns": [ + "form_id", + "source_url", + "observed_hash" + ], + "isUnique": true + }, + "idx_statutory_form_sightings_form": { + "name": "idx_statutory_form_sightings_form", + "columns": [ + "form_id", + "last_seen_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "statutory_form_versions": { + "name": "statutory_form_versions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "form_id": { + "name": "form_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "effective_from": { + "name": "effective_from", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mandatory_from": { + "name": "mandatory_from", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "effective_until": { + "name": "effective_until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_statutory_form_versions_form_version": { + "name": "uq_statutory_form_versions_form_version", + "columns": [ + "form_id", + "version" + ], + "isUnique": true + }, + "idx_statutory_form_versions_form": { + "name": "idx_statutory_form_versions_form", + "columns": [ + "form_id", + "effective_from" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tags": { + "name": "tags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_seed": { + "name": "is_seed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_tags_tenant_name": { + "name": "idx_tags_tenant_name", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "templates": { + "name": "templates", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "schema": { + "name": "schema", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rating_system_id": { + "name": "rating_system_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commercial_subtype": { + "name": "commercial_subtype", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_featured": { + "name": "is_featured", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_profile_id": { + "name": "default_profile_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retired_at": { + "name": "retired_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_templates_tenant": { + "name": "idx_templates_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_templates_rating_system": { + "name": "idx_templates_rating_system", + "columns": [ + "rating_system_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "templates_tenant_id_tenants_id_fk": { + "name": "templates_tenant_id_tenants_id_fk", + "tableFrom": "templates", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_custom_holidays": { + "name": "tenant_custom_holidays", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_tenant_custom_holidays_tenant_date": { + "name": "uq_tenant_custom_holidays_tenant_date", + "columns": [ + "tenant_id", + "date" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_library_imports": { + "name": "tenant_library_imports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "library_id": { + "name": "library_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imported_semver": { + "name": "imported_semver", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imported_at": { + "name": "imported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "local_entity_id": { + "name": "local_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uninstalled_at": { + "name": "uninstalled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_tenant_library_import": { + "name": "uq_tenant_library_import", + "columns": [ + "tenant_id", + "library_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_marketplace_import_history": { + "name": "tenant_marketplace_import_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "library_id": { + "name": "library_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_version": { + "name": "source_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_version": { + "name": "target_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rows_affected": { + "name": "rows_affected", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_marketplace_history_tenant": { + "name": "idx_marketplace_history_tenant", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "idx_marketplace_history_template": { + "name": "idx_marketplace_history_template", + "columns": [ + "template_id" + ], + "isUnique": false + }, + "idx_marketplace_history_library": { + "name": "idx_marketplace_history_library", + "columns": [ + "library_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_counters": { + "name": "usage_counters", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "period_key": { + "name": "period_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_usage_counters_tenant": { + "name": "idx_usage_counters_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_counters_tenant_id_metric_period_key_pk": { + "columns": [ + "tenant_id", + "metric", + "period_key" + ], + "name": "usage_counters_tenant_id_metric_period_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_configs": { + "name": "tenant_configs", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "support_email": { + "name": "support_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "company_address": { + "name": "company_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_pdf_footer_shown": { + "name": "is_pdf_footer_shown", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "is_pdf_page_numbers_shown": { + "name": "is_pdf_page_numbers_shown", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "is_pdf_license_shown": { + "name": "is_pdf_license_shown", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sender_email": { + "name": "sender_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reply_to": { + "name": "reply_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_mode": { + "name": "email_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'platform'" + }, + "video_mode": { + "name": "video_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'r2'" + }, + "sms_mode": { + "name": "sms_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'platform'" + }, + "sender_display_name": { + "name": "sender_display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "point_of_contact": { + "name": "point_of_contact", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'company'" + }, + "billing_url": { + "name": "billing_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "review_url": { + "name": "review_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "company_phone": { + "name": "company_phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "integration_config": { + "name": "integration_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secrets_enc": { + "name": "secrets_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dek_enc": { + "name": "dek_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ics_token": { + "name": "ics_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "widget_allowed_origins": { + "name": "widget_allowed_origins", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_profile_id": { + "name": "default_profile_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'signature'" + }, + "attention_thresholds": { + "name": "attention_thresholds", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"agreement_unsigned_h\":72,\"invoice_overdue_h\":72,\"report_unpublished_h\":72}'" + }, + "inspection_prefs": { + "name": "inspection_prefs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_repair_list_enabled": { + "name": "is_repair_list_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_customer_repair_export_enabled": { + "name": "is_customer_repair_export_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_unpaid_blocked": { + "name": "is_unpaid_blocked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_unsigned_agreement_blocked": { + "name": "is_unsigned_agreement_blocked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "custom_referral_sources": { + "name": "custom_referral_sources", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dashboard_column_prefs": { + "name": "dashboard_column_prefs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_concierge_review_required": { + "name": "is_concierge_review_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_inspector_choice_allowed": { + "name": "is_inspector_choice_allowed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_pdf_pipeline_enabled": { + "name": "is_pdf_pipeline_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_team_mode_default": { + "name": "is_team_mode_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "require_defect_fields": { + "name": "require_defect_fields", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "agreement_retention_years": { + "name": "agreement_retention_years", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 6 + }, + "reinspection_statuses": { + "name": "reinspection_statuses", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_collab_editing_enabled": { + "name": "is_collab_editing_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sms_byo_provider": { + "name": "sms_byo_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_byo_provider": { + "name": "email_byo_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'resend'" + }, + "is_managed_eligible": { + "name": "is_managed_eligible", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "managed_provider": { + "name": "managed_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'twilio'" + }, + "is_reserve_schedule_enabled": { + "name": "is_reserve_schedule_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "reserve_term_years": { + "name": "reserve_term_years", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 12 + }, + "inflation_rate_bps": { + "name": "inflation_rate_bps", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_timezone": { + "name": "default_timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'UTC'" + }, + "booking_slot_mode": { + "name": "booking_slot_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fixed'" + }, + "booking_slot_interval_min": { + "name": "booking_slot_interval_min", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "holiday_region": { + "name": "holiday_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "holiday_public_policy": { + "name": "holiday_public_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'open'" + }, + "holiday_internal_policy": { + "name": "holiday_internal_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'advisory'" + }, + "default_locale": { + "name": "default_locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en-US'" + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'USD'" + }, + "is_archive_revoking_access": { + "name": "is_archive_revoking_access", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "legal_mode": { + "name": "legal_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'hosted'" + }, + "custom_privacy_url": { + "name": "custom_privacy_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_terms_url": { + "name": "custom_terms_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "privacy_body": { + "name": "privacy_body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terms_body": { + "name": "terms_body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date_format": { + "name": "date_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'us'" + }, + "time_format": { + "name": "time_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'12h'" + }, + "booking_conflict_policy": { + "name": "booking_conflict_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'advisory'" + }, + "cancellation_policy": { + "name": "cancellation_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancellation_clause_agreement_id": { + "name": "cancellation_clause_agreement_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancellation_clause_version": { + "name": "cancellation_clause_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancellation_clause_attested_at": { + "name": "cancellation_clause_attested_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deposit_policy": { + "name": "deposit_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "booking_routing_strategy": { + "name": "booking_routing_strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'first_available'" + }, + "booking_min_lead_hours": { + "name": "booking_min_lead_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "booking_same_day_cutoff_time": { + "name": "booking_same_day_cutoff_time", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "company_lat": { + "name": "company_lat", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "company_lng": { + "name": "company_lng", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "company_geocoded_at": { + "name": "company_geocoded_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repair_quick_phrases": { + "name": "repair_quick_phrases", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "legal_name": { + "name": "legal_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_seq": { + "name": "invoice_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1000 + }, + "report_pdf_retention_years": { + "name": "report_pdf_retention_years", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 7 + }, + "is_report_view_counting_enabled": { + "name": "is_report_view_counting_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "tenant_configs_tenant_id_tenants_id_fk": { + "name": "tenant_configs_tenant_id_tenants_id_fk", + "tableFrom": "tenant_configs", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'free'" + }, + "stripe_connect_account_id": { + "name": "stripe_connect_account_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "max_users": { + "name": "max_users", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 5 + }, + "deployment_mode": { + "name": "deployment_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'shared'" + }, + "applied_cmd_seq": { + "name": "applied_cmd_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "applied_cred_seq": { + "name": "applied_cred_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_version": { + "name": "content_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "tenants_slug_unique": { + "name": "tenants_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discovery_objections": { + "name": "discovery_objections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email_hash": { + "name": "email_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "proved_by": { + "name": "proved_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "withdrawn_at": { + "name": "withdrawn_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uq_discovery_objections_email_hash": { + "name": "uq_discovery_objections_email_hash", + "columns": [ + "email_hash" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_ai_attestations": { + "name": "tenant_ai_attestations", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_owner": { + "name": "account_owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "terms_version": { + "name": "terms_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attested_at": { + "name": "attested_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "policy_version": { + "name": "policy_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_ai_configs": { + "name": "tenant_ai_configs", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_courtesy_translation_enabled": { + "name": "is_courtesy_translation_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_templates": { + "name": "email_templates", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "blocks": { + "name": "blocks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "email_templates_tenant_id_tenants_id_fk": { + "name": "email_templates_tenant_id_tenants_id_fk", + "tableFrom": "email_templates", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "email_templates_tenant_id_trigger_pk": { + "columns": [ + "tenant_id", + "trigger" + ], + "name": "email_templates_tenant_id_trigger_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_slug_history": { + "name": "tenant_slug_history", + "columns": { + "old_slug": { + "name": "old_slug", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "changed_at": { + "name": "changed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retired_until": { + "name": "retired_until", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_tenant_slug_history_tenant": { + "name": "idx_tenant_slug_history_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_invites": { + "name": "tenant_invites", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'inspector'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_overrides": { + "name": "permission_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_invites_tenant": { + "name": "idx_invites_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "uq_tenant_invites_pending_email": { + "name": "uq_tenant_invites_pending_email", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true, + "where": "status = 'pending'" + } + }, + "foreignKeys": { + "tenant_invites_tenant_id_tenants_id_fk": { + "name": "tenant_invites_tenant_id_tenants_id_fk", + "tableFrom": "tenant_invites", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "photo_url": { + "name": "photo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_signature_base64": { + "name": "default_signature_base64", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_signature_enabled": { + "name": "is_signature_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manager'" + }, + "onboarding_state": { + "name": "onboarding_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_totp_enabled": { + "name": "is_totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_recovery_codes": { + "name": "totp_recovery_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_verified_at": { + "name": "totp_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terms_accepted": { + "name": "terms_accepted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_overrides": { + "name": "permission_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date_format": { + "name": "date_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_format": { + "name": "time_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "service_origin_address": { + "name": "service_origin_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "service_origin_lat": { + "name": "service_origin_lat", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "service_origin_lng": { + "name": "service_origin_lng", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "statutory_license_type": { + "name": "statutory_license_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "statutory_qualification": { + "name": "statutory_qualification", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_users_deleted_at": { + "name": "idx_users_deleted_at", + "columns": [ + "deleted_at" + ], + "isUnique": false + }, + "uq_users_tenant_email": { + "name": "uq_users_tenant_email", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true, + "where": "deleted_at IS NULL" + }, + "idx_users_slug_per_tenant": { + "name": "idx_users_slug_per_tenant", + "columns": [ + "tenant_id", + "slug" + ], + "isUnique": true + }, + "idx_users_email": { + "name": "idx_users_email", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspector_slug": { + "name": "inspector_slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_kind": { + "name": "actor_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'tenant_user'" + }, + "platform_actor_id": { + "name": "platform_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_audit_tenant_created": { + "name": "idx_audit_tenant_created", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "idx_audit_entity": { + "name": "idx_audit_entity", + "columns": [ + "entity_type", + "entity_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_tenant_id_tenants_id_fk": { + "name": "audit_logs_tenant_id_tenants_id_fk", + "tableFrom": "audit_logs", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration_test_results": { + "name": "integration_test_results", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_ok": { + "name": "is_ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tested_by_user_id": { + "name": "tested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tested_at": { + "name": "tested_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step": { + "name": "step", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_integration_test_tenant_target": { + "name": "idx_integration_test_tenant_target", + "columns": [ + "tenant_id", + "target", + "tested_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notifications": { + "name": "notifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read_at": { + "name": "read_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_notifications_tenant_user_created": { + "name": "idx_notifications_tenant_user_created", + "columns": [ + "tenant_id", + "user_id", + "created_at" + ], + "isUnique": false + }, + "idx_notifications_tenant_user_unread": { + "name": "idx_notifications_tenant_user_unread", + "columns": [ + "tenant_id", + "user_id", + "read_at" + ], + "isUnique": false + }, + "idx_notifications_tenant_contact_created": { + "name": "idx_notifications_tenant_contact_created", + "columns": [ + "tenant_id", + "contact_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "notifications_tenant_id_tenants_id_fk": { + "name": "notifications_tenant_id_tenants_id_fk", + "tableFrom": "notifications", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "parked_cmd_events": { + "name": "parked_cmd_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "envelope": { + "name": "envelope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "received_at": { + "name": "received_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_parked_cmd_events_received_at": { + "name": "idx_parked_cmd_events_received_at", + "columns": [ + "received_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "processed_cmd_events": { + "name": "processed_cmd_events", + "columns": { + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "cmd_type": { + "name": "cmd_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "processed_at": { + "name": "processed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "slug_reservations": { + "name": "slug_reservations", + "columns": { + "slug": { + "name": "slug", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_outbox": { + "name": "sync_outbox", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_tried_at": { + "name": "last_tried_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_sync_outbox_status_created": { + "name": "idx_sync_outbox_status_created", + "columns": [ + "status", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_destruction_records": { + "name": "tenant_destruction_records", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_slug": { + "name": "tenant_slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rows_deleted": { + "name": "rows_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "r2_objects": { + "name": "r2_objects", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "r2_bytes": { + "name": "r2_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "kv_keys": { + "name": "kv_keys", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "destroyed_at": { + "name": "destroyed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'completed'" + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "record_version": { + "name": "record_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "stores_measured": { + "name": "stores_measured", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "store_results": { + "name": "store_results", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "incomplete_notified_at": { + "name": "incomplete_notified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_destruction_tenant": { + "name": "idx_destruction_tenant", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idx_destruction_destroyed_at": { + "name": "idx_destruction_destroyed_at", + "columns": [ + "destroyed_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenant_legal_versions": { + "name": "tenant_legal_versions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "doc": { + "name": "doc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body_snapshot": { + "name": "body_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_material": { + "name": "is_material", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_by_user_id": { + "name": "published_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_tenant_legal_versions_doc_version": { + "name": "idx_tenant_legal_versions_doc_version", + "columns": [ + "tenant_id", + "doc", + "version" + ], + "isUnique": true + }, + "idx_tenant_legal_versions_latest": { + "name": "idx_tenant_legal_versions_latest", + "columns": [ + "tenant_id", + "doc", + "published_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account_acceptances": { + "name": "account_acceptances", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_identity_ref": { + "name": "actor_identity_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "doc": { + "name": "doc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authority_basis": { + "name": "authority_basis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_account_acceptances_user_doc_version": { + "name": "uq_account_acceptances_user_doc_version", + "columns": [ + "user_id", + "doc", + "version" + ], + "isUnique": true + }, + "idx_account_acceptances_tenant": { + "name": "idx_account_acceptances_tenant", + "columns": [ + "tenant_id", + "accepted_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "legal_holds": { + "name": "legal_holds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "matter": { + "name": "matter", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "placed_by": { + "name": "placed_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "placed_at": { + "name": "placed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "released_at": { + "name": "released_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "released_by": { + "name": "released_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "release_reason": { + "name": "release_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_legal_holds_tenant_active": { + "name": "idx_legal_holds_tenant_active", + "columns": [ + "tenant_id", + "released_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "client_uploads": { + "name": "client_uploads", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uploaded_by_kind": { + "name": "uploaded_by_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uploaded_by_ref": { + "name": "uploaded_by_ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uploaded_by_name": { + "name": "uploaded_by_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_client_uploads_inspection": { + "name": "idx_client_uploads_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "statutory_form_entries": { + "name": "statutory_form_entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "form_id": { + "name": "form_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "values": { + "name": "values", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_statutory_form_entries_subject": { + "name": "uq_statutory_form_entries_subject", + "columns": [ + "tenant_id", + "inspection_id", + "form_id" + ], + "isUnique": true + }, + "idx_statutory_form_entries_inspection": { + "name": "idx_statutory_form_entries_inspection", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "statutory_inspection_details": { + "name": "statutory_inspection_details", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspection_id": { + "name": "inspection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inspector_signature_date": { + "name": "inspector_signature_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "employee_printed_name": { + "name": "employee_printed_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_name": { + "name": "owner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_email": { + "name": "owner_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_mailing_address": { + "name": "owner_mailing_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_home_phone": { + "name": "owner_home_phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_work_phone": { + "name": "owner_work_phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_cell_phone": { + "name": "owner_cell_phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "uq_statutory_inspection_details_subject": { + "name": "uq_statutory_inspection_details_subject", + "columns": [ + "tenant_id", + "inspection_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "uq_discount_codes_code_tenant": { + "columns": { + "upper(code)": { + "isExpression": true + } + } + } + } + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 5e1df96d8..66c4f7578 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -8,6 +8,20 @@ "when": 1788909939593, "tag": "0000_baseline", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1791726611028, + "tag": "0001_integration_test_step", + "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1791761229428, + "tag": "0002_automation_log_human_verified", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/api-types/index.ts b/packages/api-types/index.ts index 86c17207b..8b2d135ac 100644 --- a/packages/api-types/index.ts +++ b/packages/api-types/index.ts @@ -40,6 +40,7 @@ export type { InspectionSyncApi } from '../../server/api/inspection-sync'; export type { InspectionTagApi } from '../../server/api/tags'; export type { IntegrationsApi } from '../../server/api/integrations'; export type { IntegrationsAiApi } from '../../server/api/integrations-ai'; +export type { IntegrationsTestStepsApi } from '../../server/api/integrations-test-steps'; export type { InvoicesApi } from '../../server/api/invoices'; export type { MarketplaceApi } from '../../server/api/marketplace'; export type { MigrationIntakeApi } from '../../server/api/migration-intake'; diff --git a/scripts/backfill-reply-to.mjs b/scripts/backfill-reply-to.mjs new file mode 100644 index 000000000..584176b7e --- /dev/null +++ b/scripts/backfill-reply-to.mjs @@ -0,0 +1,118 @@ +#!/usr/bin/env node +/** + * One-off: give every tenant that has no Reply-To the email of its owner. + * + * New workspaces get this default at creation time; this covers the ones that + * predate it. Without a configured Reply-To, a customer replying to an outbound + * message reaches the platform sender address rather than the inspector. + * + * Rules (decided in scripts/lib/reply-to-backfill.mjs, unit-tested there): + * - only a NULL / empty / whitespace-only `tenant_configs.reply_to` is filled; + * a value anyone set is never overwritten, and the write statement re-checks + * that condition itself so a concurrent edit between read and write wins; + * - the owner is the earliest-created user whose `users.role` is 'owner' + * (the role enum is owner | manager | inspector | agent); + * - a tenant without an owner, or whose owner email is blank, is skipped. + * + * DRY RUN BY DEFAULT: it prints every change it would make and writes nothing. + * `--apply` performs the writes. Target is local D1 unless `--remote` is given. + * + * node scripts/backfill-reply-to.mjs # dry run, local D1 + * node scripts/backfill-reply-to.mjs --remote # dry run, remote D1 + * node scripts/backfill-reply-to.mjs --apply --remote # write, remote D1 + * + * Manual steps around a remote run, in order: + * 1. Take a D1 Time Travel bookmark first and record it + * (`wrangler d1 time-travel info `), so the run can be undone. + * 2. Run the dry run and review the list: right tenants, right addresses. + * 3. Run with `--apply`. Every statement re-checks that the field is still + * blank, so a run that stops half way can simply be repeated. + * 4. Verify: run the dry run again. It must report `filled 0`, and the + * `skipped` count must equal the previous `filled` plus the previous + * `skipped`. + * + * The script reaches D1 through `scripts/wrangler.mjs` like the other scripts + * here, and passes every statement as a temp file (`--file`), for reads and + * for writes. + */ +import { writeFileSync, unlinkSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { spawnSync } from 'node:child_process'; +import { planReplyToBackfill } from './lib/reply-to-backfill.mjs'; + +const args = process.argv.slice(2); +const apply = args.includes('--apply'); +const target = args.includes('--remote') ? '--remote' : '--local'; +const wrangler = join(import.meta.dirname, 'wrangler.mjs'); + +/** + * One SELECT through wrangler; returns the result rows. + * + * The statement goes through a temp file (`--file`): `scripts/wrangler.mjs` + * forwards its arguments through a shell without quoting them, so a SQL string + * passed with `--command` is split at the first space. + */ +function query(sql) { + const file = join(tmpdir(), `backfill-reply-to-read-${process.pid}.sql`); + writeFileSync(file, `${sql}; +`, 'utf8'); + try { + const r = spawnSync(process.execPath, [wrangler, 'd1', 'execute', 'DB', target, '--json', '--file', file], { + encoding: 'utf8', + env: { ...process.env, NO_COLOR: '1' }, + }); + if (r.status !== 0) throw new Error(`query failed: ${(r.stderr || r.stdout || '').slice(-400)}`); + // Wrangler prints warnings before the JSON; the JSON array starts on a + // line that holds nothing but "[". + const out = r.stdout; + const start = out.search(/^\[\s*$/m); + if (start === -1) throw new Error(`no JSON in wrangler output: ${out.slice(0, 200)}`); + return JSON.parse(out.slice(start))[0]?.results ?? []; + } finally { + unlinkSync(file); + } +} + +const quote = (s) => `'${String(s).replaceAll("'", "''")}'`; + +/** + * Upsert guarded on the field still being blank, so the statement is safe even + * if the row changed after the read. A tenant with no config row gets one; + * `updated_at` is the only other NOT NULL column. + */ +const writeSql = ({ tenantId, email }) => + `INSERT INTO tenant_configs (tenant_id, reply_to, updated_at) VALUES (${quote(tenantId)}, ${quote(email)}, ${Date.now()})\n` + + `ON CONFLICT(tenant_id) DO UPDATE SET reply_to = excluded.reply_to, updated_at = excluded.updated_at\n` + + `WHERE tenant_configs.reply_to IS NULL OR trim(tenant_configs.reply_to) = '';`; + +console.info(`[backfill-reply-to] ${apply ? 'APPLY' : 'DRY RUN'} against ${target} D1`); + +const tenants = query( + 'SELECT t.id AS tenantId, c.reply_to AS replyTo FROM tenants t LEFT JOIN tenant_configs c ON c.tenant_id = t.id', +); +const owners = query( + "SELECT tenant_id AS tenantId, email, created_at AS createdAt FROM users WHERE role = 'owner' AND deleted_at IS NULL AND tenant_id IS NOT NULL", +); +const { fills, skipped } = planReplyToBackfill(tenants, owners); + +for (const f of fills) console.info(` fill ${f.tenantId} -> ${f.email}`); +for (const s of skipped) console.info(` skip ${s.tenantId} (${s.reason})`); + +if (apply && fills.length > 0) { + const file = join(tmpdir(), `backfill-reply-to-${process.pid}.sql`); + writeFileSync(file, fills.map(writeSql).join('\n') + '\n', 'utf8'); + try { + const r = spawnSync(process.execPath, [wrangler, 'd1', 'execute', 'DB', target, '--file', file, '--yes'], { + stdio: 'inherit', + }); + if (r.status !== 0) { + console.error('[backfill-reply-to] write FAILED — nothing is confirmed; re-run the dry run to see the state'); + process.exitCode = r.status ?? 1; + } + } finally { + unlinkSync(file); + } +} + +console.info(`filled ${fills.length} / skipped ${skipped.length}${apply ? '' : ' (dry run — nothing written; re-run with --apply)'}`); diff --git a/scripts/check-notification-dispatch.mjs b/scripts/check-notification-dispatch.mjs index 419dd1b81..ad0dd96b8 100644 --- a/scripts/check-notification-dispatch.mjs +++ b/scripts/check-notification-dispatch.mjs @@ -106,6 +106,11 @@ const HTML_IN_ROUTE_ALLOW = [ // jane@x.com" cannot be compared to "Jane muted review requests". const SEND_EMAIL_RE = /\.sendEmail\s*\(/g; const SEND_EMAIL_ALLOW = [ + // The stepwise "Test connection" send: calls the provider adapter directly, and + // only ever to the signed-in user's own address (the request carries no + // recipient). It is a diagnostic of the workspace's own account, not a + // notification, so there is no class to name. + /^server\/lib\/integration\/email-test-steps\.ts$/, // The email service layer itself — `sendRendered` supplies the class from // the RenderResult, and `sendEmail` is the boundary being annotated. /^server\/services\/email\//, diff --git a/scripts/file-size-baseline.json b/scripts/file-size-baseline.json index 98ae75ccb..705dcf863 100644 --- a/scripts/file-size-baseline.json +++ b/scripts/file-size-baseline.json @@ -1,6 +1,6 @@ { - "app/routes/inspection-edit.tsx": 2602, - "app/routes/inspector-portal.tsx": 1393, + "app/routes/inspection-edit.tsx": 2605, + "app/routes/inspector-portal.tsx": 1396, "scripts/verify-statutory-render.mjs": 1283, "server/durable-objects/inspection-doc.ts": 1028, "app/routes/inspections.tsx": 929, @@ -15,7 +15,7 @@ "server/api/inspections/report-delivery.ts": 756, "server/services/inspection/inspection-analytics.service.ts": 748, "server/api/admin/admin-settings.ts": 747, - "server/index.ts": 747, + "server/index.ts": 749, "server/services/inspection/inspection-publish.service.ts": 746, "scripts/check-messaging-rules.mjs": 735, "app/routes/template-edit.tsx": 723, @@ -39,7 +39,7 @@ "server/services/inspection/inspection-photo.service.ts": 541, "app/routes/settings-profile.tsx": 540, "scripts/check-i18n-glossary.mjs": 540, - "server/api/inspections/publish.ts": 531, + "server/api/inspections/publish.ts": 535, "server/api/inspections/media-studio.ts": 530, "scripts/check-converter-literals.mjs": 528, "scripts/check-statutory-fidelity.mjs": 527, @@ -60,10 +60,10 @@ "scripts/lib/gate-registry.mjs": 465, "app/components/inspection/PeopleEditor.tsx": 463, "scripts/check-retention-manifest.mjs": 462, - "server/lib/middleware/di.ts": 460, + "server/lib/middleware/di.ts": 464, "server/lib/db/schema/tenant/core.ts": 459, "server/api/calendar.ts": 453, - "app/components/portal/sections/RepairBuilderSection.tsx": 450, + "app/components/portal/sections/RepairBuilderSection.tsx": 454, "app/components/editor/CostItemsPanel.tsx": 449, "server/api/inspections/results.ts": 449, "app/lib/collab/results-doc-connection.ts": 435, @@ -84,6 +84,7 @@ "app/routes/team.tsx": 405, "server/api/admin/admin-data.ts": 406, "server/services/email/base.ts": 430, + "server/lib/email/build-email-service.ts": 403, "scripts/check-non-translatable.mjs": 402, "playwright.config.ts": 401 } diff --git a/scripts/lib/reply-to-backfill.mjs b/scripts/lib/reply-to-backfill.mjs new file mode 100644 index 000000000..3bbd768db --- /dev/null +++ b/scripts/lib/reply-to-backfill.mjs @@ -0,0 +1,50 @@ +/** + * Which tenants get a Reply-To filled in, and with what. + * + * Pure: rows in, plan out, no database. `scripts/backfill-reply-to.mjs` reads + * the rows and writes the plan; this module only decides, so the rules can be + * tested without either. + * + * Rules: + * - a `reply_to` that is NULL, empty or whitespace-only counts as unset; + * anything else is a choice the operator made and is never touched; + * - the owner is the EARLIEST-created user with the `owner` role. If that + * person's email is blank the tenant is skipped, not handed to the next + * owner: guessing a different address is worse than leaving the field empty; + * - a tenant with no owner is skipped. + */ + +/** @param {unknown} v */ +const blank = (v) => typeof v !== 'string' || v.trim() === ''; + +/** + * @param {{ tenantId: string, replyTo: string | null }[]} tenants + * @param {{ tenantId: string, email: string | null, createdAt: number }[]} owners + * @returns {{ fills: { tenantId: string, email: string }[], skipped: { tenantId: string, reason: string }[] }} + */ +export function planReplyToBackfill(tenants, owners) { + /** @type {Map} */ + const earliest = new Map(); + for (const o of owners) { + const cur = earliest.get(o.tenantId); + if (!cur || o.createdAt < cur.createdAt) earliest.set(o.tenantId, o); + } + + const fills = []; + const skipped = []; + for (const t of tenants) { + if (!blank(t.replyTo)) { + skipped.push({ tenantId: t.tenantId, reason: 'already-set' }); + continue; + } + const owner = earliest.get(t.tenantId); + if (!owner) { + skipped.push({ tenantId: t.tenantId, reason: 'no-owner' }); + } else if (blank(owner.email)) { + skipped.push({ tenantId: t.tenantId, reason: 'owner-email-blank' }); + } else { + fills.push({ tenantId: t.tenantId, email: owner.email.trim() }); + } + } + return { fills, skipped }; +} diff --git a/scripts/middleware-budget-baseline.json b/scripts/middleware-budget-baseline.json index 1152efad7..7a76c653e 100644 --- a/scripts/middleware-budget-baseline.json +++ b/scripts/middleware-budget-baseline.json @@ -6,7 +6,7 @@ ], "globalMiddlewareUse": 12, "fanOutTotals": { - "files": 145, + "files": 146, "dollarMethods": 567, "apiWorker": 9 }, @@ -91,6 +91,10 @@ "dollarMethods": 1, "apiWorker": 0 }, + "app/lib/test-step.server.ts": { + "dollarMethods": 2, + "apiWorker": 0 + }, "app/lib/view-tracking.server.ts": { "dollarMethods": 2, "apiWorker": 0 @@ -488,7 +492,7 @@ "apiWorker": 0 }, "app/routes/settings-communication.tsx": { - "dollarMethods": 21, + "dollarMethods": 20, "apiWorker": 0 }, "app/routes/settings-compliance.tsx": { @@ -536,7 +540,7 @@ "apiWorker": 2 }, "app/routes/settings-integrations.tsx": { - "dollarMethods": 11, + "dollarMethods": 10, "apiWorker": 0 }, "app/routes/settings-profile.tsx": { diff --git a/scripts/tenant-scoping-baseline.json b/scripts/tenant-scoping-baseline.json index 7a48f9b25..b4cbf0f73 100644 --- a/scripts/tenant-scoping-baseline.json +++ b/scripts/tenant-scoping-baseline.json @@ -31,8 +31,9 @@ "server/services/automation/deliver-email.ts::deliverTemplatedEmail::const et = await db.select().from(eventTypes).where(eq(eventTypes.id, ev.eventTypeId as string)).get();", "server/services/automation/deliver-email.ts::deliverTemplatedEmail::const ev = await db.select().from(inspectionEvents).where(eq(inspectionEvents.id, log.eventId)).get();", "server/services/automation/deliver-email.ts::deliverTemplatedEmail::}).where(eq(automationLogs.id, log.id));", - "server/services/automation/delivery.ts::emailSvcCache::.where(eq(automationLogs.id, log.id));", - "server/services/automation/delivery.ts::emailSvcCache::}).where(eq(automationLogs.id, log.id));", + "server/services/automation/delivery.ts::getOrBuild::.where(eq(automationLogs.id, log.id));", + "server/services/automation/delivery.ts::getOrBuild::}).where(eq(automationLogs.id, log.id));", + "server/services/automation/provider-deferral.ts::applyProviderOutcome::.where(eq(automationLogs.id, logId));", "server/services/automation/report-email.ts::deliverReportEmail::.where(eq(automationLogs.id, log.id));", "server/services/concierge.service.ts::approveByInspector::.where(eq(inspections.id, row.inspectionId))", "server/services/concierge.service.ts::resolveToken::.where(eq(inspections.id, row.inspectionId))", diff --git a/server/api/auth.ts b/server/api/auth.ts index ae6acb1a7..d21fa646e 100644 --- a/server/api/auth.ts +++ b/server/api/auth.ts @@ -17,7 +17,7 @@ import { JoinTeamSchema, ForgotPasswordSchema, ResetPasswordSchema, - AuthResponseSchema, + AuthResponseSchema, LoginResponseSchema, SetupSchema, } from '../lib/validations/auth.schema'; import { createApiResponseSchema, SuccessResponseSchema } from '../lib/validations/shared.schema'; @@ -50,7 +50,7 @@ const loginRoute = createRoute(withMcpMetadata({ responses: { 200: { content: { - 'application/json': { schema: AuthResponseSchema.describe('TODO describe schema field for the OpenInspection MCP integration') } + 'application/json': { schema: LoginResponseSchema.describe('TODO describe schema field for the OpenInspection MCP integration') } }, description: 'Login successful' }, diff --git a/server/api/auth/totp.ts b/server/api/auth/totp.ts index fde387e48..208d675e4 100644 --- a/server/api/auth/totp.ts +++ b/server/api/auth/totp.ts @@ -296,7 +296,17 @@ const totpRoutes = createApiRouter() }, keyring); setCookie(c, AUTH_COOKIE_NAME, sessionToken, authCookieOptions()); - return c.json({ success: true, data: { redirect: '/inspections' } }, 200); + + // Token Relay BFF, exactly as the password step in server/api/auth.ts: + // the login page calls this in-process and its client does not forward + // Set-Cookie, so the only way it can open a session is to receive the + // JWT in the body. Only the BFF sends this header; a browser caller + // gets the cookie and never the token. + const isBff = c.req.header('x-token-relay') === '1'; + return c.json({ + success: true, + data: { redirect: '/inspections', ...(isBff ? { token: sessionToken } : {}) }, + }, 200); }); export default totpRoutes; diff --git a/server/api/inspections/publish.ts b/server/api/inspections/publish.ts index 709145916..d3fe08b02 100644 --- a/server/api/inspections/publish.ts +++ b/server/api/inspections/publish.ts @@ -15,6 +15,7 @@ import { auditFromContext } from '../../lib/audit'; import { getBookingHost, resolveTenantSlug } from '../../lib/url'; import { buildRenderReportUrl } from '../../lib/public-urls'; import { logger } from '../../lib/logger'; +import { stampIfHumanVerified } from '../../services/automation/human-verified'; import { createApiResponseSchema, SuccessResponseSchema } from '../../lib/validations/shared.schema'; import { PublishInspectionSchema, CreateReinspectionSchema } from '../../lib/validations/inspection.schema'; import { inspections as inspectionTable } from '../../lib/db/schema'; @@ -375,6 +376,9 @@ const publishRoutes = createApiRouter() }; const result = await service.publishInspection(id, tenantId, publishOptions); + // A verified person vouches for the report emails this publish queued, delayed ones included. + await stampIfHumanVerified(c.get('humanVerified'), getDrizzle(c), tenantId, id); + // The only record that a PERSON published this, and the only one that // survives a later unpublish. Written HERE rather than beside the // `return`: everything between is best-effort follow-on work, and the diff --git a/server/api/integrations-test-steps.ts b/server/api/integrations-test-steps.ts new file mode 100644 index 000000000..57035904b --- /dev/null +++ b/server/api/integrations-test-steps.ts @@ -0,0 +1,145 @@ +import { createRoute, z } from '@hono/zod-openapi'; +import { eq } from 'drizzle-orm'; +import { createApiRouter } from '../lib/openapi-router'; +import { Errors } from '../lib/errors'; +import { withMcpMetadata } from '../lib/route-metadata-standards'; +import { requireRole } from '../lib/middleware/rbac'; +import { checkRateLimit } from '../lib/rate-limit'; +import { getDrizzle, createDrizzle } from '../lib/route-helpers'; +import { tenantConfigs } from '../lib/db/schema'; +import { coerceEmailByoProvider, resolveEmailProvider } from '../lib/email/resolve-provider'; +import { recordIntegrationTest, type IntegrationTarget } from '../lib/integration-test-results'; +import { EmailTestStepBodySchema, StripeTestStepBodySchema, TestStepResultSchema } from '../lib/validations/integrations.schema'; +import { runEmailStep, type StepResult } from '../lib/integration/email-test-steps'; +import { runStripeStep } from '../lib/integration/stripe-test-steps'; +import { clearLastSend, lookupResendDelivery, readLastSend, readSessionEmail, writeLastSend } from '../lib/integration/email-test-store'; +import { readWebhookLog } from '../lib/stripe-webhook-log'; + +/** + * Stepwise "Test connection": the caller runs ONE step per request and the server + * re-derives whatever that step depends on. Mounted on its own base path (see + * `server/index.ts`) for the same typed-client reason as the AI routes. + * + * The single-shot probes in `integrations.ts` stay as they are; these routes add + * the checks that prove a real send / a real payment account, one at a time. + */ + +const StepResponse = z.object({ success: z.literal(true), data: TestStepResultSchema }); + +const emailStepRoute = createRoute(withMcpMetadata({ + method: 'post', + path: '/email', + tags: ['integrations'], + summary: 'Run one step of the stepwise email connection test', + middleware: [requireRole('owner', 'manager')], + request: { body: { content: { 'application/json': { schema: EmailTestStepBodySchema } } } }, + responses: { + 200: { content: { 'application/json': { schema: StepResponse.openapi('EmailTestStepResponse') } }, description: 'The step ran; `data.status` says how it went' }, + 429: { description: 'Too many test steps for this workspace' }, + }, + operationId: 'runEmailTestStep', + description: 'Runs one step against the stored email provider credentials. The test email goes to the signed-in user\'s own address only; the request carries no recipient. Non-Resend providers support only key_valid.', +}, { scopes: ['admin'], tier: 'extended' })); + +const stripeStepRoute = createRoute(withMcpMetadata({ + method: 'post', + path: '/stripe', + tags: ['integrations'], + summary: 'Run one step of the stepwise Stripe connection test', + middleware: [requireRole('owner', 'manager')], + request: { body: { content: { 'application/json': { schema: StripeTestStepBodySchema } } } }, + responses: { + 200: { content: { 'application/json': { schema: StepResponse.openapi('StripeTestStepResponse') } }, description: 'The step ran; `data.status` says how it went' }, + 429: { description: 'Too many test steps for this workspace' }, + }, + operationId: 'runStripeTestStep', + description: 'Read-only checks against the stored Stripe key: account reachable, charges/payouts enabled, and a verified webhook delivery seen. Creates nothing in the Stripe account.', +}, { scopes: ['admin'], tier: 'extended' })); + +/** Persist a decided step (pass/fail only). Best effort: never changes the response. */ +async function logStep( + env: { DB: D1Database }, tenantId: string, userId: string | null, + target: IntegrationTarget, provider: string | null, r: StepResult, +): Promise { + if (r.status !== 'pass' && r.status !== 'fail') return; + await recordIntegrationTest(createDrizzle(env.DB), { + tenantId, target, provider, ok: r.status === 'pass', detail: r.detail, testedByUserId: userId, step: r.step, + }).catch(() => {}); +} + +const integrationsTestStepsRoutes = createApiRouter() + .openapi(emailStepRoute, async (c) => { + const tenantId = c.get('tenantId'); + const user = c.get('user'); + if (!tenantId || !user?.sub) throw Errors.Unauthorized('Missing tenant scope'); + await checkRateLimit(c, 'integration-test-step', tenantId); + const { step } = c.req.valid('json'); + const db = getDrizzle(c); + const cfg = await db.select({ p: tenantConfigs.emailByoProvider }).from(tenantConfigs) + .where(eq(tenantConfigs.tenantId, tenantId)).get(); + const provider = coerceEmailByoProvider(cfg?.p); + + let result: StepResult; + if (provider === 'resend') { + const apiKey = c.env.RESEND_API_KEY; + if (!apiKey) { + result = { step, status: 'fail', detail: 'No Resend API key is saved.' }; + } else { + const identity = await c.var.services.branding.getEmailIdentity(tenantId); + result = await runEmailStep(step, { + apiKey, + senderEmail: identity.senderEmail, + senderDisplayName: identity.senderDisplayName ?? identity.companyName, + replyTo: identity.replyTo, + // Session user only. The body schema has no recipient field. + sessionEmail: await readSessionEmail(db, tenantId, user.sub), + lastSend: await readLastSend(c.env.TENANT_CACHE, tenantId, user.sub), + saveLastSend: (s) => writeLastSend(c.env.TENANT_CACHE, tenantId, user.sub, s), + clearLastSend: () => clearLastSend(c.env.TENANT_CACHE, tenantId, user.sub), + lookupDelivery: (id) => lookupResendDelivery(db, tenantId, id), + }); + } + } else if (step !== 'key_valid') { + result = { step, status: 'skipped', detail: 'Only the key check is available for this provider.' }; + } else { + const env = c.env; + const apiKey = provider === 'sendgrid' ? env.SENDGRID_API_KEY + : provider === 'postmark' ? env.POSTMARK_SERVER_TOKEN + : env.MAILGUN_API_KEY; + const domain = env.MAILGUN_DOMAIN; + if (!apiKey || (provider === 'mailgun' && !domain)) { + result = { step, status: 'fail', detail: 'The credentials for this provider are not fully saved.' }; + } else { + const adapter = resolveEmailProvider(provider, provider === 'mailgun' ? { apiKey, domain: domain! } : { apiKey }); + const v = adapter.validateCredentials ? await adapter.validateCredentials().catch(() => null) : { ok: true as const }; + result = v?.ok + ? { step, status: 'pass', detail: `${provider} accepted the credentials.` } + : { step, status: 'fail', detail: v ? `${provider} rejected the credentials.` : 'The credential check failed unexpectedly.' }; + } + } + await logStep(c.env, tenantId, user.sub, 'email', provider, result); + return c.json({ success: true as const, data: { ...result, provider } }, 200); + }) + .openapi(stripeStepRoute, async (c) => { + const tenantId = c.get('tenantId'); + const user = c.get('user'); + if (!tenantId || !user?.sub) throw Errors.Unauthorized('Missing tenant scope'); + await checkRateLimit(c, 'integration-test-step', tenantId); + const { step } = c.req.valid('json'); + const secretKey = c.env.STRIPE_SECRET_KEY; + const result = await runStripeStep(step, { + fetchAccount: secretKey + ? async () => ({ + ...(await new (await import('../services/stripe.service')).StripeService(secretKey).getAccountStatus()), + livemode: secretKey.startsWith('sk_live_') || secretKey.startsWith('rk_live_'), + }) + : null, + webhookSecretConfigured: !!c.env.STRIPE_WEBHOOK_SECRET, + readDeliveries: () => readWebhookLog(c.env.TENANT_CACHE, tenantId), + }); + await logStep(c.env, tenantId, user.sub, 'stripe', null, result); + return c.json({ success: true as const, data: { ...result, provider: null } }, 200); + }); + +export type IntegrationsTestStepsApi = typeof integrationsTestStepsRoutes; +export default integrationsTestStepsRoutes; diff --git a/server/api/integrations.ts b/server/api/integrations.ts index 5db872b16..ce359def0 100644 --- a/server/api/integrations.ts +++ b/server/api/integrations.ts @@ -147,6 +147,7 @@ const TestResultSchema = z.object({ detail: z.string().nullable().describe('Non-sensitive outcome summary or provider error message.'), testedByUserId: z.string().nullable().describe('JWT sub of the user who ran the probe.'), testedAt: z.number().describe('Epoch milliseconds when the probe ran.'), + step: z.string().nullable().describe('Step id when the row records one step of a stepwise test; null for single-shot probes.'), }).openapi('IntegrationTestResult'); const testResultsRoute = createRoute(withMcpMetadata({ diff --git a/server/api/session-context.ts b/server/api/session-context.ts index f6949ca73..dc8cbaa23 100644 --- a/server/api/session-context.ts +++ b/server/api/session-context.ts @@ -58,6 +58,7 @@ import { getBaseUrl } from '../lib/url'; import { resolveTenantLegalUrls, type LegalMode } from '../lib/legal-links'; import { resolveVideoProvider, videoStreamServiceable } from '../services/video/resolve'; import { unlockAtMs } from '../lib/email/outbound-cooling-window'; +import { resolveTurnstileSiteKey } from '../lib/middleware/bot-protection'; /** * Portal #98 item 3 — is the outbound cooling window OPEN for this viewer, and @@ -287,7 +288,7 @@ const sessionContextRoutes = createApiRouter() // Fail-open, matching the send gate: an unreadable anchor leaves this // null, so a D1 blip never puts a banner in front of someone whose sends // work fine. - let outboundCoolingWindow: { unlockAtMs: number } | null = null; + let outboundCoolingWindow: { unlockAtMs: number; turnstileSiteKey: string | null } | null = null; if (tenantId && profile.mode === 'saas') { try { const tenantRow = await getDrizzle(c) @@ -295,9 +296,12 @@ const sessionContextRoutes = createApiRouter() .from(tenants) .where(eq(tenants.id, tenantId)) .get(); - outboundCoolingWindow = resolveCoolingWindowForSession({ + const open = resolveCoolingWindowForSession({ mode: profile.mode, createdAt: tenantRow?.createdAt, nowMs: Date.now(), }); + // The site key rides along so the web app can ask for the + // challenge before a send, matched to the secret the API verifies with. + outboundCoolingWindow = open ? { ...open, turnstileSiteKey: resolveTurnstileSiteKey(c.env) } : null; } catch (e) { logger.warn('[session-context] cooling-window anchor read failed', { error: (e as Error).message }); } diff --git a/server/cron/jobs/automation.ts b/server/cron/jobs/automation.ts index f7a684a98..d23d9106c 100644 --- a/server/cron/jobs/automation.ts +++ b/server/cron/jobs/automation.ts @@ -139,9 +139,11 @@ export const automationFlushJob: CronJob = { renderSecret: env.JWT_SECRET, } : undefined; await svc.flush( - async (tid) => { + async (tid, opts) => { const tier = quotaGuard ? await readTenantTier(env.DB, tid) : undefined; - return buildTenantEmailService(env as EmailServiceEnv, tid, quotaGuard, tier); + // A row whose publisher passed the human check keeps that proof. + return buildTenantEmailService(env as EmailServiceEnv, tid, quotaGuard, tier, + opts?.humanVerified ? { humanVerified: async () => true } : {}); }, env.APP_NAME || 'OpenInspection', appBaseUrl, diff --git a/server/index.ts b/server/index.ts index 228fa6ee0..3f8b53fa0 100644 --- a/server/index.ts +++ b/server/index.ts @@ -37,6 +37,7 @@ import testHooksRoutes from './api/test-hooks'; import identityRoutes from './api/identity'; import integrationsApiRoutes from './api/integrations'; import integrationsAiRoutes from './api/integrations-ai'; +import integrationsTestStepsRoutes from './api/integrations-test-steps'; import analyticsRoutes from './api/analytics'; import billingRoutes from './api/billing'; import usageRoutes from './api/usage'; @@ -330,6 +331,7 @@ const routes = app // under one path collapse to the first in the typed client, so the second's // routes would exist at runtime and be uncallable from a loader. .route('/api/integrations/ai', integrationsAiRoutes) + .route('/api/integrations/test-steps', integrationsTestStepsRoutes) .route('/api/analytics', analyticsRoutes) .route('/api/inspections', inspectionsRoutes) .route('/api/credentials', credentialsRoutes) diff --git a/server/lib/db/schema/inspection/automation.ts b/server/lib/db/schema/inspection/automation.ts index d63137fdd..426d9df99 100644 --- a/server/lib/db/schema/inspection/automation.ts +++ b/server/lib/db/schema/inspection/automation.ts @@ -185,6 +185,13 @@ export const automationLogs = sqliteTable('automation_logs', { */ senderIdentity: text('sender_identity', { mode: 'json' }) .$type(), + /** + * When a person passed the human check on the request that queued this row + * (publishing a report in a new company's first 24 hours). Set, the delivery + * path sends at the due time instead of deferring to the end of the cooling + * window; null for every other row. Appended at table end for D1 rebuild safety. + */ + humanVerifiedAt: integer('human_verified_at', { mode: 'timestamp_ms' }), }, (t) => [ index('idx_automation_logs_pending').on(t.tenantId, t.status, t.sendAt), index('idx_automation_logs_insp').on(t.inspectionId), diff --git a/server/lib/db/schema/tenant/integration.ts b/server/lib/db/schema/tenant/integration.ts index 43c7fe42a..aa631eaaa 100644 --- a/server/lib/db/schema/tenant/integration.ts +++ b/server/lib/db/schema/tenant/integration.ts @@ -303,6 +303,9 @@ export const integrationTestResults = sqliteTable('integration_test_results', { // User who ran the probe (JWT sub); NULL if unknown. testedByUserId: text('tested_by_user_id'), testedAt: integer('tested_at', { mode: 'timestamp_ms' }).notNull(), + // Which step of a stepwise test this row records (e.g. 'key_valid', + // 'send_test'); NULL for the single-shot probes. Appended last (forward-only). + step: text('step'), }, (t) => [ index('idx_integration_test_tenant_target').on(t.tenantId, t.target, t.testedAt), ]); diff --git a/server/lib/email/build-email-service.ts b/server/lib/email/build-email-service.ts index 2aac03152..2aee4b278 100644 --- a/server/lib/email/build-email-service.ts +++ b/server/lib/email/build-email-service.ts @@ -12,7 +12,8 @@ import { buildEmailSuppression } from './suppression'; import { buildEmailDedupe } from './dedupe'; import { buildNotificationPreferences } from '../notifications/preference-port'; import { buildUnsubscribeLinks } from '../notifications/unsubscribe-footer'; -import { buildOutboundCoolingWindow, coolingWindowApplies } from './outbound-cooling-window'; +import { resolveTurnstileSiteKey } from '../middleware/bot-protection'; +import { buildOutboundCoolingWindow, coolingWindowApplies, type HumanVerifier } from './outbound-cooling-window'; import { getDeploymentProfile } from '../deployment-profile'; import { logger } from '../logger'; import { memoOnce } from '../request-scope'; @@ -32,6 +33,7 @@ import type { PlanQuotaGuard } from '../../features/plan-quota/guard'; export interface EmailServiceEnv { DB: D1Database; APP_MODE?: string; + TURNSTILE_SITE_KEY?: string; TENANT_CACHE: KVNamespace; /** Test-only email sink flag (see AppEnv.E2E_EMAIL_SINK). '1' ⇒ capture, don't send. */ E2E_EMAIL_SINK?: string; @@ -116,7 +118,7 @@ export function assembleTenantEmailService( cfg: LoadedEmailConfig, meterTenantId?: string, quotaGuard?: PlanQuotaGuard, tenantTier?: string, - options: { recipientInitiated?: boolean } = {}, + options: { recipientInitiated?: boolean; humanVerified?: HumanVerifier } = {}, ): EmailService { const { emailIdentity, emailBrand, dbSecrets, emailOverrides, emailByoProvider } = cfg; // The ONE place an absent answer becomes `false` (a platform-default @@ -264,7 +266,7 @@ export function assembleTenantEmailService( // here with no cast, which is the whole point of that narrowing. const profile = getDeploymentProfile(env); const cooling = meterTenantId && coolingWindowApplies({ profile, platformFunded: !isByo, recipientInitiated: options.recipientInitiated }) - ? buildOutboundCoolingWindow(env.DB, meterTenantId) + ? buildOutboundCoolingWindow(env.DB, meterTenantId, Date.now, options.humanVerified, resolveTurnstileSiteKey(env)) : undefined; // TEST-ONLY email sink (E2E). Capture every message to KV instead of @@ -391,9 +393,10 @@ export async function buildTenantEmailService( tenantId: string | undefined, quotaGuard?: PlanQuotaGuard, tenantTier?: string, + options: { humanVerified?: HumanVerifier } = {}, ): Promise { if (!tenantId) { return assembleTenantEmailService(env, { dbSecrets: {} }); } - return assembleTenantEmailService(env, await loadTenantEmailConfig(env, tenantId), tenantId, quotaGuard, tenantTier); + return assembleTenantEmailService(env, await loadTenantEmailConfig(env, tenantId), tenantId, quotaGuard, tenantTier, options); } diff --git a/server/lib/email/human-verifier.ts b/server/lib/email/human-verifier.ts new file mode 100644 index 000000000..272da1dae --- /dev/null +++ b/server/lib/email/human-verifier.ts @@ -0,0 +1,53 @@ +import type { Context } from 'hono'; +import type { HumanVerifier } from './outbound-cooling-window'; +import { isInProcessRequest, memoOnce } from '../request-scope'; +import { logger } from '../logger'; +import { resolveTurnstile, verifyTurnstileOutcome } from '../middleware/bot-protection'; + +/** + * The request's Turnstile proof, if the web app attached one (`x-turnstile-token`, + * relayed by the worker entry from the browser's one-time cookie). + * + * Verified lazily and at most once: only a send inside a new company's cooling + * window ever asks, so most requests never pay for the siteverify call. Any + * failure to verify reads as "not a person", never as a pass. + */ +export function buildHumanVerifier(c: Context): HumanVerifier | undefined { + // Only the web app's own server-side relay may present a token. An API or MCP + // client has no page to run the challenge on and is refused as before, and a + // header it sets itself is ignored rather than trusted. + if (!isInProcessRequest(c.env)) return undefined; + const token = c.req.header('x-turnstile-token')?.trim(); + if (!token) return undefined; + // Keyed by the token on the OUTER request's scope: a token redeems once, so every + // in-process call of one page action must share a single answer, or the second + // send of the same action would be refused for a token the first one spent. + const attempt = async (idempotencyKey: string): Promise<'yes' | 'no' | 'error'> => { + const policy = resolveTurnstile(c.env); + if (!policy.enforced) return 'no'; + try { + return await verifyTurnstileOutcome(token, policy.secret, idempotencyKey); + } catch { + logger.warn('[human-check] verification unavailable (secret missing?)'); + return 'error'; + } + }; + // One answer per token per page action, shared by every in-process call and by + // concurrent callers. A could-not-tell answer gets one fresh attempt INSIDE the + // shared computation, so the outcome that is kept is the second one; only if that + // also fails is the person treated as unverified. + const verifier: HumanVerifier = async () => { + const outcome = await memoOnce(c.env, `human-verified:${token}`, async () => { + // The same key on both tries: a first request that reached Cloudflare and only + // lost its answer must not turn into "already redeemed" on the retry. + const key = crypto.randomUUID(); + const first = await attempt(key); + return first === 'error' ? attempt(key) : first; + }); + return outcome === 'yes'; + }; + // Also exposed on the context, so a route that learns the person is verified + // (publish) can vouch for work it queued. + c.set('humanVerified', verifier); + return verifier; +} diff --git a/server/lib/email/outbound-cooling-window.ts b/server/lib/email/outbound-cooling-window.ts index 7866d7cbf..54118b6e9 100644 --- a/server/lib/email/outbound-cooling-window.ts +++ b/server/lib/email/outbound-cooling-window.ts @@ -18,6 +18,11 @@ * exempt for the same reason — which is also what makes the escape hatch * in the UI notice a real one rather than a consolation. * + * INSIDE THE WINDOW a send is allowed when the request carries a fresh Turnstile + * token (`HumanVerifier`). The window itself stays: it is the observation + * period, and what it no longer does is stop a person who has just passed a + * challenge. A request that cannot present a token is refused as before. + * * WHY THE COST IS DELIVERABILITY, NOT SENDS. `inspectorhub.io` publishes * `p=reject` with Resend and SES aligned. The damage from 50 free emails per * throwaway company is what it does to paying customers' report email, not @@ -139,6 +144,14 @@ export function unlockAtMs(tenantCreatedAtMs: number): number { * does not, so "off" is expressed by absence rather than by a branch inside * the transport. */ +/** + * Resolves true when the request in flight carries a Turnstile token that + * Cloudflare accepts. Built per request (see `diMiddleware`) and memoised, so a + * request that sends several messages pays for one verification, and a request + * that never reaches a gated send pays for none. + */ +export type HumanVerifier = () => Promise; + export interface OutboundCoolingPort { check(classId: string | undefined): Promise; } @@ -162,6 +175,9 @@ export function buildOutboundCoolingWindow( db: D1Database, tenantId: string, now: () => number = Date.now, + humanVerified?: HumanVerifier, + /** Rides along on the refusal so a page that has no session context can still show the challenge. */ + turnstileSiteKey?: string | null, ): OutboundCoolingPort { return { async check(classId) { @@ -190,7 +206,13 @@ export function buildOutboundCoolingWindow( const unlock = unlockAtMs(createdAt.getTime()); if (now() >= unlock) return; - throw Errors.OutboundCoolingWindow({ unlockAtMs: unlock, windowHours: COOLING_WINDOW_HOURS }); + // Inside the window a person may still send, by proving they are one. + // A request with no token (an API or MCP client, or a browser that + // has not been through the challenge) falls through to the refusal, + // whose code is what tells the web app to ask for the challenge. + if (humanVerified && await humanVerified()) return; + + throw Errors.OutboundCoolingWindow({ unlockAtMs: unlock, windowHours: COOLING_WINDOW_HOURS, turnstileSiteKey: turnstileSiteKey ?? null }); }, }; } diff --git a/server/lib/email/provider-failure.ts b/server/lib/email/provider-failure.ts new file mode 100644 index 000000000..b88283630 --- /dev/null +++ b/server/lib/email/provider-failure.ts @@ -0,0 +1,40 @@ +import { AppError, ErrorCode, Errors } from '../errors'; +import { logger } from '../logger'; +import type { EmailProvider } from './provider'; + +type Failure = Extract>, { ok: false }>; + +/** + * Turn a provider failure that needs no side effect into the error callers see. + * (A suppressed recipient is handled by the sender itself because it writes + * back to the suppression table first.) + * + * - quota_exceeded: 402. `details.quotaWindow` says daily or monthly when the + * adapter knew, so a caller that can wait for the reset tells them apart. + * - rate_limited: 429 with a plain "try again shortly" message. + * - sender_domain: the operator-facing domain error. + * - anything else: the generic delivery failure. + */ +export function providerFailureError(failure: Failure): AppError { + const { error, kind } = failure; + if (kind === 'quota_exceeded') { + logger.error('[email] Provider quota exceeded', { error, quotaWindow: failure.quotaWindow }); + return new AppError(402, ErrorCode.QUOTA_EXHAUSTED, + 'Your email sending quota is exhausted. Upgrade your plan or wait for the quota to reset.', + { + metric: 'email', used: 0, cap: 0, billingPortalUrl: null, + ...(failure.quotaWindow ? { quotaWindow: failure.quotaWindow } : {}), + }, + ); + } + if (kind === 'rate_limited') { + logger.warn('[email] Provider rate limit persisted after the inline retry', { retryAfterSeconds: failure.retryAfterSeconds }); + return Errors.EmailRateLimited(failure.retryAfterSeconds); + } + if (kind === 'sender_domain') { + logger.error('[email] Sender domain error', { error }); + return Errors.EmailSenderDomain({ hint: error }); + } + logger.error('[email] Delivery failed', { kind, error }); + return new AppError(502, ErrorCode.SERVICE_UNAVAILABLE, 'Email delivery failed. Please try again.'); +} diff --git a/server/lib/email/provider.ts b/server/lib/email/provider.ts index 25800c69c..cc7e64eec 100644 --- a/server/lib/email/provider.ts +++ b/server/lib/email/provider.ts @@ -79,6 +79,10 @@ export type NormalizedEmailEvent = { * so we stop trying before the provider rejects it again. * `quota_exceeded` — daily or monthly send quota exhausted. Operator must * upgrade their plan or wait for the quota to reset. + * `quotaWindow` says which one when the provider told us: + * a daily quota resets at the next UTC midnight, a monthly + * one only with the next billing period. Absent means the + * adapter could not tell (treat as unknown, not as either). * `sender_domain` — the `from` domain is unverified or the key is limited to * test sends. Operator must verify the domain in Resend. */ @@ -94,6 +98,8 @@ export type EmailFailureKind = * emails USED so far in the window (not remaining). Each field is present only * when the provider sent a valid reading for it. */ +export type EmailQuotaWindow = 'daily' | 'monthly'; + export interface EmailSendUsage { dailyUsed?: number; monthlyUsed?: number; @@ -104,13 +110,13 @@ export interface EmailProvider { * Send a transactional email. * * Returns `{ ok: true; id?; usage? }` on success. - * Returns `{ ok: false; error; kind; retryAfterSeconds?; usage? }` on failure — implementations MUST NOT + * Returns `{ ok: false; error; kind; retryAfterSeconds?; quotaWindow?; usage? }` on failure — implementations MUST NOT * throw. `kind` lets callers branch on WHY delivery failed rather than * treating all failures identically. */ sendEmail(args: EmailSendArgs): Promise< | { ok: true; id?: string; usage?: EmailSendUsage } - | { ok: false; error: string; kind: EmailFailureKind; retryAfterSeconds?: number; usage?: EmailSendUsage } + | { ok: false; error: string; kind: EmailFailureKind; retryAfterSeconds?: number; quotaWindow?: EmailQuotaWindow; usage?: EmailSendUsage } >; /** diff --git a/server/lib/email/providers/resend.ts b/server/lib/email/providers/resend.ts index 92c89273f..fa32f84e2 100644 --- a/server/lib/email/providers/resend.ts +++ b/server/lib/email/providers/resend.ts @@ -1,5 +1,5 @@ import { logger } from '../../logger'; -import type { EmailFailureKind, EmailProvider, EmailSendArgs, EmailSendUsage, EmailWebhookContext, NormalizedEmailEvent } from '../provider'; +import type { EmailFailureKind, EmailQuotaWindow, EmailProvider, EmailSendArgs, EmailSendUsage, EmailWebhookContext, NormalizedEmailEvent } from '../provider'; import { base64ToBytes, bytesToBase64, @@ -93,6 +93,12 @@ export function classifyResendError( return 'transient'; } +/** Which quota a Resend 429 error name refers to, or undefined when it is neither. */ +export function quotaWindowFor(name: string | undefined): EmailQuotaWindow | undefined { + if (name === 'daily_quota_exceeded') return 'daily'; + return name === 'monthly_quota_exceeded' ? 'monthly' : undefined; +} + /** * ResendProvider — thin fetch-based adapter over the Resend REST API. * Satisfies EmailProvider for send + credential validation. @@ -105,7 +111,7 @@ export class ResendProvider implements EmailProvider { args: EmailSendArgs, ): Promise< | { ok: true; id?: string; usage?: EmailSendUsage } - | { ok: false; error: string; kind: EmailFailureKind; retryAfterSeconds?: number; usage?: EmailSendUsage } + | { ok: false; error: string; kind: EmailFailureKind; retryAfterSeconds?: number; quotaWindow?: EmailQuotaWindow; usage?: EmailSendUsage } > { const payload: Record = { from: args.from, @@ -161,9 +167,11 @@ export class ResendProvider implements EmailProvider { else if (kind === 'suppressed') logger.warn('[email] ResendProvider delivery failed (suppressed)', failure); else logger.error(`[email] ResendProvider delivery failed (${kind})`, failure); const retryAfterSeconds = kind === 'rate_limited' ? readRetryAfterSeconds(res.headers) : undefined; + const quotaWindow = kind === 'quota_exceeded' ? quotaWindowFor(errName) : undefined; return { ok: false, error: errMsg, kind, ...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}), + ...(quotaWindow ? { quotaWindow } : {}), ...(usage ? { usage } : {}), }; } @@ -221,7 +229,7 @@ export class ResendProvider implements EmailProvider { * `restricted_api_key` and reports `reason: 'restricted'`. */ async listDomains(): Promise< - | { ok: true; total: number; verified: number } + | { ok: true; total: number; verified: number; domains: Array<{ name: string; status: string }> } | { ok: false; reason: 'restricted' | 'unreachable' | 'error'; status: number | null } > { let res: Response; @@ -244,7 +252,13 @@ export class ResendProvider implements EmailProvider { const status = (d as { status?: unknown } | null)?.status; return typeof status === 'string' && status.toLowerCase() === 'verified'; }).length; - return { ok: true, total: data.length, verified }; + const domains = data.flatMap((d) => { + const row = d as { name?: unknown; status?: unknown } | null; + return typeof row?.name === 'string' + ? [{ name: row.name.toLowerCase(), status: typeof row.status === 'string' ? row.status.toLowerCase() : 'unknown' }] + : []; + }); + return { ok: true, total: data.length, verified, domains }; } /** Count of verified domains visible to this key (0 when the key cannot list). */ diff --git a/server/lib/email/rate-limit-retry.ts b/server/lib/email/rate-limit-retry.ts new file mode 100644 index 000000000..2fcea7c37 --- /dev/null +++ b/server/lib/email/rate-limit-retry.ts @@ -0,0 +1,40 @@ +import type { EmailProvider } from './provider'; + +/** + * Longest provider-requested pause the request path will sit through. A person + * is waiting on the response, so anything longer is reported back instead. + */ +const MAX_INLINE_RETRY_SECONDS = 2; + +export interface RateLimitRetryOptions { + /** Injected so tests do not wait; defaults to a timer. */ + sleep?: (ms: number) => Promise; + maxWaitSeconds?: number; +} + +const timerSleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Wrap a provider so a short rate limit is absorbed once, in the request path. + * + * When the provider answers `rate_limited` AND told us to wait no longer than + * `maxWaitSeconds`, sleep that long and send once more. A longer wait, or no + * hint at all, is returned untouched for the caller to report: guessing a wait + * would hold the request open for a time the provider never asked for. Only one + * retry is made, so the worst case is a single short pause. + * + * Every other provider member is reachable through the prototype, unchanged. + */ +export function withRateLimitRetry(provider: EmailProvider, opts: RateLimitRetryOptions = {}): EmailProvider { + const sleep = opts.sleep ?? timerSleep; + const maxWait = opts.maxWaitSeconds ?? MAX_INLINE_RETRY_SECONDS; + const sendEmail: EmailProvider['sendEmail'] = async (args) => { + const first = await provider.sendEmail(args); + if (first.ok || first.kind !== 'rate_limited') return first; + const wait = first.retryAfterSeconds; + if (wait === undefined || wait > maxWait) return first; + await sleep(wait * 1000); + return provider.sendEmail(args); + }; + return Object.assign(Object.create(provider) as EmailProvider, { sendEmail }); +} diff --git a/server/lib/email/tenant-email-resolver.ts b/server/lib/email/tenant-email-resolver.ts index 69f875dda..24d3abbcd 100644 --- a/server/lib/email/tenant-email-resolver.ts +++ b/server/lib/email/tenant-email-resolver.ts @@ -2,6 +2,7 @@ import type { EmailService } from '../../services/email.service'; import { readTenantPlan, type PlanQuotaGuard } from '../../features/plan-quota/guard'; import { memoOnce } from '../request-scope'; import { assembleTenantEmailService, loadTenantEmailConfig, type EmailServiceEnv } from './build-email-service'; +import type { HumanVerifier } from './outbound-cooling-window'; /** * Explicit per-tenant email resolution for one request. @@ -53,6 +54,8 @@ export interface TenantEmailResolverDeps { /** Whether this deployment meters tenants against a usage allowance. */ hasUsageQuota: boolean; buildPlanQuota: () => PlanQuotaGuard | undefined; + /** Present when the request carries a Turnstile token; lets a new company send inside its cooling window. */ + humanVerified?: HumanVerifier; } export function createTenantEmailResolver(deps: TenantEmailResolverDeps): TenantEmailResolver { @@ -72,7 +75,7 @@ export function createTenantEmailResolver(deps: TenantEmailResolverDeps): Tenant const plan = deps.hasUsageQuota ? await memoOnce(deps.env, `plan:${tenantId}`, () => readTenantPlan(deps.env.DB, tenantId)).catch(() => null) : null; - return assembleTenantEmailService(deps.env, cfg, tenantId, deps.buildPlanQuota(), plan?.tier, { recipientInitiated }); + return assembleTenantEmailService(deps.env, cfg, tenantId, deps.buildPlanQuota(), plan?.tier, { recipientInitiated, ...(deps.humanVerified ? { humanVerified: deps.humanVerified } : {}) }); }; return { diff --git a/server/lib/errors.ts b/server/lib/errors.ts index c5419e0b5..91bccfc9f 100644 --- a/server/lib/errors.ts +++ b/server/lib/errors.ts @@ -60,6 +60,10 @@ export enum ErrorCode { // provider, or the API key is scoped to test sends only. Operator action // required: verify the domain in email provider settings. EMAIL_SENDER_DOMAIN = 'EMAIL_SENDER_DOMAIN', + // The email provider's request-rate limit was hit and did not clear within + // the short inline retry. Nothing is wrong with the message or the account; + // trying again in a few seconds is the right response. + EMAIL_RATE_LIMITED = 'EMAIL_RATE_LIMITED', } /** @@ -129,11 +133,11 @@ export const Errors = { // 403 rather than 402/429 deliberately: this is not a quota and not a rate // limit, it is a permission that has not vested yet. The CODE, not the // status, is what distinguishes it. - OutboundCoolingWindow: (details: { unlockAtMs: number; windowHours: number }) => + OutboundCoolingWindow: (details: { unlockAtMs: number; windowHours: number; turnstileSiteKey?: string | null }) => new AppError( 403, ErrorCode.OUTBOUND_COOLING_WINDOW, - `New companies cannot send client email on the shared sender for the first ${details.windowHours} hours. Everything else works, and connecting your own email provider removes the wait entirely.`, + `New companies must confirm they are a person before sending client email on the shared sender during the first ${details.windowHours} hours. Do it in the web app, or connect your own email provider to remove the check entirely.`, details, ), /** @@ -214,4 +218,12 @@ export const Errors = { 'Email could not be sent: the sender domain is not verified. Go to Settings → Email to fix this.', details, ), + /** 429: the provider is rate limiting sends right now. `retryAfterSeconds` is its hint, when it gave one. */ + EmailRateLimited: (retryAfterSeconds?: number) => + new AppError( + 429, + ErrorCode.EMAIL_RATE_LIMITED, + 'Email delivery is busy right now. Please try again shortly.', + retryAfterSeconds !== undefined ? { retryAfterSeconds } : undefined, + ), }; diff --git a/server/lib/integration-test-results.ts b/server/lib/integration-test-results.ts index 33f016e14..57530614d 100644 --- a/server/lib/integration-test-results.ts +++ b/server/lib/integration-test-results.ts @@ -11,7 +11,7 @@ * message) — never a key, token, or raw response body. */ import type { drizzle } from 'drizzle-orm/d1'; -import { and, desc, eq, inArray } from 'drizzle-orm'; +import { and, desc, eq, inArray, isNull } from 'drizzle-orm'; import { integrationTestResults } from './db/schema'; type Db = ReturnType; @@ -28,6 +28,8 @@ export interface IntegrationTestResult { detail: string | null; testedByUserId: string | null; testedAt: number; // epoch ms + /** Step id for a stepwise test row; null for single-shot probes. */ + step: string | null; } export interface RecordTestArgs { @@ -37,6 +39,8 @@ export interface RecordTestArgs { provider?: string | null; detail?: string | null; testedByUserId?: string | null; + /** Step id when this row records one step of a stepwise test. */ + step?: string | null; /** Injectable for tests; defaults to wall-clock. */ nowMs?: number; } @@ -74,15 +78,19 @@ export async function recordIntegrationTest(db: Db, args: RecordTestArgs): Promi detail: clampDetail(args.detail), testedByUserId: args.testedByUserId ?? null, testedAt: new Date(testedAt), + step: args.step ?? null, }); - // Prune to the newest KEEP_PER_TARGET for this (tenant, target). + // Prune to the newest KEEP_PER_TARGET for this (tenant, target, step): a stepwise + // run writes one row per step, and pruning the whole target would let a six-step + // run evict every earlier result, including the single-shot (step-less) ones. const rows = await db .select({ id: integrationTestResults.id }) .from(integrationTestResults) .where(and( eq(integrationTestResults.tenantId, args.tenantId), eq(integrationTestResults.target, args.target), + args.step ? eq(integrationTestResults.step, args.step) : isNull(integrationTestResults.step), )) .orderBy(desc(integrationTestResults.testedAt)) .all(); @@ -112,5 +120,6 @@ export async function listIntegrationTestResults(db: Db, tenantId: string): Prom detail: r.detail, testedByUserId: r.testedByUserId, testedAt: r.testedAt instanceof Date ? r.testedAt.getTime() : Number(r.testedAt), + step: r.step, })); } diff --git a/server/lib/integration/email-test-steps.ts b/server/lib/integration/email-test-steps.ts new file mode 100644 index 000000000..34dfe66b6 --- /dev/null +++ b/server/lib/integration/email-test-steps.ts @@ -0,0 +1,187 @@ +/** + * Stepwise "Test connection" for a workspace's own Resend account. + * + * The single-shot probe only proved the key was accepted. Each step here proves + * one more thing a real send depends on, and reports one concrete reason when it + * fails, so a workspace can see WHERE its setup stops working: + * + * key_valid the key is accepted (401/403 = rejected, no reply = unreachable) + * key_scope full-access key, or send-only (cannot read the domain list) + * sender_domain_verified the saved sender's domain is on the account and verified + * send_test one real message, to the signed-in user's own address + * delivery the provider's delivered / bounced event for that message + * quota usage counters the send response carried + * + * Steps run one at a time. The runner never trusts the caller's idea of what + * already passed: the cheap read-only prerequisites are re-run here, and the + * delivery and quota steps read what the last `send_test` stored. A step whose + * prerequisite FAILED reports `skipped`; one whose prerequisite was merely + * skipped (a send-only key cannot list domains) still runs. + * + * Recipient: `send_test` mails `deps.sessionEmail` and nothing else. There is no + * recipient parameter anywhere in this module, so a test endpoint built on it + * cannot be turned into a way to mail an arbitrary address. + * + * `detail` strings are fixed, short and non-sensitive: never the key, never a + * provider response body. + */ +import { ResendProvider } from '../email/providers/resend'; +import type { EmailSendUsage } from '../email/provider'; + +export type EmailStepId = + 'key_valid' | 'key_scope' | 'sender_domain_verified' | 'send_test' | 'delivery' | 'quota'; + +export type StepStatus = 'pass' | 'fail' | 'skipped' | 'pending'; + +export interface StepResult { + step: string; + status: StepStatus; + detail: string; +} + +/** What the last `send_test` left behind, for the delivery and quota steps. */ +export interface LastSend { + messageId: string; + usage?: EmailSendUsage; +} + +export type DeliveryOutcome = 'delivered' | 'bounced' | 'none'; + +export interface EmailStepDeps { + apiKey: string; + senderEmail: string | null; + senderDisplayName: string | null; + replyTo: string | null; + /** The signed-in user's own address, read from their account row. */ + sessionEmail: string | null; + lastSend: LastSend | null; + saveLastSend(send: LastSend): Promise; + /** Forget the previous test send, so a failed new one cannot leave an old message to be reported on. */ + clearLastSend(): Promise; + lookupDelivery(messageId: string): Promise; +} + +const result = (step: EmailStepId, status: StepStatus, detail: string): StepResult => ({ step, status, detail }); + +/** Resend allows only a couple of requests per second per team, and a step makes several. */ +const RATE_LIMIT_PAUSE_MS = 1100; +const pause = () => new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_PAUSE_MS)); + +async function keyValid(provider: ResendProvider): Promise { + let probe = await provider.probeApiKey(); + if (probe.status === 429) { await pause(); probe = await provider.probeApiKey(); } + // Still rate limited: the key was never checked, which is not the same as accepted. + if (probe.status === 429) return result('key_valid', 'fail', 'Resend is rate limiting this account, so the key could not be checked. Wait a moment and retry.'); + if (!probe.reachable) return result('key_valid', 'fail', 'Could not reach Resend. Try again in a moment.'); + if (!probe.valid) return result('key_valid', 'fail', 'Resend rejected the saved API key.'); + return result('key_valid', 'pass', 'Resend accepted the API key.'); +} + +type DomainListing = Awaited>; + +function keyScope(listing: DomainListing): StepResult { + if (listing.ok) return result('key_scope', 'pass', 'Full-access key: the account\'s domains can be read.'); + if (listing.reason === 'restricted') { + return result('key_scope', 'pass', 'Send-only key: domains cannot be read, so the sender domain check is skipped. Continue with the test email.'); + } + if (listing.reason === 'unreachable') return result('key_scope', 'fail', 'Could not reach Resend. Try again in a moment.'); + return result('key_scope', 'fail', `Resend could not list domains (status ${listing.status ?? 'unknown'}).`); +} + +function senderDomain(listing: DomainListing, senderEmail: string | null): StepResult { + if (!listing.ok && listing.reason === 'restricted') { + return result('sender_domain_verified', 'skipped', 'A send-only key cannot read domains. The test email will show whether the sender is accepted.'); + } + if (!listing.ok) return result('sender_domain_verified', 'fail', 'Resend could not list domains.'); + const at = senderEmail?.lastIndexOf('@') ?? -1; + if (!senderEmail || at < 1) return result('sender_domain_verified', 'fail', 'No sender email is saved. Save one under Communication settings first.'); + const domain = senderEmail.slice(at + 1).trim().toLowerCase(); + const match = listing.domains.find((d) => d.name === domain); + if (!match) return result('sender_domain_verified', 'fail', `${domain} is not a domain on this Resend account.`); + if (match.status !== 'verified') { + return result('sender_domain_verified', 'fail', `${domain} is on the account but its status is "${match.status}", not verified.`); + } + return result('sender_domain_verified', 'pass', `${domain} is verified.`); +} + +/** Strip characters that would break out of a quoted display name. */ +function displayName(name: string | null): string { + return (name ?? '').replace(/[<>"\r\n]/g, '').trim(); +} + +async function sendTest(provider: ResendProvider, deps: EmailStepDeps): Promise { + if (!deps.senderEmail) return result('send_test', 'fail', 'No sender email is saved. Save one under Communication settings first.'); + if (!deps.sessionEmail) return result('send_test', 'fail', 'Your account has no email address to send the test to.'); + if (/[\r\n]/.test(deps.senderEmail)) return result('send_test', 'fail', 'The saved sender email is not valid. Correct it under Communication settings.'); + const name = displayName(deps.senderDisplayName); + await deps.clearLastSend(); + // Straight to the provider adapter: a workspace testing its OWN account is not + // subject to the platform-funded send cooling window or the suppression list. + const message = { + from: name ? `${name} <${deps.senderEmail}>` : deps.senderEmail, + to: deps.sessionEmail, + subject: 'Test email from your inspection software', + html: '

This is a test message confirming that your email provider settings can send.

', + text: 'This is a test message confirming that your email provider settings can send.', + ...(deps.replyTo ? { replyTo: deps.replyTo } : {}), + }; + let sent = await provider.sendEmail(message); + if (!sent.ok && sent.kind === 'rate_limited') { await pause(); sent = await provider.sendEmail(message); } + if (sent.ok) { + if (sent.id) await deps.saveLastSend({ messageId: sent.id, ...(sent.usage ? { usage: sent.usage } : {}) }); + return result('send_test', 'pass', 'Test email accepted by Resend and sent to your own address.'); + } + switch (sent.kind) { + case 'sender_domain': return result('send_test', 'fail', 'Resend refused the sender address. Check that its domain is verified.'); + case 'quota_exceeded': return result('send_test', 'fail', `Your Resend ${sent.quotaWindow ?? 'sending'} quota is used up.`); + case 'rate_limited': return result('send_test', 'fail', 'Resend is rate limiting this account. Wait a moment and retry.'); + case 'suppressed': return result('send_test', 'fail', 'Resend suppressed your address as a recipient.'); + default: return result('send_test', 'fail', 'Resend did not accept the test email. Try again in a moment.'); + } +} + +async function delivery(deps: EmailStepDeps): Promise { + if (!deps.lastSend) return result('delivery', 'skipped', 'Send the test email first.'); + const outcome = await deps.lookupDelivery(deps.lastSend.messageId); + if (outcome === 'delivered') return result('delivery', 'pass', 'Resend reported the test email as delivered.'); + if (outcome === 'bounced') return result('delivery', 'fail', 'Resend reported the test email as bounced.'); + return result('delivery', 'pending', 'No delivery report yet. It needs the Resend webhook set up; check again shortly.'); +} + +function quota(deps: EmailStepDeps): StepResult { + if (!deps.lastSend) return result('quota', 'skipped', 'Send the test email first.'); + const u = deps.lastSend.usage; + if (!u) return result('quota', 'skipped', 'Resend did not report quota usage on the send response.'); + const parts = [ + u.dailyUsed !== undefined ? `${u.dailyUsed} sent today` : null, + u.monthlyUsed !== undefined ? `${u.monthlyUsed} sent this month` : null, + ].filter((p): p is string => p !== null); + return result('quota', 'pass', `${parts.join(', ')}.`); +} + +function blockedBy(step: EmailStepId, prior: StepResult): StepResult { + return result(step, 'skipped', `Skipped because ${prior.step} did not pass.`); +} + +/** Run ONE step, re-deriving whatever it depends on. Never throws on provider failure. */ +export async function runEmailStep(target: EmailStepId, deps: EmailStepDeps): Promise { + if (target === 'delivery') return delivery(deps); + if (target === 'quota') return quota(deps); + + const provider = new ResendProvider({ apiKey: deps.apiKey }); + const valid = await keyValid(provider); + if (target === 'key_valid') return valid; + if (valid.status === 'fail') return blockedBy(target, valid); + + let listing = await provider.listDomains(); + if (!listing.ok && listing.status === 429) { await pause(); listing = await provider.listDomains(); } + const scope = keyScope(listing); + if (target === 'key_scope') return scope; + if (scope.status === 'fail') return blockedBy(target, scope); + + const domain = senderDomain(listing, deps.senderEmail); + if (target === 'sender_domain_verified') return domain; + if (domain.status === 'fail') return blockedBy(target, domain); + + return sendTest(provider, deps); +} diff --git a/server/lib/integration/email-test-store.ts b/server/lib/integration/email-test-store.ts new file mode 100644 index 000000000..631ddafd7 --- /dev/null +++ b/server/lib/integration/email-test-store.ts @@ -0,0 +1,61 @@ +/** + * Storage-facing helpers for the stepwise email test, kept apart from the pure + * step runner so the runner can be exercised with plain fakes. + */ +import { and, eq } from 'drizzle-orm'; +import type { drizzle } from 'drizzle-orm/d1'; +import { processedWebhookEvents, users } from '../db/schema'; +import type { DeliveryOutcome, LastSend } from './email-test-steps'; + +type Db = ReturnType; + +/** Long enough to run the delivery and quota steps after the send; short enough to expire unattended. */ +const LAST_SEND_TTL_S = 60 * 60; + +const lastSendKey = (tenantId: string, userId: string) => `email-test-last-send:${tenantId}:${userId}`; + +export async function readLastSend(kv: KVNamespace | undefined, tenantId: string, userId: string): Promise { + if (!kv) return null; + try { + const raw = (await kv.get(lastSendKey(tenantId, userId), { type: 'json' })) as LastSend | null; + return raw && typeof raw.messageId === 'string' ? raw : null; + } catch { + return null; + } +} + +export async function writeLastSend(kv: KVNamespace | undefined, tenantId: string, userId: string, send: LastSend): Promise { + if (!kv) return; + try { + await kv.put(lastSendKey(tenantId, userId), JSON.stringify(send), { expirationTtl: LAST_SEND_TTL_S }); + } catch { /* diagnostics only — a missing record just skips the later steps */ } +} + +export async function clearLastSend(kv: KVNamespace | undefined, tenantId: string, userId: string): Promise { + if (!kv) return; + try { await kv.delete(lastSendKey(tenantId, userId)); } catch { /* best effort */ } +} + +/** + * The signed-in user's own address. Looked up by the verified JWT subject within + * the verified tenant; no request field participates. + */ +export async function readSessionEmail(db: Db, tenantId: string, userId: string): Promise { + const row = await db.select({ email: users.email }).from(users) + .where(and(eq(users.id, userId), eq(users.tenantId, tenantId))).get(); + return row?.email?.trim() || null; +} + +/** + * Whether Resend has reported this message as delivered or bounced. The webhook + * receiver dedup-records every verified event under + * `:resend:::`, which is the + * only trace of a delivered event (delivered events are not stored elsewhere). + */ +export async function lookupResendDelivery(db: Db, tenantId: string, messageId: string): Promise { + const has = async (eventId: string) => !!(await db.select({ id: processedWebhookEvents.eventId }) + .from(processedWebhookEvents).where(eq(processedWebhookEvents.eventId, eventId)).get()); + if (await has(`${tenantId}:resend:bounced:${messageId}:email.bounced`)) return 'bounced'; + if (await has(`${tenantId}:resend:delivered:${messageId}:email.delivered`)) return 'delivered'; + return 'none'; +} diff --git a/server/lib/integration/owner-reply-to.ts b/server/lib/integration/owner-reply-to.ts new file mode 100644 index 000000000..94c68c171 --- /dev/null +++ b/server/lib/integration/owner-reply-to.ts @@ -0,0 +1,48 @@ +import { eq } from 'drizzle-orm'; +import type { DrizzleD1Database } from 'drizzle-orm/d1'; +import { tenantConfigs } from '../db/schema'; +import { logger } from '../logger'; + +/** + * Initialise `tenant_configs.reply_to` from the owner's email, once, when the + * owner account is created. + * + * Without a configured Reply-To a customer's reply to an outbound message goes + * to the platform sender address instead of the inspector. The owner's email is + * the sensible default; the operator can change it later in Settings. + * + * Initialize-only: a value that is already set (anything but NULL / empty / + * whitespace) is never touched. Callers invoke this only on the branch that + * CREATES the owner, so rotating a credential later cannot refill a field the + * operator cleared on purpose. + * + * Best-effort: the owner account already exists by the time this runs, and + * failing the whole provisioning command (which retries on the branch that no + * longer creates anything) would be worse than a blank field. The one-off + * `scripts/backfill-reply-to.mjs` covers any tenant this misses. + */ +export async function initReplyToFromOwner( + db: DrizzleD1Database, + tenantId: string, + ownerEmail: string, +): Promise { + const email = ownerEmail.trim(); + if (!email) return; + try { + const cfg = await db.select({ replyTo: tenantConfigs.replyTo }) + .from(tenantConfigs).where(eq(tenantConfigs.tenantId, tenantId)).get(); + const now = new Date(); + if (!cfg) { + await db.insert(tenantConfigs).values({ tenantId, replyTo: email, updatedAt: now }); + } else if (!cfg.replyTo || cfg.replyTo.trim() === '') { + await db.update(tenantConfigs) + .set({ replyTo: email, updatedAt: now }) + .where(eq(tenantConfigs.tenantId, tenantId)); + } + } catch (err) { + logger.warn('reply-to.init.failed', { + tenantId, + error: err instanceof Error ? err.message : String(err), + }); + } +} diff --git a/server/lib/integration/standalone.ts b/server/lib/integration/standalone.ts index c4410dadf..e6a2c8226 100644 --- a/server/lib/integration/standalone.ts +++ b/server/lib/integration/standalone.ts @@ -10,6 +10,7 @@ import { SQL_UUID_V4 } from './standalone-uuid'; import { seedDefaultAutomations } from './standalone-seed-automations'; import { SmsConsentService } from '../../services/sms-consent.service'; import { SMS_DISCLOSURE_V1 } from '../../services/automation/shared'; +import { initReplyToFromOwner } from './owner-reply-to'; // Default Comment Library entries seeded into every new tenant. The same set // is also backfilled into existing tenants by the default-comments seed @@ -305,6 +306,10 @@ export class StandaloneProvider implements IntegrationProvider { ]; await db.batch(statements as unknown as [BatchItem<'sqlite'>, ...BatchItem<'sqlite'>[]]); + // First owner exists: default the tenant's Reply-To to their + // email (initialize-only; a value set earlier is kept). + await initReplyToFromOwner(db, tenantId, adminEmail); + // Default Template — empty starter. Renamed from "Standard // Home Inspection" (R7-23) because it collided semantically // with the Marketplace template "Standard Residential diff --git a/server/lib/integration/stripe-test-steps.ts b/server/lib/integration/stripe-test-steps.ts new file mode 100644 index 000000000..4ca4b4d89 --- /dev/null +++ b/server/lib/integration/stripe-test-steps.ts @@ -0,0 +1,98 @@ +/** + * Stepwise "Test connection" for a workspace's own Stripe account. + * + * Three read-only steps, each proving one more thing: + * + * key_valid the stored secret key can read its own account + * account_capabilities charges_enabled / payouts_enabled / details_submitted + * webhook a verified delivery has actually arrived (from the + * per-tenant delivery log; Stripe's signature cannot be + * triggered from our side, so "arrived and verified" is + * the only observable proof) + * + * NOT implemented here, on purpose: any step that creates a payment. A test-mode + * charge-and-refund and a live-mode create-then-cancel intent both write to the + * workspace's Stripe account, and the live one leaves a record there. They need a + * confirmation flow (what will be created, in which mode, that it leaves a + * cancelled record) designed with the interface, so they are a separate piece of + * work and nothing in this module may be extended to create one silently. + * + * `detail` strings are fixed and non-sensitive: never the key or a response body. + */ +import type { StepResult, StepStatus } from './email-test-steps'; + +export type StripeStepId = 'key_valid' | 'account_capabilities' | 'webhook'; + +interface StripeAccountStatus { + accountName: string; + chargesEnabled: boolean; + payoutsEnabled: boolean; + detailsSubmitted: boolean; + /** From the key's prefix (the Account API does not expose it); absent when unknown. */ + livemode?: boolean; +} + +interface StripeWebhookDelivery { + ts: string; + eventType: string; + result: 'processed' | 'received' | 'signature_failed' | 'tenant_mismatch'; +} + +export interface StripeStepDeps { + /** Null when no secret key is stored. */ + fetchAccount: (() => Promise) | null; + webhookSecretConfigured: boolean; + /** Recent deliveries, newest first. */ + readDeliveries(): Promise; +} + +const result = (step: StripeStepId, status: StepStatus, detail: string): StepResult => ({ step, status, detail }); + +async function loadAccount(deps: StripeStepDeps): Promise { + if (!deps.fetchAccount) return null; + try { return await deps.fetchAccount(); } catch { return null; } +} + +function keyValid(account: StripeAccountStatus | null, deps: StripeStepDeps): StepResult { + if (!deps.fetchAccount) return result('key_valid', 'fail', 'No Stripe secret key is saved.'); + if (!account) return result('key_valid', 'fail', 'Stripe rejected the saved secret key, or could not be reached.'); + return result('key_valid', 'pass', `Connected to ${account.accountName}${account.livemode === undefined ? '' : account.livemode ? ' (live mode)' : ' (test mode)'}.`); +} + +function capabilities(account: StripeAccountStatus): StepResult { + const missing = [ + account.chargesEnabled ? null : 'charges are not enabled', + account.payoutsEnabled ? null : 'payouts are not enabled', + account.detailsSubmitted ? null : 'account details are not submitted', + ].filter((m): m is string => m !== null); + if (missing.length > 0) { + return result('account_capabilities', 'fail', `Stripe reports that ${missing.join(', ')}. Finish onboarding in the Stripe dashboard.`); + } + return result('account_capabilities', 'pass', 'Charges and payouts are enabled and account details are submitted.'); +} + +async function webhook(deps: StripeStepDeps): Promise { + if (!deps.webhookSecretConfigured) return result('webhook', 'fail', 'No webhook signing secret is saved.'); + const log = await deps.readDeliveries(); + const good = log.find((e) => e.result === 'processed' || e.result === 'received'); + if (good) return result('webhook', 'pass', `Last verified delivery: ${good.eventType} at ${good.ts}.`); + if (log.some((e) => e.result === 'signature_failed')) { + return result('webhook', 'fail', 'Deliveries are arriving but fail signature checks. Re-copy the signing secret from the Stripe dashboard.'); + } + if (log.length > 0) return result('webhook', 'fail', 'Deliveries are arriving for a different workspace than this one.'); + return result('webhook', 'skipped', 'No deliveries recorded yet. Send a test event from the Stripe dashboard, then run this step again.'); +} + +/** Run ONE step, re-deriving its prerequisites. Never throws on provider failure. */ +export async function runStripeStep(target: StripeStepId, deps: StripeStepDeps): Promise { + const account = await loadAccount(deps); + const valid = keyValid(account, deps); + if (target === 'key_valid') return valid; + if (valid.status === 'fail' || !account) return result(target, 'skipped', 'Skipped because key_valid did not pass.'); + + const caps = capabilities(account); + if (target === 'account_capabilities') return caps; + if (caps.status === 'fail') return result(target, 'skipped', 'Skipped because account_capabilities did not pass.'); + + return webhook(deps); +} diff --git a/server/lib/mcp/openapi-snapshot.json b/server/lib/mcp/openapi-snapshot.json index c0d0582e1..a98c581a4 100644 --- a/server/lib/mcp/openapi-snapshot.json +++ b/server/lib/mcp/openapi-snapshot.json @@ -18230,6 +18230,42 @@ "summary": "Rotate the tenant e-signature signing key", "description": "Retire the tenant's current Ed25519 e-signature key and mint a replacement. Previously signed agreements and report versions keep verifying: the retired public key is kept forever and every audit row is checked against the key its own fingerprint names. Nothing is re-signed and no existing evidence is modified." }, + { + "operationId": "runEmailTestStep", + "method": "POST", + "pathTemplate": "/api/integrations/test-steps/email", + "scopes": [ + "admin" + ], + "tag": "integrations", + "tier": "extended", + "inputSchema": { + "parameters": [], + "body": { + "$ref": "#/components/schemas/EmailTestStepBody" + } + }, + "summary": "Run one step of the stepwise email connection test", + "description": "Runs one step against the stored email provider credentials. The test email goes to the signed-in user's own address only; the request carries no recipient. Non-Resend providers support only key_valid." + }, + { + "operationId": "runStripeTestStep", + "method": "POST", + "pathTemplate": "/api/integrations/test-steps/stripe", + "scopes": [ + "admin" + ], + "tag": "integrations", + "tier": "extended", + "inputSchema": { + "parameters": [], + "body": { + "$ref": "#/components/schemas/StripeTestStepBody" + } + }, + "summary": "Run one step of the stepwise Stripe connection test", + "description": "Read-only checks against the stored Stripe key: account reachable, charges/payouts enabled, and a verified webhook delivery seen. Creates nothing in the Stripe account." + }, { "operationId": "saveAgentNotificationPreference", "method": "PUT", diff --git a/server/lib/middleware/bot-protection.ts b/server/lib/middleware/bot-protection.ts index 21369c634..54090e7cb 100644 --- a/server/lib/middleware/bot-protection.ts +++ b/server/lib/middleware/bot-protection.ts @@ -91,15 +91,48 @@ export function resolveTurnstileSiteKey( * reaching that state means the caller ignored it. */ export async function verifyTurnstile(token: string, secretKey: string): Promise { + const res = await postSiteverify(token, secretKey); + if (!res.ok) return false; + const data = await res.json() as { success: boolean }; + return data.success; +} + +const SITEVERIFY_TIMEOUT_MS = 5000; + +/** The one siteverify request. `idempotencyKey` makes a retry of the same check return the same answer. */ +function postSiteverify(token: string, secretKey: string, idempotencyKey?: string, timeoutMs?: number): Promise { if (!secretKey) throw new Error('TURNSTILE_SECRET_KEY is not configured'); const body = new FormData(); body.append('secret', secretKey); body.append('response', token); - const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { + if (idempotencyKey) body.append('idempotency_key', idempotencyKey); + return fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body, + ...(timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {}), }); - if (!res.ok) return false; - const data = await res.json() as { success: boolean }; - return data.success; +} + +/** + * Like `verifyTurnstile`, but tells a definite "no" from "could not tell": a + * throttled or failing siteverify (429, 5xx), a timeout or a network error is + * `'error'`, so a caller can retry instead of treating a Cloudflare hiccup as a + * bot. Pass the same `idempotencyKey` on the retry: if the first request reached + * Cloudflare and only its answer was lost, the retry returns that same answer + * instead of "already redeemed". An empty secret still throws. + */ +export async function verifyTurnstileOutcome( + token: string, + secretKey: string, + idempotencyKey?: string, +): Promise<'yes' | 'no' | 'error'> { + try { + const res = await postSiteverify(token, secretKey, idempotencyKey, SITEVERIFY_TIMEOUT_MS); + if (res.status === 429 || res.status >= 500) return 'error'; + if (!res.ok) return 'no'; + return ((await res.json()) as { success: boolean }).success ? 'yes' : 'no'; + } catch (err) { + if (!secretKey) throw err; + return 'error'; + } } diff --git a/server/lib/middleware/di.ts b/server/lib/middleware/di.ts index 733412c02..fc9fe5790 100644 --- a/server/lib/middleware/di.ts +++ b/server/lib/middleware/di.ts @@ -16,6 +16,7 @@ import { LegalVersionService } from '../../services/legal-version.service'; import { drizzle } from 'drizzle-orm/d1'; import { assembleTenantEmailService, loadTenantEmailConfig, type LoadedEmailConfig } from '../email/build-email-service'; import { createTenantEmailResolver } from '../email/tenant-email-resolver'; +import { buildHumanVerifier } from '../email/human-verifier'; import { InspectionService } from '../../services/inspection.service'; import type { ImagesBinding } from '../media/strip-exif'; import { PortalService } from '../../services/portal.service'; @@ -114,7 +115,9 @@ export async function diMiddleware(c: Context, next: Next) { // One place decides own-vs-platform Resend + branded renderer, shared with // non-request contexts (workflows/scheduled) via assembleTenantEmailService. - const buildEmailService = () => assembleTenantEmailService(c.env, emailCfg, c.get('tenantId'), buildPlanQuota(), tenantTierForQuota); + const humanVerified = buildHumanVerifier(c); + const emailOptions = humanVerified ? { humanVerified } : {}; + const buildEmailService = () => assembleTenantEmailService(c.env, emailCfg, c.get('tenantId'), buildPlanQuota(), tenantTierForQuota, emailOptions); // Build the core->portal outbox sink, gated on the SYNC_QUEUE producer // binding — the transport itself. No queue → no sink → append() no-ops: @@ -238,6 +241,7 @@ export async function diMiddleware(c: Context, next: Next) { Object.assign(target, createTenantEmailResolver({ env: c.env, loadedTenantId: emailCfgFor, loadedService: () => (target.email ??= buildEmailService()), fixedTenantId: c.var.profile.fixedTenantId, hasUsageQuota: c.var.profile.hasUsageQuota, buildPlanQuota, + ...(humanVerified ? { humanVerified } : {}), })); break; case 'inspection': diff --git a/server/lib/rate-limit.ts b/server/lib/rate-limit.ts index 443b442b8..faef0d870 100644 --- a/server/lib/rate-limit.ts +++ b/server/lib/rate-limit.ts @@ -6,15 +6,17 @@ import { Errors } from './errors'; * Check the Cloudflare Rate Limiter for the given prefix + CF-Connecting-IP key. * No-ops when RATE_LIMITER binding is absent (local dev). * Throws RateLimited if the limit is exceeded. + * `scope` replaces the client IP in the key (e.g. a tenant id) so a limit can be + * shared by everyone acting for one workspace. */ -export async function checkRateLimit(c: Context, prefix: string): Promise { +export async function checkRateLimit(c: Context, prefix: string, scope?: string): Promise { if (!c.env.RATE_LIMITER) return; // Test/dev escape hatch (defaults to enforced): the seeded E2E suite drives // ~140 specs' logins from a single runner IP, which blows past the 10/60s // login limiter and 429s beforeAll hooks flakily. Only the ephemeral E2E // .dev.vars sets this; production and self-host never do. if (c.env.DISABLE_RATE_LIMIT === '1') return; - const ip = c.req.header('CF-Connecting-IP') ?? 'unknown'; - const { success } = await c.env.RATE_LIMITER.limit({ key: `${prefix}:${ip}` }); + const who = scope ?? c.req.header('CF-Connecting-IP') ?? 'unknown'; + const { success } = await c.env.RATE_LIMITER.limit({ key: `${prefix}:${who}` }); if (!success) throw Errors.RateLimited(); } diff --git a/server/lib/request-scope.ts b/server/lib/request-scope.ts index c984ca070..19c5e4d49 100644 --- a/server/lib/request-scope.ts +++ b/server/lib/request-scope.ts @@ -49,3 +49,13 @@ export function memoOnce(env: unknown, key: string, fn: () => Promise): Pr const scope = (env as Record | undefined)?.[REQUEST_SCOPE] as RequestScope | undefined; return scope ? scope.once(key, fn) : fn(); } + +/** + * True when this request reached the API through the worker's in-process + * self-binding (the web app's own loaders and actions), false for external HTTP. + * Unforgeable for the same reason the scope is: only `workers/app.ts` puts the + * key on the env, and external traffic is handed the plain env. + */ +export function isInProcessRequest(env: unknown): boolean { + return !!(env as Record | undefined)?.[REQUEST_SCOPE]; +} diff --git a/server/lib/validations/auth.schema.ts b/server/lib/validations/auth.schema.ts index 7002f6400..af389d514 100644 --- a/server/lib/validations/auth.schema.ts +++ b/server/lib/validations/auth.schema.ts @@ -16,7 +16,29 @@ export const LoginSchema = z.object({ }); /** - * Validation schema for the login response. + * Response of `POST /api/auth/login` — the password step only. + * + * Two shapes: a session (`redirect`, plus `token` for the in-process BFF that + * sends `x-token-relay: 1`), or a second-factor challenge when the account has + * TOTP enabled. Join and setup keep `AuthResponseSchema`: they never put the + * JWT in the body, so declaring `token` there would document a field nothing + * sends. + */ +export const LoginResponseSchema = createApiResponseSchema(z.union([ + z.object({ + redirect: z.string().openapi({ example: '/inspections' }).describe('Path the client navigates to once the session cookie is set.'), + token: z.string().optional().describe('Session JWT. Present ONLY for the in-process BFF caller that sends `x-token-relay: 1`; browsers receive the session as the HttpOnly cookie and never in the body.'), + }), + z.object({ + requires2fa: z.literal(true).describe('The password was correct and the account has TOTP enabled; no session has been granted yet.'), + challengeToken: z.string().describe('Five-minute challenge to post back with the TOTP code to `POST /api/auth/login/2fa`.'), + }), +])); + +/** + * Response of `POST /api/auth/join` and `POST /api/auth/setup`. The session is + * delivered only as the HttpOnly cookie; neither route returns the JWT in the + * body, whatever headers the caller sends. */ export const AuthResponseSchema = createApiResponseSchema( z.object({ @@ -120,7 +142,10 @@ export const TotpSetupResponseSchema = createApiResponseSchema(z.object({ })); export const Login2faResponseSchema = createApiResponseSchema(z.union([ - z.object({ redirect: z.string().describe('TODO describe redirect field for the OpenInspection MCP integration') }), + z.object({ + redirect: z.string().describe('TODO describe redirect field for the OpenInspection MCP integration'), + token: z.string().optional().describe('Session JWT. Present ONLY for the in-process BFF caller that sends `x-token-relay: 1`; browsers receive the session as the HttpOnly cookie and never in the body.'), + }), z.object({ requires2fa: z.literal(true).describe('TODO describe requires2fa field for the OpenInspection MCP integration'), challengeToken: z.string().describe('TODO describe challengeToken field for the OpenInspection MCP integration') }), ])); diff --git a/server/lib/validations/integrations.schema.ts b/server/lib/validations/integrations.schema.ts index 5ed5a96b8..2528f479d 100644 --- a/server/lib/validations/integrations.schema.ts +++ b/server/lib/validations/integrations.schema.ts @@ -80,3 +80,31 @@ export const AiConnectionTestResultSchema = z message: z.string().optional().describe('What to show. Never contains the provider response body.'), }) .openapi('AiConnectionTestResult'); + +/** + * Request body for POST /test-steps/email and /test-steps/stripe: which ONE step + * to run. Deliberately nothing else — in particular no recipient. Unknown keys + * are stripped by the default object behaviour, never read. + */ +export const EmailTestStepBodySchema = z + .object({ + step: z.enum(['key_valid', 'key_scope', 'sender_domain_verified', 'send_test', 'delivery', 'quota']) + .describe('Which step to run. Earlier steps are re-checked server-side, not trusted from the client.'), + }) + .openapi('EmailTestStepBody'); + +export const StripeTestStepBodySchema = z + .object({ + step: z.enum(['key_valid', 'account_capabilities', 'webhook']).describe('Which step to run.'), + }) + .openapi('StripeTestStepBody'); + +/** One step outcome. `detail` is short, fixed and non-sensitive. */ +export const TestStepResultSchema = z + .object({ + step: z.string().describe('Step id.'), + status: z.enum(['pass', 'fail', 'skipped', 'pending']), + detail: z.string().describe('Short non-sensitive explanation; on a failure, the reason.'), + provider: z.string().nullable().describe('Email provider the test ran against; null for Stripe.'), + }) + .openapi('TestStepResult'); diff --git a/server/portal/admin-credential.ts b/server/portal/admin-credential.ts index 4b8a13003..c59885adf 100644 --- a/server/portal/admin-credential.ts +++ b/server/portal/admin-credential.ts @@ -3,6 +3,7 @@ import { eq, and, lt } from 'drizzle-orm'; import type { BatchItem } from 'drizzle-orm/batch'; import { users, tenants } from '../lib/db/schema'; import { deriveAuthorityBasis } from '../lib/auth/authority-basis'; +import { initReplyToFromOwner } from '../lib/integration/owner-reply-to'; import { buildAcceptanceStatement, type CapturedAcceptance } from '../services/legal/account-acceptance'; /** @@ -110,6 +111,11 @@ export async function applyAdminCredential( // buildAcceptanceStatement threw above rather than return zero // statements), which the compiler cannot see through the spread. await db.batch(statements as unknown as [BatchItem<'sqlite'>, ...BatchItem<'sqlite'>[]]); + // Only here, on the branch that CREATES the owner: the Reply-To default + // is a provisioning-time fact, not something to re-assert on every + // credential rotation. Placed after the batch so the atomic + // account+acceptance write is untouched by a config failure. + await initReplyToFromOwner(db, p.tenantId, p.adminEmail); } else { await db.update(users) .set({ diff --git a/server/services/automation/deliver-email.ts b/server/services/automation/deliver-email.ts index 1f4b6e16c..24c82bef1 100644 --- a/server/services/automation/deliver-email.ts +++ b/server/services/automation/deliver-email.ts @@ -13,6 +13,9 @@ import { COOLING_WINDOW_SENTINEL, COOLING_WINDOW_DEFER_REASON, coolingWindowSentinelFor, coolingWindowUnlockAtMs, deferUntilCoolingWindowOpens, } from './cooling-window'; +import { + PROVIDER_DEFERRAL_SENTINEL, providerOutcomeFor, applyProviderOutcome, type ProviderOutcome, +} from './provider-deferral'; import type { FlushInspection } from './shared'; import type { EmailService } from '../email.service'; import type { automations, automationLogs as automationLogsTable, tenants } from '../../lib/db/schema'; @@ -188,6 +191,9 @@ export async function deliverTemplatedEmail( // which is what the log row is re-scheduled to. Scoped to this one // delivery, written on exactly one branch, read on exactly one. let coolingUnlockAtMs: number | null = null; + // Same idea for a provider refusal that undoes itself (rate limit, daily + // quota) or is final for the month: see ./provider-deferral. + let providerOutcome: ProviderOutcome | null = null; const transport = { sendEmail: async (a: { to: string; subject: string; html: string }) => { // The rules layer names what it is sending, like every other @@ -207,7 +213,11 @@ export async function deliverTemplatedEmail( // The cooling window DECLINED; it did not break. Anything else // is a real fault and must keep propagating as one. const sentinel = coolingWindowSentinelFor(err); - if (!sentinel) throw err; + if (!sentinel) { + providerOutcome = providerOutcomeFor(err, Date.now()); + if (!providerOutcome) throw err; + return { ok: false as const, error: PROVIDER_DEFERRAL_SENTINEL }; + } coolingUnlockAtMs = coolingWindowUnlockAtMs(err); return { ok: false as const, error: sentinel }; } @@ -248,6 +258,10 @@ export async function deliverTemplatedEmail( } return; } + if (row.status === 'failed' && row.error === PROVIDER_DEFERRAL_SENTINEL && providerOutcome) { + await applyProviderOutcome(db, log.id, providerOutcome); + return; + } if (row.status === 'sent') { await db.update(automationLogs).set({ status: 'sent', diff --git a/server/services/automation/delivery.ts b/server/services/automation/delivery.ts index 6674ff323..36c5bed93 100644 --- a/server/services/automation/delivery.ts +++ b/server/services/automation/delivery.ts @@ -11,6 +11,7 @@ import type { ManagedSendGateEnv } from '../../lib/sms/managed-send-gate'; import type { PlanQuotaGuard } from '../../features/plan-quota/guard'; import { deliverReportEmail, type ReportDeliveryDeps } from './report-email'; import { deliverTemplatedEmail } from './deliver-email'; +import { createSendThrottle, type SendThrottle } from './send-throttle'; /** * The flush query's SELECT projection. `inspection` is narrowed to the @@ -72,7 +73,7 @@ export const FLUSH_SELECTION = { export function AutomationDelivery>(Base: TBase) { return class extends Base { async flush( - emailFor: (tenantId: string) => Promise, + emailFor: (tenantId: string, options?: { humanVerified?: boolean }) => Promise, appName: string, appBaseUrl: string, sms?: SmsRuntime, batchSize = 50, @@ -86,6 +87,9 @@ export function AutomationDelivery { const db = this.getDrizzle(); const now = new Date(); @@ -247,9 +251,14 @@ export function AutomationDelivery { const cache = new Map(); return { - async getOrBuild(tenantId: string, factory: (tid: string) => Promise): Promise { - let svc = cache.get(tenantId); - if (!svc) { svc = await factory(tenantId); cache.set(tenantId, svc); } + async getOrBuild( + tenantId: string, + factory: (tid: string, options?: { humanVerified?: boolean }) => Promise, + humanVerified = false, + ): Promise { + const key = humanVerified ? `${tenantId}#verified` : tenantId; + let svc = cache.get(key); + if (!svc) { svc = await factory(tenantId, humanVerified ? { humanVerified } : undefined); cache.set(key, svc); } return svc; }, }; @@ -261,6 +270,9 @@ export function AutomationDelivery>(); + // Email sends in this batch start at most N per second (wall time only). + const throttle = flushDeps?.throttle ?? createSendThrottle(); + // report.amended templates carry the re-publish change note // ({{summary}}) from the latest report_versions row. Fetch once per // inspection per flush; only amended emails need it, so the query is @@ -345,7 +357,8 @@ export function AutomationDelivery>, + tenantId: string, + inspectionId: string, + now: number = Date.now(), +): Promise { + await db.update(automationLogs) + .set({ humanVerifiedAt: new Date(now) }) + .where(and( + eq(automationLogs.tenantId, tenantId), + eq(automationLogs.inspectionId, inspectionId), + eq(automationLogs.channel, 'email'), + eq(automationLogs.status, 'pending'), + // The rows a publish queues: this tenant's report.published and + // report.amended rules. Reminders or follow-ups from other rules are not + // vouched for. This is per inspection, not per report: on an order with + // two deliverables it also covers the other report's pending rows. + inArray(automationLogs.automationId, db.select({ id: automations.id }).from(automations).where(and( + eq(automations.tenantId, tenantId), + inArray(automations.trigger, ['report.published', 'report.amended']), + ))), + isNull(automationLogs.humanVerifiedAt), + )); +} + +/** The route-facing form: only when the request's token verifies, and never fatal. */ +export async function stampIfHumanVerified( + humanVerified: HumanVerifier | undefined, + db: DrizzleD1Database>, + tenantId: string, + inspectionId: string, +): Promise { + if (!humanVerified || !(await humanVerified())) return; + await stampHumanVerifiedLogs(db, tenantId, inspectionId).catch((e: unknown) => + logger.warn('human-verified stamp failed (non-fatal)', { inspectionId, error: String(e) })); +} diff --git a/server/services/automation/logs.ts b/server/services/automation/logs.ts index a7c10f3cf..eb775903c 100644 --- a/server/services/automation/logs.ts +++ b/server/services/automation/logs.ts @@ -1,6 +1,7 @@ -import { eq, and, or, sql, desc, lte } from 'drizzle-orm'; +import { eq, and, or, sql, desc, lte, inArray } from 'drizzle-orm'; import { automationLogs, automations, contactRoleProfiles } from '../../lib/db/schema'; import { COOLING_WINDOW_DEFER_REASON } from './cooling-window'; +import { RATE_LIMIT_DEFER_REASON, DAILY_QUOTA_DEFER_REASON } from './provider-deferral'; import type { Constructor } from './shared'; import type { AutomationBase } from './shared'; @@ -105,7 +106,9 @@ export function AutomationLogs>(Base: lte(automationLogs.sendAt, new Date(now)), and( eq(automationLogs.status, 'pending'), - eq(automationLogs.error, COOLING_WINDOW_DEFER_REASON), + // Held by the email provider's own limits: same shape, a + // future send_at that is a platform clock, not the operator's. + inArray(automationLogs.error, [COOLING_WINDOW_DEFER_REASON, RATE_LIMIT_DEFER_REASON, DAILY_QUOTA_DEFER_REASON]), ), ), )) diff --git a/server/services/automation/provider-deferral.ts b/server/services/automation/provider-deferral.ts new file mode 100644 index 000000000..4285e47ed --- /dev/null +++ b/server/services/automation/provider-deferral.ts @@ -0,0 +1,102 @@ +import { eq } from 'drizzle-orm'; +import { automationLogs } from '../../lib/db/schema'; +import { AppError, ErrorCode } from '../../lib/errors'; +import { logger } from '../../lib/logger'; + +/** + * What the automation ledger does when the email PROVIDER pushes back. + * + * Three refusals look alike and want different treatment: + * + * - rate limit — heals in seconds. The row stays PENDING with `send_at` + * moved by the provider's own `retry-after`. + * - daily quota — heals at the next UTC midnight. Same shape: PENDING, + * `send_at` moved to that instant. + * - monthly quota — does not heal in any useful time. A real failure, with a + * reason a person can act on. + * + * The first two reuse the "stay pending and push send_at" mechanism the + * outbound cooling window already established (./cooling-window): flush() needs + * no new concept to deliver the row later, and nothing in this repository moves + * a terminal row back to pending, so spending `failed` on a message that is + * still owed would lose it for good. + * + * A quota refusal whose window the adapter could not name is NOT handled here: + * it keeps the old terminal behaviour rather than being guessed into a retry. + */ + +/** Sentinel the email transport adapter returns so the log writer can tell this + * outcome from a fault without carrying the error object across the boundary. */ +export const PROVIDER_DEFERRAL_SENTINEL = '__provider_deferral__'; + +export const RATE_LIMIT_DEFER_REASON = + 'waiting — email provider rate limit, retrying shortly'; +export const DAILY_QUOTA_DEFER_REASON = + 'waiting — daily email sending limit reached, retrying after the next UTC midnight'; +export const MONTHLY_QUOTA_FAIL_REASON = + 'monthly email sending limit reached at the email provider; this message was not sent'; + +/** Pause used when the provider rate-limited us without saying for how long. */ +const DEFAULT_RATE_LIMIT_PAUSE_MS = 60_000; +/** Floor, so a `retry-after: 0` cannot make the row due again within the same batch. */ +const MIN_PAUSE_MS = 1_000; + +export type ProviderOutcome = + | { action: 'defer'; sendAtMs: number; reason: string } + | { action: 'fail'; reason: string }; + +/** The start of the UTC day after the one `nowMs` falls in. */ +export function nextUtcMidnightMs(nowMs: number): number { + const d = new Date(nowMs); + return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1); +} + +function detailsOf(err: unknown): Record { + const d = err instanceof AppError ? err.details : undefined; + return typeof d === 'object' && d !== null ? d as Record : {}; +} + +/** + * Decide what a provider refusal means for the log row, or null when this is not + * one of the refusals handled here (so every other error keeps reading as the + * failure it is). + */ +export function providerOutcomeFor(err: unknown, nowMs: number): ProviderOutcome | null { + if (!(err instanceof AppError)) return null; + const details = detailsOf(err); + if (err.code === ErrorCode.EMAIL_RATE_LIMITED) { + const secs = details.retryAfterSeconds; + const pause = typeof secs === 'number' && Number.isFinite(secs) + ? Math.max(secs * 1000, MIN_PAUSE_MS) + : DEFAULT_RATE_LIMIT_PAUSE_MS; + return { action: 'defer', sendAtMs: nowMs + pause, reason: RATE_LIMIT_DEFER_REASON }; + } + if (err.code === ErrorCode.QUOTA_EXHAUSTED && details.metric === 'email') { + if (details.quotaWindow === 'daily') { + return { action: 'defer', sendAtMs: nextUtcMidnightMs(nowMs), reason: DAILY_QUOTA_DEFER_REASON }; + } + if (details.quotaWindow === 'monthly') return { action: 'fail', reason: MONTHLY_QUOTA_FAIL_REASON }; + } + return null; +} + +/** Write the outcome onto the log row: pending with a later send_at, or failed with the reason. */ +export async function applyProviderOutcome( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + db: any, + logId: string, + outcome: ProviderOutcome, +): Promise { + if (outcome.action === 'defer') { + await db.update(automationLogs) + .set({ status: 'pending', error: outcome.reason, sendAt: new Date(outcome.sendAtMs) }) + .where(eq(automationLogs.id, logId)); + // NO recipient/PII — the fact and the instant only. + logger.info('[automation] send deferred after an email provider refusal', { logId, sendAtMs: outcome.sendAtMs }); + return; + } + await db.update(automationLogs) + .set({ status: 'failed', error: outcome.reason }) + .where(eq(automationLogs.id, logId)); + logger.warn('[automation] send failed: provider quota exhausted for the month', { logId }); +} diff --git a/server/services/automation/report-email.ts b/server/services/automation/report-email.ts index 6d6d8b4c3..18182cb66 100644 --- a/server/services/automation/report-email.ts +++ b/server/services/automation/report-email.ts @@ -11,6 +11,7 @@ import { createOiTemplateStore } from './template-store'; import { createRecipientLocaleResolver } from '../../lib/i18n/recipient-locale'; import { automationClassId } from '../../lib/notifications/automation-classes'; import { coolingWindowUnlockAtMs, deferUntilCoolingWindowOpens } from './cooling-window'; +import { providerOutcomeFor, applyProviderOutcome } from './provider-deferral'; import { reportDeliverySystemBlocks } from '../../lib/email-templates/renderer'; import { readReportViewCountingEnabled } from '../../lib/report-views'; import type { FlushInspection } from './shared'; @@ -222,7 +223,8 @@ export async function deliverReportEmail( // provider request is built, so the retry raises the identical // error, logs a second scary line about a PDF that was fine, // and arrives at the same outer catch. Hand it straight up. - if (coolingWindowUnlockAtMs(err) !== null) throw err; + // Same for a provider refusal: the text-only retry would hit it again. + if (coolingWindowUnlockAtMs(err) !== null || providerOutcomeFor(err, Date.now())) throw err; logger.error('AutomationService.flush: report PDF email send failed; falling back to text-only email', { inspectionId: inspection.id, logId: log.id }, err instanceof Error ? err : undefined); delivered = await emailSvc.sendReportReady(log.recipient, address, linkUrl, undefined, reportDelivery.renderHost); @@ -256,6 +258,13 @@ export async function deliverReportEmail( await deferUntilCoolingWindowOpens(db, log.id, unlockAtMs); return; } + // A provider rate limit / daily quota keeps the row pending (a monthly + // quota fails it with a readable reason) — see ./provider-deferral. + const providerOutcome = providerOutcomeFor(err, Date.now()); + if (providerOutcome) { + await applyProviderOutcome(db, log.id, providerOutcome); + return; + } await db.update(automationLogs) .set({ status: 'failed', error: err instanceof Error ? err.message.slice(0, 500) : 'Unknown error' }) .where(eq(automationLogs.id, log.id)); diff --git a/server/services/automation/send-throttle.ts b/server/services/automation/send-throttle.ts new file mode 100644 index 000000000..3f85ad5c0 --- /dev/null +++ b/server/services/automation/send-throttle.ts @@ -0,0 +1,42 @@ +/** + * Keeps a flush batch under a provider's request-rate limit. + * + * `beforeSend()` resolves immediately while fewer than `maxPerSecond` sends + * have started in the last second, and otherwise waits until the oldest of + * them is a full second old. A sliding window, not fixed spacing, so a quiet + * batch is never slowed and a burst is cut at exactly the limit. The throttle + * only adds wall-clock time: waiting on a timer costs no CPU. + * + * Clock and sleep are injected so tests assert the schedule without waiting. + */ +export interface SendThrottle { + beforeSend(): Promise; +} + +export interface SendThrottleOptions { + maxPerSecond?: number; + now?: () => number; + sleep?: (ms: number) => Promise; +} + +/** Sends per second a flush batch may start. Half of a typical provider limit, to leave room for other senders. */ +const FLUSH_MAX_SENDS_PER_SECOND = 5; + +const timerSleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); + +export function createSendThrottle(opts: SendThrottleOptions = {}): SendThrottle { + const max = Math.max(1, opts.maxPerSecond ?? FLUSH_MAX_SENDS_PER_SECOND); + const now = opts.now ?? Date.now; + const sleep = opts.sleep ?? timerSleep; + const starts: number[] = []; + return { + async beforeSend() { + if (starts.length >= max) { + const wait = starts[starts.length - max]! + 1000 - now(); + if (wait > 0) await sleep(wait); + } + starts.push(now()); + if (starts.length > max) starts.splice(0, starts.length - max); + }, + }; +} diff --git a/server/services/email/base.ts b/server/services/email/base.ts index f109adb7e..d910479c7 100644 --- a/server/services/email/base.ts +++ b/server/services/email/base.ts @@ -1,4 +1,6 @@ import { AppError, Errors, ErrorCode } from '../../lib/errors'; +import { providerFailureError } from '../../lib/email/provider-failure'; +import { withRateLimitRetry } from '../../lib/email/rate-limit-retry'; import { logger } from '../../lib/logger'; import { buildIcs, type IcsEvent } from '../../lib/ics'; import { inspectorSignature, type SignatureUser } from '../../lib/inspector-signature'; @@ -94,7 +96,8 @@ export class EmailBaseService { */ public unsubscribeLinks?: UnsubscribeLinkPort, ) { - this.provider = provider ?? new ResendProvider({ apiKey: this.apiKey }); + // A short provider rate limit is absorbed once here (see rate-limit-retry.ts). + this.provider = withRateLimitRetry(provider ?? new ResendProvider({ apiKey: this.apiKey })); } /** Render `trigger` via the template registry when a renderer is injected; @@ -368,7 +371,7 @@ export class EmailBaseService { }, to, html, this.unsubscribeLinks, opts?.classId); if (!result.ok) { - const { error: providerError, kind } = result; + const { kind } = result; if (kind === 'suppressed') { // Write back to the local suppression table so the send-path gate @@ -382,26 +385,9 @@ export class EmailBaseService { throw Errors.EmailRecipientSuppressed({ recipient: to.join(', ') }); } - if (kind === 'quota_exceeded') { - // Re-use the existing QuotaExhausted shape. Provider-level quota - // (no cap/used numbers available), but the code lets the UI - // surface the upgrade path. - logger.error('[email] Provider quota exceeded', { error: providerError }); - throw new AppError(402, ErrorCode.QUOTA_EXHAUSTED, - 'Your email sending quota is exhausted. Upgrade your plan or wait for the quota to reset.', - { metric: 'email', used: 0, cap: 0, billingPortalUrl: null }, - ); - } - - if (kind === 'sender_domain') { - // Operator configuration problem — the from-domain is not verified. - logger.error('[email] Sender domain error', { error: providerError }); - throw Errors.EmailSenderDomain({ hint: providerError }); - } - - // Transient / unclassified: generic delivery failure. - logger.error('[email] Delivery failed', { kind, error: providerError }); - throw new AppError(502, ErrorCode.SERVICE_UNAVAILABLE, 'Email delivery failed. Please try again.'); + // Quota, rate limit, sender domain and the generic fault — see + // provider-failure.ts for the status each one maps to. + throw providerFailureError(result); } // success — meter the send (best-effort; never blocks or breaks delivery). diff --git a/server/services/stripe.service.ts b/server/services/stripe.service.ts index bd766e775..f0c38ca75 100644 --- a/server/services/stripe.service.ts +++ b/server/services/stripe.service.ts @@ -117,4 +117,15 @@ export class StripeService { accountName: acct.settings?.dashboard?.display_name || acct.email || acct.id, }; } + + /** The own-account capability flags the "Test connection" steps report on. */ + async getAccountStatus(): Promise<{ accountName: string; chargesEnabled: boolean; payoutsEnabled: boolean; detailsSubmitted: boolean }> { + const acct = await this.stripe.accounts.retrieve(null); + return { + accountName: acct.settings?.dashboard?.display_name || acct.email || acct.id, + chargesEnabled: acct.charges_enabled === true, + payoutsEnabled: acct.payouts_enabled === true, + detailsSubmitted: acct.details_submitted === true, + }; + } } diff --git a/server/types/hono.ts b/server/types/hono.ts index 6fab12457..b6912ec11 100644 --- a/server/types/hono.ts +++ b/server/types/hono.ts @@ -302,6 +302,8 @@ type AppVariables = AuthVariables & { /** The platform person behind this request, if any. Written only from signed * material — the M2M header (seam guard) or a session claim (jwtAuth). */ platformActor?: import('../lib/m2m-auth').PlatformActor; + /** Present when the request carried a Turnstile token; resolves true once it verifies. */ + humanVerified?: import('../lib/email/outbound-cooling-window').HumanVerifier; }; /** diff --git a/tests/e2e/auth-two-factor.spec.ts b/tests/e2e/auth-two-factor.spec.ts index 8adb6502a..bd7882c65 100644 --- a/tests/e2e/auth-two-factor.spec.ts +++ b/tests/e2e/auth-two-factor.spec.ts @@ -22,7 +22,7 @@ * out of the same account. */ import { test, expect } from '@playwright/test'; -import type { APIRequestContext } from '@playwright/test'; +import type { APIRequestContext, Page } from '@playwright/test'; import { TOTP, Secret } from 'otpauth'; import { csrfHeaders } from './helpers/csrf'; @@ -109,6 +109,31 @@ async function post(request: APIRequestContext, path: string, data: unknown, tok const sessionFrom = (res: { headers(): Record }) => (res.headers()['set-cookie'] ?? '').match(/__Host-inspector_token=([^;]+)/)?.[1] ?? ''; +/** + * Sign in through the /login PAGE, as a person does, up to the point where the + * page either lands on /inspections or asks for a code. + * + * The API-level tests below cannot see the BFF hop: the page calls the API + * in-process and has to receive the JWT in the body to open a session, which a + * Playwright `request` never exercises. A correct code that the API accepted + * still failed on this page for exactly that reason. + */ +async function submitPassword(page: Page): Promise { + await page.goto(`${BASE_URL}/login`, { waitUntil: 'networkidle' }); + await page.fill('input[name=email]', EMAIL); + await page.fill('input[name=password]', PASSWORD); + await page.click('button[type=submit]'); +} + +/** Landed AND authenticated: a reload of /inspections must not bounce to /login. */ +async function expectSignedIn(page: Page): Promise { + await page.waitForURL('**/inspections', { timeout: 15_000 }); + const names = (await page.context().cookies()).map((c) => c.name); + expect(names, 'the BFF opened a session').toContain('__session'); + await page.reload({ waitUntil: 'networkidle' }); + expect(new URL(page.url()).pathname, 'still signed in after a reload').toBe('/inspections'); +} + test.describe.configure({ mode: 'serial' }); test.describe('two-factor sign-in', () => { @@ -116,6 +141,13 @@ test.describe('two-factor sign-in', () => { let recoveryCodes: string[] = []; let session = ''; + test('CONTROL — the login page signs in a password-only account', async ({ page }) => { + // Same page, same account, same run, before 2FA is on: proves the + // page-level step below CAN pass, so a red there is the second factor. + await submitPassword(page); + await expectSignedIn(page); + }); + test('enrolling issues a secret, a QR and eight recovery codes', async ({ request }) => { const login = await post(request, '/api/auth/login', { email: EMAIL, password: PASSWORD }); expect(login.status(), 'password-only login before 2FA').toBe(200); @@ -162,6 +194,15 @@ test.describe('two-factor sign-in', () => { expect(sessionFrom(done), 'a real session cookie').not.toBe(''); }); + test('the login page completes a sign-in with a TOTP code', async ({ page }) => { + await submitPassword(page); + const code = page.locator('input[name=code]'); + await expect(code, 'the page asks for the second factor').toBeVisible({ timeout: 15_000 }); + await code.fill(codeFor(secret)); + await page.click('button[type=submit]'); + await expectSignedIn(page); + }); + test('a wrong code is refused WITHOUT spending the challenge', async ({ request }) => { // Dropping the challenge on a mistyped digit returns the person to the // password form, which reads as "your password was wrong" — a working diff --git a/tests/unit/auth/login-2fa-token-relay.spec.ts b/tests/unit/auth/login-2fa-token-relay.spec.ts new file mode 100644 index 000000000..f215fff89 --- /dev/null +++ b/tests/unit/auth/login-2fa-token-relay.spec.ts @@ -0,0 +1,174 @@ +/** + * POST /api/auth/login and /login/2fa — the token-relay contract with the login page. + * + * The login page does not talk to the browser's cookie jar. It calls the API + * in-process through the BFF client, and that client does not forward the + * API's Set-Cookie. The only way the page can open a session is to receive the + * JWT in the body and write its own cookie (`createSessionWithToken`). The + * password login has always honoured that: a caller sending `x-token-relay: 1` + * gets `data.token`. The second-factor endpoint did not, so a correct TOTP code + * entered on /login produced "Verification could not be completed" and no + * session — and every test passed, because the page's unit test mocked a + * `data.token` the server never sent, and the E2E spec only called the API. + * + * Driven through the REAL app (`server/index.ts`), real middleware chain, real + * keyring and an in-memory D1 (see helpers/tenant-email-harness.ts). The token + * is checked by verifying it against the deployment's own keyring, not by + * shape. The negative case — no header, no token in the body — is the rule in + * CLAUDE.md's JWT section: a browser must never be handed the JWT. + */ +import { describe, it, expect, afterEach } from 'vitest'; +import { eq } from 'drizzle-orm'; +import { TOTP, Secret } from 'otpauth'; +import { app } from '../../../server/index'; +import { makeExecutionContext } from '../helpers/exec-ctx'; +import * as schema from '../../../server/lib/db/schema'; +import { hashPassword } from '../../../server/lib/password'; +import { buildKeyring, verifyJwt } from '../../../server/lib/jwt-keyring'; +import { TotpService } from '../../../server/services/totp.service'; +import { createHarness, OWNER_ID, TENANT_ID, type Harness } from '../helpers/tenant-email-harness'; +import { LoginResponseSchema } from '../../../server/lib/validations/auth.schema'; + +const EMAIL = 'owner@harbor-light.test'; +const PASSWORD = 'Correct-Horse-Battery-9'; + +const codeFor = (secret: string) => + new TOTP({ secret: Secret.fromBase32(secret), algorithm: 'SHA1', digits: 6, period: 30 }).generate(); + +/** + * The API's CSRF check is a stateless double-submit (cookie equals header); the + * BFF mints its own pair per request (app/lib/csrf.ts), so the test does too. + */ +const CSRF = 'a1b2c3d4e5f60718293a4b5c6d7e8f90'; + +function postJson(body: Record, headers: Record = {}): RequestInit { + return { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-csrf-token': CSRF, + cookie: `__Host-csrf_token=${CSRF}`, + ...headers, + }, + body: JSON.stringify(body), + }; +} + +async function call(h: Harness, path: string, init: RequestInit): Promise { + const exec = makeExecutionContext({ label: path }); + const res = await app.request(path, init, h.env, exec.ctx); + await exec.settle(); + return res; +} + +/** Enrol the seeded owner: a real password and an active TOTP secret. */ +async function enrol(h: Harness): Promise { + const secret = new TotpService().generateSecret(); + await h.db.update(schema.users).set({ + passwordHash: await hashPassword(PASSWORD), + totpSecret: secret, + totpEnabled: true, + totpVerifiedAt: new Date(), + }).where(eq(schema.users.id, OWNER_ID)); + return secret; +} + +/** The first half of the sign-in: the password buys a challenge. */ +async function challenge(h: Harness): Promise { + const res = await call(h, '/api/auth/login', postJson({ email: EMAIL, password: PASSWORD }, { 'x-token-relay': '1' })); + const text = await res.text(); + expect(res.status, text).toBe(200); + const body = JSON.parse(text) as { data: { requires2fa?: boolean; challengeToken?: string; token?: string } }; + expect(body.data.requires2fa, 'a 2FA account gets a challenge').toBe(true); + expect(body.data.token, 'and no session before the second factor, even for the BFF').toBeUndefined(); + return body.data.challengeToken!; +} + +describe('POST /api/auth/login/2fa — token relay (standalone)', () => { + let h: Harness; + afterEach(() => h.close()); + + it('returns a verifiable session JWT in data.token to a caller sending x-token-relay: 1', { timeout: 60_000 }, async () => { + h = await createHarness('standalone'); + const secret = await enrol(h); + const challengeToken = await challenge(h); + + const res = await call(h, '/api/auth/login/2fa', postJson({ challengeToken, code: codeFor(secret) }, { 'x-token-relay': '1' })); + const text = await res.text(); + expect(res.status, text).toBe(200); + const body = JSON.parse(text) as { data: { redirect?: string; token?: string } }; + expect(body.data.redirect).toBe('/inspections'); + expect(typeof body.data.token, `the BFF needs the JWT to open a session: ${text.slice(0, 300)}`).toBe('string'); + + // Verified with the deployment's own keyring — a string that merely + // looks like a JWT would not open a session either. + const keyring = await buildKeyring(h.env as unknown as Record); + const claims = await verifyJwt(body.data.token!, keyring); + expect(claims['sub']).toBe(OWNER_ID); + expect(claims['custom:tenantId']).toBe(TENANT_ID); + expect(claims['t'], 'a session, not another challenge').toBeUndefined(); + }); + + it('does NOT put the JWT in the body for a caller without the header, and still sets the cookie', { timeout: 60_000 }, async () => { + h = await createHarness('standalone'); + const secret = await enrol(h); + const challengeToken = await challenge(h); + + const res = await call(h, '/api/auth/login/2fa', postJson({ challengeToken, code: codeFor(secret) })); + const text = await res.text(); + expect(res.status, text).toBe(200); + const body = JSON.parse(text) as { data: { redirect?: string; token?: string } }; + expect(body.data.redirect).toBe('/inspections'); + expect(body.data.token, 'a browser must never be handed the JWT').toBeUndefined(); + // Positive control for the same response: the browser path still + // gets its session, just not in the body. + expect(res.headers.get('set-cookie') ?? '').toMatch(/__Host-inspector_token=[^;]+/); + }); +}); + +/** + * The password step for an account WITHOUT a second factor: the same rule, on + * the route whose response schema used to be shared with join and setup and so + * did not declare `token` at all. Each body is parsed with the route's own + * declared schema, so the documented shape and the handler cannot drift apart + * without one of these failing. + */ +describe('POST /api/auth/login — token relay (standalone, no second factor)', () => { + let h: Harness; + afterEach(() => h.close()); + + async function withPassword(): Promise { + h = await createHarness('standalone'); + await h.db.update(schema.users) + .set({ passwordHash: await hashPassword(PASSWORD), totpEnabled: false }) + .where(eq(schema.users.id, OWNER_ID)); + } + + it('returns a verifiable session JWT in data.token to a caller sending x-token-relay: 1', { timeout: 60_000 }, async () => { + await withPassword(); + const res = await call(h, '/api/auth/login', postJson({ email: EMAIL, password: PASSWORD }, { 'x-token-relay': '1' })); + const text = await res.text(); + expect(res.status, text).toBe(200); + const body = LoginResponseSchema.parse(JSON.parse(text)); + expect(body.data, text.slice(0, 300)).toHaveProperty('redirect', '/inspections'); + const token = (body.data as { token?: string }).token; + expect(typeof token, `the BFF needs the JWT to open a session: ${text.slice(0, 300)}`).toBe('string'); + + const keyring = await buildKeyring(h.env as unknown as Record); + const claims = await verifyJwt(token!, keyring); + expect(claims['sub']).toBe(OWNER_ID); + expect(claims['custom:tenantId']).toBe(TENANT_ID); + }); + + it('does NOT put the JWT in the body for a caller without the header, and still sets the cookie', { timeout: 60_000 }, async () => { + await withPassword(); + const res = await call(h, '/api/auth/login', postJson({ email: EMAIL, password: PASSWORD })); + const text = await res.text(); + expect(res.status, text).toBe(200); + const raw = JSON.parse(text) as { data: Record }; + // Asserted on the raw body, before any schema parse touches it. + expect(raw.data, 'a browser must never be handed the JWT').not.toHaveProperty('token'); + expect(LoginResponseSchema.parse(raw).data).toEqual({ redirect: '/inspections' }); + expect(res.headers.get('set-cookie') ?? '').toMatch(/__Host-inspector_token=[^;]+/); + }); +}); diff --git a/tests/unit/automations/automation-flush-provider-refusal.spec.ts b/tests/unit/automations/automation-flush-provider-refusal.spec.ts new file mode 100644 index 000000000..8e8dff9dc --- /dev/null +++ b/tests/unit/automations/automation-flush-provider-refusal.spec.ts @@ -0,0 +1,142 @@ +/** + * flush() when the email provider refuses a send, and the per-second throttle. + * + * A rate limit and a daily quota leave the row PENDING with send_at pushed (the + * message is still owed and nothing moves a terminal row back to pending); a + * monthly quota fails the row with a readable reason; anything else keeps its + * old terminal behaviour. The throttle is injected so no test waits. + */ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import * as schema from '../../../server/lib/db/schema'; +import { createTestDb, setupSchema } from '../db'; +import { asD1Db } from '../helpers/test-db'; +import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'; +import { eq } from 'drizzle-orm'; + +vi.mock('drizzle-orm/d1', () => ({ drizzle: vi.fn() })); +import { drizzle as mockDrizzle } from 'drizzle-orm/d1'; +import { AutomationService } from '../../../server/services/automation.service'; +import { seedRoleProfiles } from '../../../server/services/seed/seed-role-profiles'; +import { AppError, ErrorCode, Errors } from '../../../server/lib/errors'; +import { + RATE_LIMIT_DEFER_REASON, DAILY_QUOTA_DEFER_REASON, MONTHLY_QUOTA_FAIL_REASON, nextUtcMidnightMs, +} from '../../../server/services/automation/provider-deferral'; +import type { EmailService } from '../../../server/services/email.service'; +import type { SendThrottle } from '../../../server/services/automation/send-throttle'; + +const T = '00000000-0000-0000-0000-0000000000c4'; +let db: BetterSQLite3Database; +let svc: AutomationService; + +beforeEach(async () => { + const fx = createTestDb(); + db = fx.db; + await setupSchema(fx.sqlite); + (mockDrizzle as unknown as ReturnType).mockReturnValue(db); + await db.insert(schema.tenants).values({ + id: T, slug: 'acme-c4', status: 'active', deploymentMode: 'shared', tier: 'free', createdAt: new Date(), + } as never); + await seedRoleProfiles(asD1Db(db), T, new Date(1)); + await db.insert(schema.inspections).values({ + id: 'insp-1', tenantId: T, propertyAddress: '1 Main St', date: '2026-06-01', status: 'completed', + reportStatus: 'published', paymentStatus: 'unpaid', price: 0, agreementRequired: false, + paymentRequired: false, createdAt: new Date(), + } as never); + svc = new AutomationService({} as D1Database); + await svc.ensureSeeds(T); +}); + +async function seedEmailLogs(ids: string[]) { + const rule = (await db.select().from(schema.automations).where(eq(schema.automations.tenantId, T))) + .find(a => a.name === 'Report Ready')!; + for (const id of ids) { + await db.insert(schema.automationLogs).values({ + id, tenantId: T, automationId: rule.id, inspectionId: 'insp-1', + recipient: `${id}@example.com`, channel: 'email', sendAt: new Date(0), status: 'pending', + } as never); + } +} +const rowOf = (id: string) => db.select().from(schema.automationLogs).where(eq(schema.automationLogs.id, id)).get(); +const emailThrowing = (err: unknown) => async () => + ({ sendEmail: async () => { throw err; } } as unknown as EmailService); +const quotaError = (quotaWindow?: 'daily' | 'monthly') => new AppError(402, ErrorCode.QUOTA_EXHAUSTED, 'quota', + { metric: 'email', used: 0, cap: 0, billingPortalUrl: null, ...(quotaWindow ? { quotaWindow } : {}) }); +const instantThrottle: SendThrottle = { beforeSend: async () => undefined }; + +async function flushWith(emailFor: () => Promise, throttle: SendThrottle = instantThrottle) { + await svc.flush(emailFor, 'Acme', 'https://app.example.com', undefined, 50, undefined, undefined, undefined, { throttle }); +} + +describe('flush() — provider refusals', () => { + it('keeps a rate-limited row PENDING and pushes send_at by retry-after', async () => { + await seedEmailLogs(['log-rl']); + const before = Date.now(); + await flushWith(emailThrowing(Errors.EmailRateLimited(30))); + const row = await rowOf('log-rl'); + expect(row?.status).toBe('pending'); + expect(row?.error).toBe(RATE_LIMIT_DEFER_REASON); + const sendAt = new Date(row!.sendAt as unknown as number | Date).getTime(); + expect(sendAt).toBeGreaterThanOrEqual(before + 30_000); + expect(sendAt).toBeLessThan(Date.now() + 31_000); + }); + + it('keeps a daily-quota row PENDING until the next UTC midnight', async () => { + await seedEmailLogs(['log-dq']); + await flushWith(emailThrowing(quotaError('daily'))); + const row = await rowOf('log-dq'); + expect(row?.status).toBe('pending'); + expect(row?.error).toBe(DAILY_QUOTA_DEFER_REASON); + expect(new Date(row!.sendAt as unknown as number | Date).getTime()).toBe(nextUtcMidnightMs(Date.now())); + }); + + it('does not pick a deferred row up again in the same tick', async () => { + await seedEmailLogs(['log-dq']); + const send = vi.fn(async () => { throw quotaError('daily'); }); + const emailFor = async () => ({ sendEmail: send } as unknown as EmailService); + await flushWith(emailFor); + await flushWith(emailFor); + expect(send).toHaveBeenCalledTimes(1); + }); + + it('fails a monthly-quota row with a readable reason', async () => { + await seedEmailLogs(['log-mq']); + await flushWith(emailThrowing(quotaError('monthly'))); + const row = await rowOf('log-mq'); + expect(row?.status).toBe('failed'); + expect(row?.error).toBe(MONTHLY_QUOTA_FAIL_REASON); + }); + + it('keeps the old terminal failure for a quota whose window is unknown', async () => { + await seedEmailLogs(['log-uq']); + await flushWith(emailThrowing(quotaError())); + expect((await rowOf('log-uq'))?.status).toBe('failed'); + }); + + it('keeps the old terminal failure for an ordinary delivery error', async () => { + await seedEmailLogs(['log-502']); + await flushWith(emailThrowing(new AppError(502, ErrorCode.SERVICE_UNAVAILABLE, 'Email delivery failed. Please try again.'))); + const row = await rowOf('log-502'); + expect(row?.status).toBe('failed'); + expect(row?.error).toBe('Email delivery failed. Please try again.'); + }); +}); + +describe('flush() — send throttle', () => { + it('waits on the throttle before every email send, one call per send', async () => { + await seedEmailLogs(['log-a', 'log-b', 'log-c']); + const order: string[] = []; + const throttle = { beforeSend: async () => { order.push('throttle'); } }; + const emailFor = async () => ({ + sendEmail: async () => { order.push('send'); return { delivered: true }; }, + } as unknown as EmailService); + await flushWith(emailFor, throttle); + expect(order).toEqual(['throttle', 'send', 'throttle', 'send', 'throttle', 'send']); + }); + + it('defaults to a real throttle so existing callers are throttled without opting in', async () => { + await seedEmailLogs(['log-a']); + const emailFor = async () => ({ sendEmail: async () => ({ delivered: true }) } as unknown as EmailService); + await svc.flush(emailFor, 'Acme', 'https://app.example.com'); + expect((await rowOf('log-a'))?.status).toBe('sent'); + }); +}); diff --git a/tests/unit/automations/automation-human-verified.spec.ts b/tests/unit/automations/automation-human-verified.spec.ts new file mode 100644 index 000000000..e406225aa --- /dev/null +++ b/tests/unit/automations/automation-human-verified.spec.ts @@ -0,0 +1,71 @@ +/** + * A report publish that passed the human check vouches for the email rows it + * queued, including delayed ones that are sent later with no request around. + * The stamp is scoped: this tenant, this inspection, pending email rows, once. + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import * as schema from '../../../server/lib/db/schema'; +import { createTestDb, setupSchema } from '../db'; +import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'; +import { stampIfHumanVerified } from '../../../server/services/automation/human-verified'; + +const T1 = '00000000-0000-0000-0000-0000000aa001'; +const T2 = '00000000-0000-0000-0000-0000000aa002'; + +let db: BetterSQLite3Database; + +const row = (id: string, over: Record = {}) => ({ + id, tenantId: T1, inspectionId: 'insp-1', recipient: 'c@example.com', channel: 'email' as const, automationId: 'rule-pub', + sendAt: new Date(), status: 'pending' as const, ...over, +}); + +beforeEach(async () => { + const fx = createTestDb(); + db = fx.db; + await setupSchema(fx.sqlite); + const rule = (id: string, trigger: string, tenantId = T1) => ({ + id, tenantId, name: id, trigger, recipientKind: 'all', createdAt: new Date(), + }); + for (const [id, slug] of [[T1, 'a1'], [T2, 'a2']] as const) { + await db.insert(schema.tenants).values({ id, slug, status: 'active', deploymentMode: 'shared', tier: 'free', createdAt: new Date() } as never); + } + await db.insert(schema.automations).values([ + rule('rule-pub', 'report.published'), rule('rule-amended', 'report.amended'), rule('rule-other', 'inspection.reminder'), + ] as never); +}); + +const stamped = async () => + (await db.select({ id: schema.automationLogs.id, at: schema.automationLogs.humanVerifiedAt }).from(schema.automationLogs)) + .filter((r) => r.at !== null).map((r) => r.id).sort(); + +describe('stampHumanVerifiedLogs', () => { + it('stamps only this inspection\'s pending email rows for this tenant', async () => { + await db.insert(schema.automationLogs).values([ + row('mine'), + row('other-inspection', { inspectionId: 'insp-2' }), + row('other-tenant', { tenantId: T2 }), + row('sms', { channel: 'sms' }), + row('already-sent', { status: 'sent' }), + row('other-trigger', { automationId: 'rule-other' }), + row('manual-send', { automationId: null }), + row('amended', { automationId: 'rule-amended' }), + ] as never); + await stampIfHumanVerified(async () => true, db as never, T1, 'insp-1'); + expect(await stamped()).toEqual(['amended', 'mine']); + }); + + it('never overwrites an earlier stamp', async () => { + const first = new Date(1_000); + await db.insert(schema.automationLogs).values(row('a', { humanVerifiedAt: first })).run(); + await stampIfHumanVerified(async () => true, db as never, T1, 'insp-1'); + const [r] = await db.select().from(schema.automationLogs); + expect(r!.humanVerifiedAt!.getTime()).toBe(1_000); + }); + + it('stamps nothing when there is no token or it does not verify', async () => { + await db.insert(schema.automationLogs).values(row('a') as never).run(); + await stampIfHumanVerified(undefined, db as never, T1, 'insp-1'); + await stampIfHumanVerified(async () => false, db as never, T1, 'insp-1'); + expect(await stamped()).toEqual([]); + }); +}); diff --git a/tests/unit/automations/automation-provider-deferral.spec.ts b/tests/unit/automations/automation-provider-deferral.spec.ts new file mode 100644 index 000000000..15c90e3dc --- /dev/null +++ b/tests/unit/automations/automation-provider-deferral.spec.ts @@ -0,0 +1,98 @@ +/** + * What the automation ledger does when the email PROVIDER pushes back. + * + * A rate limit and a daily quota both undo themselves on a known clock, so the + * row must stay pending with its send_at moved (the same shape the cooling + * window uses) rather than spend a terminal `failed` on a message that is still + * owed. A monthly quota does not undo itself within any useful time, so that one + * is a real failure with a reason a person can read. + */ +import { describe, it, expect, vi } from 'vitest'; +import { AppError, ErrorCode, Errors } from '../../../server/lib/errors'; +import { + providerOutcomeFor, nextUtcMidnightMs, applyProviderOutcome, + PROVIDER_DEFERRAL_SENTINEL, RATE_LIMIT_DEFER_REASON, DAILY_QUOTA_DEFER_REASON, MONTHLY_QUOTA_FAIL_REASON, +} from '../../../server/services/automation/provider-deferral'; + +const quotaError = (quotaWindow?: 'daily' | 'monthly') => new AppError( + 402, ErrorCode.QUOTA_EXHAUSTED, 'quota', + { metric: 'email', used: 0, cap: 0, billingPortalUrl: null, ...(quotaWindow ? { quotaWindow } : {}) }, +); + +function fakeDb() { + const set = vi.fn(); + const db = { update: vi.fn(() => ({ set: (v: Record) => { set(v); return { where: vi.fn(async () => undefined) }; } })) }; + return { db, set }; +} + +describe('nextUtcMidnightMs', () => { + it('is the start of the following UTC day', () => { + expect(nextUtcMidnightMs(Date.UTC(2026, 9, 11, 13, 45, 12))).toBe(Date.UTC(2026, 9, 12)); + }); + it('moves a full day when called exactly at midnight', () => { + expect(nextUtcMidnightMs(Date.UTC(2026, 9, 11))).toBe(Date.UTC(2026, 9, 12)); + }); + it('rolls over month and year ends', () => { + expect(nextUtcMidnightMs(Date.UTC(2026, 11, 31, 23, 59, 59))).toBe(Date.UTC(2027, 0, 1)); + }); +}); + +describe('providerOutcomeFor', () => { + const now = Date.UTC(2026, 9, 11, 10, 0, 0); + + it('defers a rate limit by the provider retry-after', () => { + expect(providerOutcomeFor(Errors.EmailRateLimited(30), now)) + .toEqual({ action: 'defer', sendAtMs: now + 30_000, reason: RATE_LIMIT_DEFER_REASON }); + }); + + it('falls back to a one-minute pause when the provider gave no hint, never to zero', () => { + expect(providerOutcomeFor(Errors.EmailRateLimited(), now)) + .toEqual({ action: 'defer', sendAtMs: now + 60_000, reason: RATE_LIMIT_DEFER_REASON }); + expect(providerOutcomeFor(Errors.EmailRateLimited(0), now)).toMatchObject({ sendAtMs: now + 1_000 }); + }); + + it('defers a daily quota to the next UTC midnight', () => { + expect(providerOutcomeFor(quotaError('daily'), now)) + .toEqual({ action: 'defer', sendAtMs: Date.UTC(2026, 9, 12), reason: DAILY_QUOTA_DEFER_REASON }); + }); + + it('fails a monthly quota with a readable reason', () => { + const out = providerOutcomeFor(quotaError('monthly'), now); + expect(out).toEqual({ action: 'fail', reason: MONTHLY_QUOTA_FAIL_REASON }); + expect(MONTHLY_QUOTA_FAIL_REASON).toMatch(/monthly/i); + }); + + it('leaves a quota of unknown window alone, so it keeps the old terminal behaviour', () => { + expect(providerOutcomeFor(quotaError(), now)).toBeNull(); + }); + + it('leaves a plan quota (no provider window) and every other error alone', () => { + expect(providerOutcomeFor(Errors.QuotaExhausted({ metric: 'email', used: 100, cap: 100, billingPortalUrl: null }), now)).toBeNull(); + expect(providerOutcomeFor(new AppError(502, ErrorCode.SERVICE_UNAVAILABLE, 'x'), now)).toBeNull(); + expect(providerOutcomeFor(new Error('boom'), now)).toBeNull(); + expect(providerOutcomeFor(undefined, now)).toBeNull(); + }); + + it('ignores a window on a quota error about another metric', () => { + const e = new AppError(402, ErrorCode.QUOTA_EXHAUSTED, 'q', { metric: 'sms', quotaWindow: 'daily' }); + expect(providerOutcomeFor(e, now)).toBeNull(); + }); +}); + +describe('applyProviderOutcome', () => { + it('keeps a deferred row PENDING and moves send_at', async () => { + const { db, set } = fakeDb(); + await applyProviderOutcome(db, 'log-1', { action: 'defer', sendAtMs: 5_000, reason: RATE_LIMIT_DEFER_REASON }); + expect(set).toHaveBeenCalledWith({ status: 'pending', error: RATE_LIMIT_DEFER_REASON, sendAt: new Date(5_000) }); + }); + + it('marks a failed row failed with the reason', async () => { + const { db, set } = fakeDb(); + await applyProviderOutcome(db, 'log-1', { action: 'fail', reason: MONTHLY_QUOTA_FAIL_REASON }); + expect(set).toHaveBeenCalledWith({ status: 'failed', error: MONTHLY_QUOTA_FAIL_REASON }); + }); + + it('exposes a sentinel distinct from the cooling-window one', () => { + expect(PROVIDER_DEFERRAL_SENTINEL).toBe('__provider_deferral__'); + }); +}); diff --git a/tests/unit/automations/automation-send-throttle.spec.ts b/tests/unit/automations/automation-send-throttle.spec.ts new file mode 100644 index 000000000..1bb4e0baf --- /dev/null +++ b/tests/unit/automations/automation-send-throttle.spec.ts @@ -0,0 +1,58 @@ +/** + * A flush batch never starts more than N sends in any one-second window. + * Clock and sleep are injected: the fake sleep advances the fake clock, so the + * test asserts the schedule without waiting. + */ +import { describe, it, expect } from 'vitest'; +import { createSendThrottle } from '../../../server/services/automation/send-throttle'; + +function fakeClock(start = 1_000_000) { + let t = start; + const sleeps: number[] = []; + return { + now: () => t, + sleep: async (ms: number) => { sleeps.push(ms); t += ms; }, + advance: (ms: number) => { t += ms; }, + sleeps, + }; +} + +describe('createSendThrottle', () => { + it('lets a first burst of the limit through with no waiting', async () => { + const c = fakeClock(); + const th = createSendThrottle({ maxPerSecond: 5, now: c.now, sleep: c.sleep }); + for (let i = 0; i < 5; i++) await th.beforeSend(); + expect(c.sleeps).toEqual([]); + }); + + it('holds the sixth send until a full second after the first', async () => { + const c = fakeClock(); + const th = createSendThrottle({ maxPerSecond: 5, now: c.now, sleep: c.sleep }); + for (let i = 0; i < 6; i++) await th.beforeSend(); + expect(c.sleeps).toEqual([1000]); + }); + + it('never starts more than the limit inside any one-second window', async () => { + const c = fakeClock(); + const th = createSendThrottle({ maxPerSecond: 5, now: c.now, sleep: c.sleep }); + const starts: number[] = []; + for (let i = 0; i < 23; i++) { await th.beforeSend(); starts.push(c.now()); c.advance(10); } + for (let i = 0; i + 5 < starts.length; i++) { + expect(starts[i + 5]! - starts[i]!).toBeGreaterThanOrEqual(1000); + } + }); + + it('does not wait when sends are naturally spread out', async () => { + const c = fakeClock(); + const th = createSendThrottle({ maxPerSecond: 5, now: c.now, sleep: c.sleep }); + for (let i = 0; i < 12; i++) { await th.beforeSend(); c.advance(400); } + expect(c.sleeps).toEqual([]); + }); + + it('defaults to five per second', async () => { + const c = fakeClock(); + const th = createSendThrottle({ now: c.now, sleep: c.sleep }); + for (let i = 0; i < 6; i++) await th.beforeSend(); + expect(c.sleeps).toEqual([1000]); + }); +}); diff --git a/tests/unit/email/human-token-relay.spec.ts b/tests/unit/email/human-token-relay.spec.ts new file mode 100644 index 000000000..d8a8f3879 --- /dev/null +++ b/tests/unit/email/human-token-relay.spec.ts @@ -0,0 +1,41 @@ +/** + * The worker entry copies the browser's one-time Turnstile cookie onto the + * in-process API call as a header. Only a mutation carries it, and only a + * well-formed value does. + */ +import { describe, it, expect } from 'vitest'; +import { humanTokenFromCookie, withHumanToken, HUMAN_TOKEN_HEADER } from '../../../workers/human-token'; + +const TOKEN = '0.AbCdEfGhIjKlMnOpQrStUvWx-_yz'; +const cookie = `a=1; oi-human-token=${TOKEN}; b=2`; +const post = () => new Request('https://x.test/api/inspections/1/send', { method: 'POST', body: '{}' }); + +describe('humanTokenFromCookie', () => { + it('reads the token out of a cookie header', () => { + expect(humanTokenFromCookie(cookie)).toBe(TOKEN); + }); + it('returns null when absent, empty or malformed', () => { + expect(humanTokenFromCookie(undefined)).toBeNull(); + expect(humanTokenFromCookie('a=1')).toBeNull(); + expect(humanTokenFromCookie('oi-human-token=short')).toBeNull(); + expect(humanTokenFromCookie('oi-human-token=has space and