Spaces:
Sleeping
Sleeping
| import { | |
| pgTable, | |
| uuid, | |
| text, | |
| jsonb, | |
| boolean, | |
| integer, | |
| bigint, | |
| numeric, | |
| timestamp, | |
| date, | |
| vector, | |
| uniqueIndex, | |
| index, | |
| customType, | |
| } from "drizzle-orm/pg-core"; | |
| import { sql } from "drizzle-orm"; | |
| /** tsvector DB-only column type (Drizzle has no built-in; we use customType). */ | |
| const tsvector = customType<{ data: string }>({ | |
| dataType() { | |
| return "tsvector"; | |
| }, | |
| }); | |
| /** | |
| * Phase 1 schema (PHASE_1 §5, arch §9 + §30). Enums are stored as `text`; the | |
| * canonical vocabularies live in @courtmitra/contracts and are validated in the | |
| * app/domain layer. Every change here must be a Drizzle migration (CLAUDE.md #8). | |
| * | |
| * Embedding dimension targets OpenAI text-embedding-3-small (1536). | |
| */ | |
| const EMBEDDING_DIM = 1536; | |
| // Reusable column builders. | |
| const id = () => uuid("id").primaryKey().defaultRandom(); | |
| const createdAt = () => timestamp("created_at", { withTimezone: true }).defaultNow().notNull(); | |
| const updatedAt = () => timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(); | |
| // ============================================================================ | |
| // Swarm / entity resolution (Phase 2 Module 1) | |
| // ============================================================================ | |
| export const entities = pgTable( | |
| "entities", | |
| { | |
| id: id(), | |
| normalizedName: text("normalized_name").notNull(), | |
| aliases: text("aliases").array(), // e.g. {"Acme Pvt Ltd", "Acme", "acme.com"} | |
| domain: text("domain").notNull(), // consumer, payment, cyber_fraud | |
| identifiers: jsonb("identifiers"), // { gst: "...", website: "...", socialHandles: [...] } | |
| createdAt: createdAt(), | |
| }, | |
| (t) => [ | |
| uniqueIndex("entities_normalized_name_domain_uq").on(t.normalizedName, t.domain), | |
| index("entities_domain_idx").on(t.domain), | |
| ], | |
| ); | |
| export const swarms = pgTable( | |
| "swarms", | |
| { | |
| id: id(), | |
| entityId: uuid("entity_id") | |
| .notNull() | |
| .references(() => entities.id), | |
| issueDomain: text("issue_domain").notNull(), // consumer, payment, cyber_fraud | |
| patternSignature: jsonb("pattern_signature"), // shared evidence/wording/route features | |
| caseCount: integer("case_count").default(0).notNull(), | |
| totalAmount: numeric("total_amount"), | |
| status: text("status").default("active").notNull(), // active, resolved, archived | |
| createdAt: createdAt(), | |
| updatedAt: updatedAt(), | |
| }, | |
| (t) => [ | |
| index("swarms_entity_idx").on(t.entityId), | |
| index("swarms_domain_idx").on(t.issueDomain), | |
| index("swarms_status_idx").on(t.status), | |
| ], | |
| ); | |
| export const swarmMembers = pgTable( | |
| "swarm_members", | |
| { | |
| swarmId: uuid("swarm_id") | |
| .notNull() | |
| .references(() => swarms.id), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| consentGrantId: uuid("consent_grant_id") | |
| .notNull() | |
| .references(() => consentGrants.id), // REQUIRED: per-mission aggregation consent | |
| contribution: jsonb("contribution"), // what this mission contributes to the swarm dossier | |
| joinedAt: createdAt(), | |
| }, | |
| (t) => [ | |
| uniqueIndex("swarm_members_swarm_mission_uq").on(t.swarmId, t.missionId), | |
| index("swarm_members_mission_idx").on(t.missionId), | |
| ], | |
| ); | |
| // ============================================================================ | |
| // ASR / Transcripts (Phase 2 Module 2) | |
| // ============================================================================ | |
| export const transcripts = pgTable("transcripts", { | |
| id: id(), | |
| evidenceItemId: uuid("evidence_item_id") | |
| .notNull() | |
| .references(() => evidenceItems.id), | |
| language: text("language"), // detected language code (e.g. "hi", "ta", "en") | |
| text: text("text").notNull(), // transcribed text | |
| model: text("model").notNull(), // "whisper-1", "whisper-cpp-base", etc. | |
| confidence: numeric("confidence"), // 0.0–1.0 | |
| durationMs: integer("duration_ms"), // audio duration in milliseconds | |
| createdAt: createdAt(), | |
| }); | |
| // ============================================================================ | |
| // Translation / localized texts (Phase 2 Module 3) | |
| // ============================================================================ | |
| export const localizedTexts = pgTable("localized_texts", { | |
| id: id(), | |
| resourceType: text("resource_type").notNull(), // 'message', 'snapshot', 'prompt', 'evidence' | |
| resourceId: uuid("resource_id").notNull(), // FK to the source row | |
| languageCode: text("language_code").notNull(), // 'hi', 'ta', 'en', etc. | |
| originalText: text("original_text").notNull(), // always preserved (CLAUDE.md: never lose original) | |
| normalizedText: text("normalized_text"), // cleaned/transliterated form | |
| translatedText: text("translated_text"), // English working form (for internal use) | |
| modelVersion: text("model_version"), // 'llm-gemini', 'indictrans2-1b' | |
| confidence: numeric("confidence"), | |
| createdAt: createdAt(), | |
| }, (t) => [ | |
| index("localized_texts_resource_idx").on(t.resourceType, t.resourceId), | |
| index("localized_texts_language_idx").on(t.languageCode), | |
| ]); | |
| // ============================================================================ | |
| // Identity / session | |
| // ============================================================================ | |
| export const users = pgTable("users", { | |
| id: id(), | |
| displayName: text("display_name"), | |
| email: text("email"), | |
| avatarUrl: text("avatar_url"), | |
| phone: text("phone"), | |
| fullName: text("full_name"), | |
| preferredLanguage: text("preferred_language").default("en"), | |
| riskFlags: jsonb("risk_flags"), | |
| createdAt: createdAt(), | |
| updatedAt: updatedAt(), | |
| }); | |
| export const userIdentities = pgTable( | |
| "user_identities", | |
| { | |
| id: id(), | |
| userId: uuid("user_id") | |
| .notNull() | |
| .references(() => users.id), | |
| provider: text("provider").notNull(), // whatsapp, telegram, google, email, phone, social | |
| providerSubject: text("provider_subject").notNull(), | |
| isVerified: boolean("is_verified").default(false).notNull(), | |
| metadata: jsonb("metadata"), | |
| createdAt: createdAt(), | |
| }, | |
| (t) => [uniqueIndex("user_identities_provider_subject_uq").on(t.provider, t.providerSubject)], | |
| ); | |
| export const sessions = pgTable("sessions", { | |
| id: id(), | |
| userId: uuid("user_id") | |
| .notNull() | |
| .references(() => users.id), | |
| sessionType: text("session_type").notNull(), // web, whatsapp, magic_link, oauth_callback | |
| channel: text("channel"), | |
| token: text("token"), | |
| expiresAt: timestamp("expires_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| }); | |
| export const accountLinkChallenges = pgTable("account_link_challenges", { | |
| id: id(), | |
| userId: uuid("user_id").references(() => users.id), | |
| challengeType: text("challenge_type").notNull(), // magic_link, whatsapp_code | |
| destination: text("destination").notNull(), | |
| codeHash: text("code_hash").notNull(), | |
| consumedAt: timestamp("consumed_at", { withTimezone: true }), | |
| expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), | |
| createdAt: createdAt(), | |
| }); | |
| export const oauthTokens = pgTable("oauth_tokens", { | |
| id: id(), | |
| userId: uuid("user_id") | |
| .notNull() | |
| .references(() => users.id), | |
| provider: text("provider").notNull(), | |
| accessTokenRef: text("access_token_ref").notNull(), // encrypted ref, never plaintext | |
| refreshTokenRef: text("refresh_token_ref"), | |
| scopes: jsonb("scopes"), | |
| expiresAt: timestamp("expires_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| }); | |
| // ============================================================================ | |
| // Conversation | |
| // ============================================================================ | |
| export const conversations = pgTable( | |
| "conversations", | |
| { | |
| id: id(), | |
| userId: uuid("user_id").references(() => users.id), | |
| channel: text("channel").notNull(), // whatsapp, telegram, web, email | |
| externalThreadId: text("external_thread_id"), | |
| activeMissionId: uuid("active_mission_id"), | |
| sessionState: jsonb("session_state"), | |
| lastMessageAt: timestamp("last_message_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| }, | |
| // Fix 6: unique index prevents duplicate conversations on concurrent ingest. | |
| (t) => [uniqueIndex("conversations_channel_thread_uq").on(t.channel, t.externalThreadId)], | |
| ); | |
| export const conversationWindows = pgTable("conversation_windows", { | |
| id: id(), | |
| conversationId: uuid("conversation_id") | |
| .notNull() | |
| .references(() => conversations.id), | |
| // WhatsApp 24h free service window. | |
| openedAt: timestamp("opened_at", { withTimezone: true }).notNull(), | |
| expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), | |
| createdAt: createdAt(), | |
| }); | |
| export const messageTemplates = pgTable("message_templates", { | |
| id: id(), | |
| channel: text("channel").notNull(), | |
| name: text("name").notNull(), | |
| language: text("language").default("en").notNull(), | |
| providerTemplateName: text("provider_template_name"), // no hardcoded ids — looked up by name | |
| body: text("body").notNull(), | |
| variables: jsonb("variables"), | |
| createdAt: createdAt(), | |
| }); | |
| export const messages = pgTable( | |
| "messages", | |
| { | |
| id: id(), | |
| conversationId: uuid("conversation_id") | |
| .notNull() | |
| .references(() => conversations.id), | |
| missionId: uuid("mission_id"), | |
| direction: text("direction").notNull(), // inbound, outbound | |
| channel: text("channel").notNull(), | |
| channelMessageId: text("channel_message_id"), | |
| idempotencyKey: text("idempotency_key"), | |
| body: text("body"), | |
| mediaRefs: jsonb("media_refs"), | |
| rawPayload: jsonb("raw_payload"), | |
| createdAt: createdAt(), | |
| }, | |
| (t) => [ | |
| uniqueIndex("messages_idempotency_key_uq").on(t.idempotencyKey), | |
| index("messages_conversation_idx").on(t.conversationId, t.createdAt), | |
| ], | |
| ); | |
| // ============================================================================ | |
| // Mission | |
| // ============================================================================ | |
| export const missions = pgTable("missions", { | |
| id: id(), | |
| userId: uuid("user_id") | |
| .notNull() | |
| .references(() => users.id), | |
| title: text("title").notNull(), | |
| issueDomain: text("issue_domain").notNull(), // consumer, payment, cyber_fraud | |
| state: text("state").notNull().default("created"), | |
| jurisdiction: jsonb("jurisdiction"), | |
| opposingParty: jsonb("opposing_party"), | |
| reliefRequested: jsonb("relief_requested"), | |
| facts: jsonb("facts"), | |
| currentRoutePlanId: uuid("current_route_plan_id"), | |
| safetyLevel: text("safety_level").notNull().default("normal"), // normal, elevated, emergency | |
| createdAt: createdAt(), | |
| updatedAt: updatedAt(), | |
| }); | |
| export const missionEvents = pgTable( | |
| "mission_events", | |
| { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| eventType: text("event_type").notNull(), | |
| actorType: text("actor_type").notNull(), // user, ai, system, responder, admin | |
| actorId: text("actor_id"), | |
| payload: jsonb("payload"), | |
| createdAt: createdAt(), | |
| }, | |
| (t) => [index("mission_events_mission_idx").on(t.missionId, t.createdAt)], | |
| ); | |
| export const missionStatusSnapshots = pgTable( | |
| "mission_status_snapshots", | |
| { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| state: text("state").notNull(), | |
| substate: text("substate"), | |
| headline: text("headline").notNull(), | |
| blockedReason: text("blocked_reason"), | |
| nextUserAction: text("next_user_action"), | |
| nextSystemAction: text("next_system_action"), | |
| primaryButton: text("primary_button"), | |
| danger: boolean("danger").default(false).notNull(), | |
| confidence: numeric("confidence"), | |
| createdAt: createdAt(), | |
| }, | |
| (t) => [index("mission_status_snapshots_mission_idx").on(t.missionId, t.createdAt)], | |
| ); | |
| export const missionLocks = pgTable("mission_locks", { | |
| missionId: uuid("mission_id").primaryKey(), | |
| lockedBy: text("locked_by").notNull(), | |
| lockedAt: timestamp("locked_at", { withTimezone: true }).defaultNow().notNull(), | |
| expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), | |
| }); | |
| // ============================================================================ | |
| // Evidence | |
| // ============================================================================ | |
| export const files = pgTable( | |
| "files", | |
| { | |
| id: id(), | |
| missionId: uuid("mission_id").references(() => missions.id), | |
| uploaderUserId: uuid("uploader_user_id").references(() => users.id), | |
| storageKey: text("storage_key").notNull(), | |
| originalFilename: text("original_filename"), | |
| mimeType: text("mime_type"), | |
| sha256: text("sha256"), | |
| sizeBytes: bigint("size_bytes", { mode: "number" }), | |
| virusScanStatus: text("virus_scan_status").default("pending").notNull(), // pending, clean, infected, skipped | |
| processingStatus: text("processing_status").default("pending").notNull(), // pending, processing, done, failed | |
| retentionPolicy: text("retention_policy").default("standard").notNull(), | |
| deletedAt: timestamp("deleted_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| }, | |
| (t) => [index("files_mission_idx").on(t.missionId)], | |
| ); | |
| export const evidenceItems = pgTable("evidence_items", { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| fileId: uuid("file_id").references(() => files.id), | |
| uploaderUserId: uuid("uploader_user_id").references(() => users.id), | |
| type: text("type").notNull(), // receipt, screenshot, chat, email, pdf, audio, video, id_doc, other | |
| captureDate: timestamp("capture_date", { withTimezone: true }), | |
| sourceChannel: text("source_channel"), | |
| piiLevel: text("pii_level").default("unknown").notNull(), | |
| admissibilityNotes: jsonb("admissibility_notes"), | |
| createdAt: createdAt(), | |
| }); | |
| export const evidenceParsedOutputs = pgTable( | |
| "evidence_parsed_outputs", | |
| { | |
| id: id(), | |
| evidenceItemId: uuid("evidence_item_id") | |
| .notNull() | |
| .references(() => evidenceItems.id), | |
| parserName: text("parser_name").notNull(), | |
| parserVersion: text("parser_version").notNull(), | |
| extractedText: text("extracted_text"), | |
| extractedEntities: jsonb("extracted_entities"), | |
| extractedDates: jsonb("extracted_dates"), | |
| extractedMoney: jsonb("extracted_money"), | |
| // Fix 5: parties column to round-trip ParsedEvidenceDto faithfully. | |
| extractedParties: jsonb("extracted_parties"), | |
| ocrConfidence: numeric("ocr_confidence"), | |
| pageSpans: jsonb("page_spans"), | |
| createdAt: createdAt(), | |
| }, | |
| // Fix 7: unique index prevents duplicate rows on Inngest step replay. | |
| (t) => [ | |
| uniqueIndex("evidence_parsed_outputs_item_parser_uq").on( | |
| t.evidenceItemId, | |
| t.parserName, | |
| t.parserVersion, | |
| ), | |
| ], | |
| ); | |
| export const evidenceClaims = pgTable("evidence_claims", { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| evidenceItemId: uuid("evidence_item_id").references(() => evidenceItems.id), | |
| claimText: text("claim_text").notNull(), | |
| claimType: text("claim_type"), | |
| supportLevel: text("support_level").notNull(), // explicit, inferred, weak, contradicted | |
| sourceSpan: jsonb("source_span"), | |
| createdAt: createdAt(), | |
| }); | |
| // ============================================================================ | |
| // Legal / RAG (curated, small) | |
| // ============================================================================ | |
| export const legalSources = pgTable("legal_sources", { | |
| id: id(), | |
| sourceType: text("source_type").notNull(), // act, rule, regulator_doc, govt_page, court_judgment, policy | |
| title: text("title").notNull(), | |
| tier: text("tier").notNull(), // tier1, tier2, tier3 | |
| jurisdiction: text("jurisdiction").default("IN"), | |
| authority: text("authority"), | |
| canonicalUrl: text("canonical_url"), | |
| versionLabel: text("version_label"), | |
| effectiveFrom: date("effective_from"), | |
| effectiveTo: date("effective_to"), | |
| sha256: text("sha256"), | |
| fetchedAt: timestamp("fetched_at", { withTimezone: true }), | |
| metadata: jsonb("metadata"), | |
| createdAt: createdAt(), | |
| }); | |
| export const legalChunks = pgTable( | |
| "legal_chunks", | |
| { | |
| id: id(), | |
| sourceId: uuid("source_id") | |
| .notNull() | |
| .references(() => legalSources.id), | |
| chunkNo: integer("chunk_no").notNull(), | |
| actName: text("act_name"), | |
| sectionNo: text("section_no"), | |
| heading: text("heading"), | |
| text: text("text").notNull(), | |
| tokenCount: integer("token_count"), | |
| pageStart: integer("page_start"), | |
| pageEnd: integer("page_end"), | |
| embedding: vector("embedding", { dimensions: EMBEDDING_DIM }), | |
| /** | |
| * DB-generated tsvector for FTS — populated via GENERATED ALWAYS AS | |
| * (see migration 0002_legal_indexes.sql). Drizzle treats this as a | |
| * read-only column; writes are handled by Postgres. | |
| */ | |
| textTsv: tsvector("text_tsv").generatedAlwaysAs( | |
| sql`to_tsvector('english', coalesce(text, ''))`, | |
| ), | |
| metadata: jsonb("metadata"), | |
| createdAt: createdAt(), | |
| }, | |
| (t) => [ | |
| index("legal_chunks_source_idx").on(t.sourceId), | |
| index("legal_chunks_tsv_idx").using("gin", t.textTsv), | |
| index("legal_chunks_embedding_idx").using("ivfflat", t.embedding.op("vector_cosine_ops")), | |
| ], | |
| ); | |
| export const legalRetrievalLogs = pgTable("legal_retrieval_logs", { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| query: text("query").notNull(), | |
| retrievedChunkIds: uuid("retrieved_chunk_ids").array(), | |
| rerankedChunkIds: uuid("reranked_chunk_ids").array(), | |
| scores: jsonb("scores"), | |
| modelVersions: jsonb("model_versions"), | |
| createdAt: createdAt(), | |
| }); | |
| export const legalClaimVerifications = pgTable("legal_claim_verifications", { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| claim: text("claim").notNull(), | |
| supported: boolean("supported").notNull(), | |
| citedChunkIds: uuid("cited_chunk_ids").array(), | |
| quoteSpans: jsonb("quote_spans"), | |
| verifierScore: numeric("verifier_score"), | |
| failureReason: text("failure_reason"), | |
| createdAt: createdAt(), | |
| }); | |
| // ============================================================================ | |
| // Route / action | |
| // ============================================================================ | |
| export const routeRules = pgTable("route_rules", { | |
| id: id(), | |
| issueDomain: text("issue_domain").notNull(), | |
| jurisdictionFilter: jsonb("jurisdiction_filter"), | |
| triggerConditions: jsonb("trigger_conditions"), | |
| priority: integer("priority").default(0).notNull(), | |
| routeGraph: jsonb("route_graph").notNull(), | |
| requiredEvidence: jsonb("required_evidence"), | |
| deadlines: jsonb("deadlines"), | |
| sourceCitations: uuid("source_citations").array(), | |
| socialTemplate: text("social_template"), | |
| stopAndRefer: jsonb("stop_and_refer"), | |
| active: boolean("active").default(true).notNull(), | |
| createdAt: createdAt(), | |
| updatedAt: updatedAt(), | |
| }); | |
| export const authorities = pgTable("authorities", { | |
| id: id(), | |
| name: text("name").notNull(), | |
| authorityType: text("authority_type").notNull(), // regulator, company, govt_dept, court, ombudsman, police, platform | |
| jurisdiction: jsonb("jurisdiction"), | |
| contactChannels: jsonb("contact_channels"), | |
| officialUrls: jsonb("official_urls"), | |
| escalationPolicy: jsonb("escalation_policy"), | |
| verificationStatus: text("verification_status").default("unverified").notNull(), | |
| lastVerifiedAt: timestamp("last_verified_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| }); | |
| export const routePlans = pgTable("route_plans", { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| summary: text("summary"), | |
| steps: jsonb("steps").notNull(), | |
| citations: jsonb("citations"), | |
| riskNotes: jsonb("risk_notes"), | |
| expectedTimeline: jsonb("expected_timeline"), | |
| confidence: numeric("confidence"), | |
| supersededAt: timestamp("superseded_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| }); | |
| export const consentGrants = pgTable("consent_grants", { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| userId: uuid("user_id") | |
| .notNull() | |
| .references(() => users.id), | |
| actionType: text("action_type").notNull(), | |
| actionPreviewHash: text("action_preview_hash").notNull(), | |
| consentText: text("consent_text").notNull(), | |
| channel: text("channel").notNull(), | |
| grantedAt: timestamp("granted_at", { withTimezone: true }), | |
| expiresAt: timestamp("expires_at", { withTimezone: true }), | |
| revokedAt: timestamp("revoked_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| }); | |
| export const actions = pgTable( | |
| "actions", | |
| { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| routePlanId: uuid("route_plan_id").references(() => routePlans.id), | |
| actionType: text("action_type").notNull(), | |
| title: text("title").notNull(), | |
| description: text("description"), | |
| target: jsonb("target"), | |
| inputPayload: jsonb("input_payload"), | |
| canonicalPayloadHash: text("canonical_payload_hash"), | |
| previewStorageKey: text("preview_storage_key"), | |
| consentRequired: boolean("consent_required").default(true).notNull(), | |
| consentGrantId: uuid("consent_grant_id").references(() => consentGrants.id), | |
| status: text("status").notNull().default("proposed"), | |
| proofState: text("proof_state"), | |
| proofRef: jsonb("proof_ref"), | |
| idempotencyKey: text("idempotency_key"), | |
| riskLevel: text("risk_level").default("low").notNull(), | |
| createdBy: text("created_by").notNull(), // user, ai, system, admin | |
| createdAt: createdAt(), | |
| updatedAt: updatedAt(), | |
| }, | |
| (t) => [ | |
| uniqueIndex("actions_idempotency_key_uq").on(t.idempotencyKey), | |
| index("actions_mission_idx").on(t.missionId), | |
| ], | |
| ); | |
| export const actionAttempts = pgTable("action_attempts", { | |
| id: id(), | |
| actionId: uuid("action_id") | |
| .notNull() | |
| .references(() => actions.id), | |
| adapterName: text("adapter_name").notNull(), | |
| attemptNo: integer("attempt_no").notNull(), | |
| requestPayloadHash: text("request_payload_hash"), | |
| responsePayload: jsonb("response_payload"), | |
| providerReference: text("provider_reference"), | |
| status: text("status").notNull(), | |
| errorCode: text("error_code"), | |
| errorMessage: text("error_message"), | |
| startedAt: timestamp("started_at", { withTimezone: true }), | |
| finishedAt: timestamp("finished_at", { withTimezone: true }), | |
| }); | |
| export const submissions = pgTable("submissions", { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| routePlanId: uuid("route_plan_id").references(() => routePlans.id), | |
| actionId: uuid("action_id").references(() => actions.id), | |
| adapterName: text("adapter_name"), | |
| targetAuthorityId: uuid("target_authority_id").references(() => authorities.id), | |
| targetContact: jsonb("target_contact"), | |
| status: text("status").notNull(), | |
| proofState: text("proof_state").notNull(), | |
| externalReference: text("external_reference"), | |
| receiptStorageKey: text("receipt_storage_key"), | |
| payloadHash: text("payload_hash"), | |
| error: jsonb("error"), | |
| sentAt: timestamp("sent_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| }); | |
| // ============================================================================ | |
| // Portal assist sessions (Phase 2 Module 5) | |
| // ============================================================================ | |
| export const portalAssistSessions = pgTable("portal_assist_sessions", { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| actionId: uuid("action_id") | |
| .notNull() | |
| .references(() => actions.id), | |
| portalUrl: text("portal_url").notNull(), | |
| state: text("state").notNull().default("packet_ready"), // PortalAssistState | |
| formFields: jsonb("form_fields").default([]), // { selector, label, value, filled }[] | |
| screenshotStorageKey: text("screenshot_storage_key"), | |
| receiptStorageKey: text("receipt_storage_key"), | |
| errorMessage: text("error_message"), | |
| createdAt: createdAt(), | |
| updatedAt: updatedAt(), | |
| }, (t) => [ | |
| index("portal_assist_sessions_mission_idx").on(t.missionId), | |
| index("portal_assist_sessions_state_idx").on(t.state), | |
| ]); | |
| // ============================================================================ | |
| // Company Resolver Portal (Phase 2 Module 6) | |
| // ============================================================================ | |
| export const organizations = pgTable("organizations", { | |
| id: id(), | |
| normalizedName: text("normalized_name").notNull().unique(), | |
| websiteDomain: text("website_domain"), | |
| officialEmails: text("official_emails").array(), // verified contact emails | |
| verificationStatus: text("verification_status").notNull().default("unverified"), // unverified, pending, verified, rejected | |
| metadata: jsonb("metadata"), | |
| createdAt: createdAt(), | |
| updatedAt: updatedAt(), | |
| }, (t) => [ | |
| index("organizations_domain_idx").on(t.websiteDomain), | |
| index("organizations_status_idx").on(t.verificationStatus), | |
| ]); | |
| export const organizationMembers = pgTable("organization_members", { | |
| id: id(), | |
| organizationId: uuid("organization_id") | |
| .notNull() | |
| .references(() => organizations.id), | |
| userId: uuid("user_id") | |
| .notNull() | |
| .references(() => users.id), | |
| email: text("email").notNull(), | |
| role: text("role").notNull().default("member"), // admin, member, viewer | |
| verificationMethod: text("verification_method"), // domain_email, magic_link, manual | |
| verifiedAt: timestamp("verified_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| }, (t) => [ | |
| uniqueIndex("org_members_org_user_uq").on(t.organizationId, t.userId), | |
| index("org_members_user_idx").on(t.userId), | |
| ]); | |
| export const resolverThreads = pgTable("resolver_threads", { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| organizationId: uuid("organization_id") | |
| .notNull() | |
| .references(() => organizations.id), | |
| status: text("status").notNull().default("open"), // open, awaiting_user, resolved, closed | |
| maskedSummary: text("masked_summary"), // PII-free summary shown to company | |
| createdAt: createdAt(), | |
| updatedAt: updatedAt(), | |
| }, (t) => [ | |
| uniqueIndex("resolver_threads_mission_org_uq").on(t.missionId, t.organizationId), | |
| index("resolver_threads_org_idx").on(t.organizationId), | |
| ]); | |
| export const resolverMessages = pgTable("resolver_messages", { | |
| id: id(), | |
| threadId: uuid("thread_id") | |
| .notNull() | |
| .references(() => resolverThreads.id), | |
| senderType: text("sender_type").notNull(), // company, user, system | |
| senderId: uuid("sender_id"), // userId or organizationId | |
| body: text("body").notNull(), | |
| visibility: text("visibility").notNull().default("both"), // both, user_only, resolver_only, public | |
| createdAt: createdAt(), | |
| }, (t) => [ | |
| index("resolver_messages_thread_idx").on(t.threadId, t.createdAt), | |
| ]); | |
| // ============================================================================ | |
| // Social publishing (Phase 2 Module 7) | |
| // ============================================================================ | |
| export const socialAccounts = pgTable("social_accounts", { | |
| id: id(), | |
| platform: text("platform").notNull(), // threads, instagram, x | |
| providerUserId: text("provider_user_id"), | |
| accessTokenRef: text("access_token_ref"), // encrypted ref, never plaintext (CLAUDE.md) | |
| refreshTokenRef: text("refresh_token_ref"), | |
| tokenExpiresAt: timestamp("token_expires_at", { withTimezone: true }), | |
| scopes: text("scopes").array(), | |
| status: text("status").notNull().default("active"), // active, expired, revoked | |
| metadata: jsonb("metadata"), | |
| createdAt: createdAt(), | |
| }, (t) => [ | |
| index("social_accounts_platform_idx").on(t.platform), | |
| ]); | |
| export const socialPosts = pgTable("social_posts", { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| actionId: uuid("action_id") | |
| .notNull() | |
| .references(() => actions.id), | |
| socialAccountId: uuid("social_account_id").references(() => socialAccounts.id), | |
| consentGrantId: uuid("consent_grant_id") | |
| .notNull() | |
| .references(() => consentGrants.id), | |
| platform: text("platform").notNull(), // threads, instagram, x | |
| draftText: text("draft_text").notNull(), | |
| redactedText: text("redacted_text").notNull(), | |
| legalRiskReview: jsonb("legal_risk_review"), | |
| cardStorageKey: text("card_storage_key"), // generated image storage key | |
| externalPostId: text("external_post_id"), | |
| status: text("status").notNull().default("draft"), // draft, approved, published, failed, copy_ready | |
| postUrl: text("post_url"), | |
| proofState: text("proof_state").notNull().default("draft_only"), | |
| publishedAt: timestamp("published_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| }, (t) => [ | |
| index("social_posts_mission_idx").on(t.missionId), | |
| index("social_posts_status_idx").on(t.status), | |
| ]); | |
| // ============================================================================ | |
| // eCourts / Case lookups (Phase 2 Module 8) | |
| // ============================================================================ | |
| export const caseLookups = pgTable("case_lookups", { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| cnrNumber: text("cnr_number").notNull(), | |
| caseNumber: text("case_number"), | |
| courtName: text("court_name"), | |
| petitioner: text("petitioner"), | |
| respondent: text("respondent"), | |
| caseStatus: text("case_status"), // pending, disposed, stayed, transferred | |
| nextHearingDate: date("next_hearing_date"), | |
| lastOrderDate: date("last_order_date"), | |
| lastOrderSummary: text("last_order_summary"), | |
| rawData: jsonb("raw_data"), | |
| source: text("source").notNull(), // surepass, manual | |
| lookupStatus: text("lookup_status").default("pending").notNull(), // pending, success, failed, not_found | |
| errorMessage: text("error_message"), | |
| createdAt: createdAt(), | |
| updatedAt: updatedAt(), | |
| }, (t) => [ | |
| index("case_lookups_mission_idx").on(t.missionId), | |
| index("case_lookups_cnr_idx").on(t.cnrNumber), | |
| ]); | |
| // ============================================================================ | |
| // Public / resolution | |
| // ============================================================================ | |
| export const publicCasePages = pgTable( | |
| "public_case_pages", | |
| { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| slug: text("slug").notNull(), | |
| visibility: text("visibility").default("private_link").notNull(), // Phase 1: private_link only | |
| redactedSummary: text("redacted_summary"), | |
| consentGrantId: uuid("consent_grant_id").references(() => consentGrants.id), | |
| takenDownAt: timestamp("taken_down_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| }, | |
| (t) => [uniqueIndex("public_case_pages_slug_uq").on(t.slug)], | |
| ); | |
| export const redactionSpans = pgTable("redaction_spans", { | |
| id: id(), | |
| sourceType: text("source_type").notNull(), // public_page, email_body, social_post | |
| sourceId: uuid("source_id").notNull(), | |
| start: integer("start").notNull(), | |
| end: integer("end").notNull(), | |
| piiType: text("pii_type").notNull(), | |
| replacement: text("replacement").notNull(), | |
| reviewed: boolean("reviewed").default(false).notNull(), | |
| createdAt: createdAt(), | |
| }); | |
| export const followups = pgTable("followups", { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| type: text("type").notNull(), // reminder, escalation, deadline | |
| dueAt: timestamp("due_at", { withTimezone: true }).notNull(), | |
| status: text("status").default("scheduled").notNull(), // scheduled, completed, escalated, snoozed, cancelled | |
| payload: jsonb("payload"), | |
| createdAt: createdAt(), | |
| }); | |
| export const resolutionOutcomes = pgTable("resolution_outcomes", { | |
| id: id(), | |
| missionId: uuid("mission_id") | |
| .notNull() | |
| .references(() => missions.id), | |
| outcomeType: text("outcome_type").notNull(), | |
| amountRecovered: numeric("amount_recovered"), | |
| userSatisfaction: integer("user_satisfaction"), | |
| routeEffectiveness: jsonb("route_effectiveness"), | |
| anonymizedPublicSummary: text("anonymized_public_summary"), | |
| publishConsent: boolean("publish_consent").default(false).notNull(), | |
| createdAt: createdAt(), | |
| }); | |
| // ============================================================================ | |
| // Reliability | |
| // ============================================================================ | |
| export const outboxEvents = pgTable( | |
| "outbox_events", | |
| { | |
| id: id(), | |
| eventType: text("event_type").notNull(), | |
| aggregateType: text("aggregate_type").notNull(), | |
| aggregateId: uuid("aggregate_id").notNull(), | |
| payload: jsonb("payload"), | |
| status: text("status").default("pending").notNull(), // pending, processing, sent, failed, dead_letter | |
| attempts: integer("attempts").default(0).notNull(), | |
| nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }).defaultNow().notNull(), | |
| lockedAt: timestamp("locked_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| }, | |
| (t) => [index("outbox_events_status_idx").on(t.status, t.nextAttemptAt)], | |
| ); | |
| export const idempotencyKeys = pgTable("idempotency_keys", { | |
| key: text("key").primaryKey(), | |
| requestHash: text("request_hash").notNull(), | |
| responseHash: text("response_hash"), | |
| status: text("status").default("in_progress").notNull(), | |
| expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), | |
| createdAt: createdAt(), | |
| }); | |
| export const webhookEvents = pgTable( | |
| "webhook_events", | |
| { | |
| id: id(), | |
| provider: text("provider").notNull(), // whatsapp, telegram, resend | |
| providerEventId: text("provider_event_id").notNull(), | |
| payload: jsonb("payload"), | |
| processedAt: timestamp("processed_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| }, | |
| (t) => [uniqueIndex("webhook_events_provider_event_uq").on(t.provider, t.providerEventId)], | |
| ); | |
| export const auditLogs = pgTable("audit_logs", { | |
| id: id(), | |
| actorType: text("actor_type").notNull(), | |
| actorId: text("actor_id"), | |
| action: text("action").notNull(), | |
| resourceType: text("resource_type").notNull(), | |
| resourceId: text("resource_id"), | |
| before: jsonb("before"), | |
| after: jsonb("after"), | |
| ipHash: text("ip_hash"), | |
| userAgentHash: text("user_agent_hash"), | |
| createdAt: createdAt(), | |
| }); | |
| // ============================================================================ | |
| // Safety / compliance | |
| // ============================================================================ | |
| export const humanReviewItems = pgTable("human_review_items", { | |
| id: id(), | |
| missionId: uuid("mission_id").references(() => missions.id), | |
| reviewType: text("review_type").notNull(), // stop_and_refer, defamation_risk, low_confidence, manual_submit | |
| priority: text("priority").default("normal").notNull(), // low, normal, high, urgent | |
| status: text("status").default("open").notNull(), // open, in_review, resolved, dismissed | |
| context: jsonb("context"), | |
| createdAt: createdAt(), | |
| updatedAt: updatedAt(), | |
| }); | |
| export const consentNotices = pgTable("consent_notices", { | |
| id: id(), | |
| userId: uuid("user_id").references(() => users.id), | |
| version: text("version").notNull(), | |
| channel: text("channel").notNull(), | |
| ackedAt: timestamp("acked_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| }); | |
| export const dataSubjectRequests = pgTable("data_subject_requests", { | |
| id: id(), | |
| userId: uuid("user_id") | |
| .notNull() | |
| .references(() => users.id), | |
| type: text("type").notNull(), // export, erase | |
| status: text("status").default("pending").notNull(), | |
| dueAt: timestamp("due_at", { withTimezone: true }), | |
| resultRef: jsonb("result_ref"), | |
| createdAt: createdAt(), | |
| }); | |
| export const notificationPrefs = pgTable("notification_prefs", { | |
| id: id(), | |
| userId: uuid("user_id") | |
| .notNull() | |
| .references(() => users.id), | |
| channel: text("channel").notNull(), | |
| timezone: text("timezone").default("Asia/Kolkata").notNull(), | |
| quietHours: jsonb("quiet_hours"), | |
| enabled: boolean("enabled").default(true).notNull(), | |
| createdAt: createdAt(), | |
| }); | |
| export const emailEvents = pgTable( | |
| "email_events", | |
| { | |
| id: id(), | |
| submissionId: uuid("submission_id").references(() => submissions.id), | |
| type: text("type").notNull(), // delivered, bounced, complained, replied | |
| providerEventId: text("provider_event_id"), | |
| payload: jsonb("payload"), | |
| createdAt: createdAt(), | |
| }, | |
| (t) => [uniqueIndex("email_events_provider_event_uq").on(t.providerEventId)], | |
| ); | |
| export const takedownRequests = pgTable( | |
| "takedown_requests", | |
| { | |
| id: id(), | |
| missionId: uuid("mission_id").notNull().references(() => missions.id), | |
| actionId: uuid("action_id").notNull().references(() => actions.id), | |
| intermediaryName: text("intermediary_name").notNull(), | |
| grievanceOfficerName: text("grievance_officer_name"), | |
| grievanceOfficerEmail: text("grievance_officer_email").notNull(), | |
| contentUrl: text("content_url").notNull(), | |
| legalGrounds: text("legal_grounds").notNull(), | |
| status: text("status").default("pending").notNull(), // pending, sent, acknowledged, complied, rejected | |
| complianceDeadline: timestamp("compliance_deadline", { withTimezone: true }), | |
| compliedAt: timestamp("complied_at", { withTimezone: true }), | |
| createdAt: createdAt(), | |
| updatedAt: updatedAt(), | |
| } | |
| ); | |