CREATE TABLE "audit_events" ( "id" bigserial PRIMARY KEY NOT NULL, "actor_id" uuid, "action" text NOT NULL, "object_type" text, "object_id" text, "ip_hash" text, "metadata_redacted" jsonb DEFAULT '{}'::jsonb NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint CREATE TABLE "email_deliveries" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "to_hash" text NOT NULL, "template" text NOT NULL, "provider_id" text, "status" text NOT NULL, "error" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint CREATE TABLE "favorites" ( "user_id" uuid NOT NULL, "listing_uid" text NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "favorites_user_id_listing_uid_pk" PRIMARY KEY("user_id","listing_uid") ); --> statement-breakpoint CREATE TABLE "inquiries" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "listing_id" uuid NOT NULL, "sender_user_id" uuid NOT NULL, "recipient_user_id" uuid NOT NULL, "category" text DEFAULT 'INFO' NOT NULL, "status" text DEFAULT 'OPEN' NOT NULL, "last_message_at" timestamp with time zone DEFAULT now() NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint CREATE TABLE "listing_claims" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "aggregated_uid" text NOT NULL, "requester_id" uuid NOT NULL, "status" text DEFAULT 'PENDING' NOT NULL, "message" text, "proof_keys" jsonb DEFAULT '[]'::jsonb NOT NULL, "reviewed_by" uuid, "reviewed_at" timestamp with time zone, "created_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint CREATE TABLE "listing_media" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "listing_id" uuid NOT NULL, "storage_key" text NOT NULL, "media_type" text DEFAULT 'IMAGE' NOT NULL, "mime_type" text, "position" integer DEFAULT 0 NOT NULL, "alt_text" text, "license_status" text DEFAULT 'OWNER_DECLARED' NOT NULL, "scan_status" text DEFAULT 'PENDING' NOT NULL, "width" integer, "height" integer, "bytes" integer, "status" text DEFAULT 'UPLOADING' NOT NULL, "upload_token" text, "uploaded_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint CREATE TABLE "listing_versions" ( "id" bigserial PRIMARY KEY NOT NULL, "listing_id" uuid NOT NULL, "version" integer NOT NULL, "actor_id" uuid, "snapshot" jsonb NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint CREATE TABLE "listings" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "slug" text NOT NULL, "property_id" uuid NOT NULL, "owner_user_id" uuid, "listing_type" text NOT NULL, "source_type" text DEFAULT 'DIRECT_OWNER' NOT NULL, "status" text DEFAULT 'DRAFT' NOT NULL, "title" text, "description" text, "price_amount" numeric(12, 2), "currency" text DEFAULT 'CAD' NOT NULL, "rental_period" text, "availability_date" text, "furnished" boolean, "pets_policy" text, "smoking_policy" text, "parking" text, "lease_terms" text, "inclusions" jsonb DEFAULT '[]'::jsonb NOT NULL, "features" jsonb DEFAULT '{}'::jsonb NOT NULL, "publication_at" timestamp with time zone, "expires_at" timestamp with time zone, "moderation_state" text DEFAULT 'NONE' NOT NULL, "moderation_note" text, "external_url" text, "upstream_source" text, "upstream_id" text, "upstream_sync_at" timestamp with time zone, "wizard_step" integer DEFAULT 0 NOT NULL, "attested_at" timestamp with time zone, "version" integer DEFAULT 1 NOT NULL, "views_count" integer DEFAULT 0 NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "listings_type_ck" CHECK ("listings"."listing_type" in ('SALE','RENT')), CONSTRAINT "listings_source_ck" CHECK ("listings"."source_type" in ('DIRECT_OWNER','DIRECT_AGENT','AGGREGATED_PARTNER')), CONSTRAINT "listings_status_ck" CHECK ("listings"."status" in ('DRAFT','PENDING_REVIEW','PUBLISHED','PAUSED','REJECTED','ARCHIVED','SOLD','RENTED')), CONSTRAINT "listings_price_ck" CHECK ("listings"."price_amount" is null or "listings"."price_amount" >= 0), CONSTRAINT "listings_owner_direct_ck" CHECK ("listings"."source_type" = 'AGGREGATED_PARTNER' or "listings"."owner_user_id" is not null), CONSTRAINT "listings_rent_period_ck" CHECK ("listings"."listing_type" <> 'RENT' or "listings"."rental_period" is null or "listings"."rental_period" in ('MONTH','WEEK')) ); --> statement-breakpoint CREATE TABLE "messages" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "inquiry_id" uuid NOT NULL, "sender_id" uuid NOT NULL, "body" text NOT NULL, "idempotency_key" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "read_at" timestamp with time zone ); --> statement-breakpoint CREATE TABLE "moderation_actions" ( "id" bigserial PRIMARY KEY NOT NULL, "actor_id" uuid NOT NULL, "target_type" text NOT NULL, "target_id" text NOT NULL, "action" text NOT NULL, "note" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint CREATE TABLE "otp_challenges" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "email_normalized" text NOT NULL, "purpose" text DEFAULT 'LOGIN' NOT NULL, "secret_digest" text NOT NULL, "expires_at" timestamp with time zone NOT NULL, "attempts" integer DEFAULT 0 NOT NULL, "consumed_at" timestamp with time zone, "requested_ip_hash" text, "meta" jsonb DEFAULT '{}'::jsonb NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint CREATE TABLE "properties" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "canonical_address" text, "unit" text, "city" text, "region" text, "postal_code" text, "lat" double precision, "lng" double precision, "geographic_precision" text DEFAULT 'EXACT' NOT NULL, "geo_source" text, "role_unit_id" text, "property_type" text NOT NULL, "bedrooms" numeric(4, 1), "bathrooms" numeric(4, 1), "area_sqft" integer, "lot_sqft" integer, "year_built" integer, "privacy_level" text DEFAULT 'EXACT' NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint CREATE TABLE "rate_limits" ( "key" text NOT NULL, "window_start" timestamp with time zone NOT NULL, "count" integer DEFAULT 0 NOT NULL, CONSTRAINT "rate_limits_key_window_start_pk" PRIMARY KEY("key","window_start") ); --> statement-breakpoint CREATE TABLE "reports" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "target_type" text NOT NULL, "target_id" text NOT NULL, "reporter_id" uuid, "reason" text NOT NULL, "details" text, "status" text DEFAULT 'OPEN' NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint CREATE TABLE "saved_searches" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "user_id" uuid NOT NULL, "label" text NOT NULL, "query" jsonb NOT NULL, "alert_enabled" boolean DEFAULT false NOT NULL, "alert_frequency" text DEFAULT 'DAILY' NOT NULL, "last_alert_at" timestamp with time zone, "created_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint CREATE TABLE "sessions" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "user_id" uuid NOT NULL, "token_digest" text NOT NULL, "issued_at" timestamp with time zone DEFAULT now() NOT NULL, "last_seen_at" timestamp with time zone DEFAULT now() NOT NULL, "expires_at" timestamp with time zone NOT NULL, "revoked_at" timestamp with time zone, "device_label" text, "ip_hash" text, "mfa_at" timestamp with time zone ); --> statement-breakpoint CREATE TABLE "user_profiles" ( "user_id" uuid PRIMARY KEY NOT NULL, "public_name" text, "bio" text, "avatar_key" text, "seller_type" text DEFAULT 'OWNER' NOT NULL, "verification_status" text DEFAULT 'NONE' NOT NULL, "contact_preferences" jsonb DEFAULT '{"email":true,"phone":false}'::jsonb NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint CREATE TABLE "user_roles" ( "user_id" uuid NOT NULL, "role" text NOT NULL, "granted_by" uuid, "granted_at" timestamp with time zone DEFAULT now() NOT NULL, "note" text, CONSTRAINT "user_roles_user_id_role_pk" PRIMARY KEY("user_id","role"), CONSTRAINT "user_roles_role_ck" CHECK ("user_roles"."role" in ('ADMIN','MODERATOR','AGENT_VERIFIED')) ); --> statement-breakpoint CREATE TABLE "users" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "email" text NOT NULL, "email_verified_at" timestamp with time zone, "display_name" text, "phone_encrypted" text, "locale" text DEFAULT 'fr-CA' NOT NULL, "status" text DEFAULT 'ACTIVE' NOT NULL, "prefs" jsonb DEFAULT '{}'::jsonb NOT NULL, "totp_secret_encrypted" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, "deleted_at" timestamp with time zone, CONSTRAINT "users_status_ck" CHECK ("users"."status" in ('ACTIVE','SUSPENDED','DELETED')) ); --> statement-breakpoint CREATE TABLE "viewing_requests" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "listing_id" uuid NOT NULL, "inquiry_id" uuid, "requester_id" uuid NOT NULL, "owner_id" uuid NOT NULL, "proposed_slots" jsonb NOT NULL, "confirmed_slot" timestamp with time zone, "status" text DEFAULT 'REQUESTED' NOT NULL, "note" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "viewings_status_ck" CHECK ("viewing_requests"."status" in ('REQUESTED','CONFIRMED','CANCELLED','DONE')) ); --> statement-breakpoint ALTER TABLE "favorites" ADD CONSTRAINT "favorites_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "inquiries" ADD CONSTRAINT "inquiries_listing_id_listings_id_fk" FOREIGN KEY ("listing_id") REFERENCES "public"."listings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "inquiries" ADD CONSTRAINT "inquiries_sender_user_id_users_id_fk" FOREIGN KEY ("sender_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "inquiries" ADD CONSTRAINT "inquiries_recipient_user_id_users_id_fk" FOREIGN KEY ("recipient_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "listing_claims" ADD CONSTRAINT "listing_claims_requester_id_users_id_fk" FOREIGN KEY ("requester_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "listing_media" ADD CONSTRAINT "listing_media_listing_id_listings_id_fk" FOREIGN KEY ("listing_id") REFERENCES "public"."listings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "listing_versions" ADD CONSTRAINT "listing_versions_listing_id_listings_id_fk" FOREIGN KEY ("listing_id") REFERENCES "public"."listings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "listings" ADD CONSTRAINT "listings_property_id_properties_id_fk" FOREIGN KEY ("property_id") REFERENCES "public"."properties"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint ALTER TABLE "listings" ADD CONSTRAINT "listings_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint ALTER TABLE "messages" ADD CONSTRAINT "messages_inquiry_id_inquiries_id_fk" FOREIGN KEY ("inquiry_id") REFERENCES "public"."inquiries"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "messages" ADD CONSTRAINT "messages_sender_id_users_id_fk" FOREIGN KEY ("sender_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "reports" ADD CONSTRAINT "reports_reporter_id_users_id_fk" FOREIGN KEY ("reporter_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint ALTER TABLE "saved_searches" ADD CONSTRAINT "saved_searches_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "user_profiles" ADD CONSTRAINT "user_profiles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "viewing_requests" ADD CONSTRAINT "viewing_requests_listing_id_listings_id_fk" FOREIGN KEY ("listing_id") REFERENCES "public"."listings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "viewing_requests" ADD CONSTRAINT "viewing_requests_inquiry_id_inquiries_id_fk" FOREIGN KEY ("inquiry_id") REFERENCES "public"."inquiries"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint ALTER TABLE "viewing_requests" ADD CONSTRAINT "viewing_requests_requester_id_users_id_fk" FOREIGN KEY ("requester_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "viewing_requests" ADD CONSTRAINT "viewing_requests_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint CREATE INDEX "audit_created_idx" ON "audit_events" USING btree ("created_at");--> statement-breakpoint CREATE INDEX "audit_actor_idx" ON "audit_events" USING btree ("actor_id");--> statement-breakpoint CREATE INDEX "email_deliveries_created_idx" ON "email_deliveries" USING btree ("created_at");--> statement-breakpoint CREATE INDEX "inquiries_listing_idx" ON "inquiries" USING btree ("listing_id");--> statement-breakpoint CREATE INDEX "inquiries_sender_idx" ON "inquiries" USING btree ("sender_user_id");--> statement-breakpoint CREATE INDEX "inquiries_recipient_idx" ON "inquiries" USING btree ("recipient_user_id");--> statement-breakpoint CREATE UNIQUE INDEX "inquiries_unique_thread" ON "inquiries" USING btree ("listing_id","sender_user_id");--> statement-breakpoint CREATE INDEX "listing_claims_uid_idx" ON "listing_claims" USING btree ("aggregated_uid");--> statement-breakpoint CREATE INDEX "listing_media_listing_idx" ON "listing_media" USING btree ("listing_id","position");--> statement-breakpoint CREATE INDEX "listing_versions_listing_idx" ON "listing_versions" USING btree ("listing_id","version");--> statement-breakpoint CREATE UNIQUE INDEX "listings_slug_uq" ON "listings" USING btree ("slug");--> statement-breakpoint CREATE INDEX "listings_status_idx" ON "listings" USING btree ("status","listing_type","publication_at");--> statement-breakpoint CREATE INDEX "listings_owner_idx" ON "listings" USING btree ("owner_user_id");--> statement-breakpoint CREATE UNIQUE INDEX "listings_upstream_uq" ON "listings" USING btree ("upstream_source","upstream_id");--> statement-breakpoint CREATE INDEX "messages_inquiry_idx" ON "messages" USING btree ("inquiry_id","created_at");--> statement-breakpoint CREATE UNIQUE INDEX "messages_idem_uq" ON "messages" USING btree ("inquiry_id","idempotency_key");--> statement-breakpoint CREATE INDEX "otp_email_idx" ON "otp_challenges" USING btree ("email_normalized","created_at");--> statement-breakpoint CREATE INDEX "otp_expires_idx" ON "otp_challenges" USING btree ("expires_at");--> statement-breakpoint CREATE INDEX "properties_geo_idx" ON "properties" USING btree ("lat","lng");--> statement-breakpoint CREATE INDEX "properties_city_idx" ON "properties" USING btree ("city");--> statement-breakpoint CREATE INDEX "reports_status_idx" ON "reports" USING btree ("status","created_at");--> statement-breakpoint CREATE INDEX "saved_searches_user_idx" ON "saved_searches" USING btree ("user_id");--> statement-breakpoint CREATE UNIQUE INDEX "sessions_token_uq" ON "sessions" USING btree ("token_digest");--> statement-breakpoint CREATE INDEX "sessions_user_idx" ON "sessions" USING btree ("user_id");--> statement-breakpoint CREATE UNIQUE INDEX "users_email_uq" ON "users" USING btree ("email");--> statement-breakpoint CREATE INDEX "viewings_listing_idx" ON "viewing_requests" USING btree ("listing_id");--> statement-breakpoint CREATE INDEX "viewings_requester_idx" ON "viewing_requests" USING btree ("requester_id");--> statement-breakpoint CREATE INDEX "viewings_owner_idx" ON "viewing_requests" USING btree ("owner_id");