diff --git a/docs/implementation/task-ep1.5-timeline-projection-foundation-2026-07-13.md b/docs/implementation/task-ep1.5-timeline-projection-foundation-2026-07-13.md new file mode 100644 index 0000000..be14b45 --- /dev/null +++ b/docs/implementation/task-ep1.5-timeline-projection-foundation-2026-07-13.md @@ -0,0 +1,155 @@ +# Task EP.1.5 Timeline Projection Foundation - 2026-07-13 + +## Scope + +- introduced first production CRM Business Projection: Timeline +- added `crm_timeline_projection` materialized read model schema +- added Timeline event builder and Activity event mapping +- added Timeline projection consumer contract and active CRM registration +- added Timeline list/query storage service +- added Timeline rebuild and legacy adapter contracts +- added reusable Timeline UI components +- added optional Activity detail Timeline integration seam +- preserved Activity, Audit Log, Approval History, follow-up APIs, Dashboard, Reports, Notifications, and current detail-page behavior + +## Review Summary + +Reviewed before and during implementation: + +- `AGENTS.md` +- `plans/task-ep.1.5.md` +- `LAYOUT.md` +- `docs/standards/engineering-constitution.md` +- `docs/standards/project-foundations.md` +- `docs/standards/architecture-rules.md` +- `docs/standards/ui-ux-rules.md` +- `docs/business/relationship-sales-workspace-blueprint-v1.md` +- `docs/security/crm-authorization-boundaries.md` +- `docs/implementation/task-ep1.4-projection-foundation-delivery-reliability-2026-07-13.md` +- `docs/implementation/task-ep1.4.1-transactional-outbox-activation-worker-runtime-2026-07-13.md` +- existing Activity business event adapter +- existing Projection Runtime and Worker +- existing CRM detail/audit UI patterns + +## Implementation Summary + +### Timeline Schema + +Added table: + +- `crm_timeline_projection` + +Generated migration: + +- `drizzle/0005_freezing_wendigo.sql` + +The table stores organization-scoped, rebuildable read-model rows with entity references, event provenance, visibility metadata, display fields, and projection version. It has indexes for event id/version idempotency, organization chronology, customer chronology, and activity/event lookup. + +### Timeline Builder + +Added: + +- `src/features/crm/timeline/server/builder.ts` + +Builder maps Business Events into business-readable Timeline items. Initial active mappings cover Activity lifecycle events: + +- `activity.created` +- `activity.assigned` +- `activity.reassigned` +- `activity.started` +- `activity.rescheduled` +- `activity.completed` +- `activity.cancelled` +- `activity.deleted` + +Generic mapping is available for registered future CRM events until their business copy is specialized. + +### Timeline Consumer + +Added: + +- `src/features/crm/timeline/server/consumer.ts` +- `src/features/crm/timeline/server/registration.ts` + +The consumer builds a Timeline item and persists it with idempotent `eventId + sourceProjectionVersion` storage. Foundation default consumers remain safe skeletons; CRM worker startup can opt into `CRM_TIMELINE_PROJECTION_CONSUMERS` for the active Timeline projection set. + +### Timeline Storage And Query Contract + +Added: + +- `src/features/crm/timeline/api/types.ts` +- `src/features/crm/timeline/server/service.ts` + +Supported filters include organization, customer, lead, opportunity, quotation, activity, event, category, actor, search, date range, and cursor/limit contract. Timeline remains read-only. + +### Legacy Adapter And Rebuild Contracts + +Added: + +- `src/features/crm/timeline/server/legacy-adapters.ts` +- `src/features/crm/timeline/server/rebuild.ts` + +Legacy adapters are explicitly read-only rebuild inputs for lead follow-up, opportunity follow-up, quotation follow-up, activity, approval, and audit history. No data migration was introduced. + +### Timeline UI + +Added: + +- `src/features/crm/timeline/components/timeline-list.tsx` + +Components: + +- `TimelineList` +- `TimelineCard` +- `TimelineEmpty` +- `TimelineSkeleton` + +The UI follows the existing dashboard card language, uses deterministic shared date formatting, shadcn primitives, and app icon registry. It preserves a calm enterprise density while adding a clear vertical chronology. + +### Workspace Integration + +Added an optional `timelineItems` prop to Activity detail. Existing callers are unaffected; when supplied, Activity detail renders an additive Timeline card below the existing Activity detail card. + +## Security Notes + +- Timeline rows are organization-scoped. +- Timeline visibility stores product type, pricing sensitivity, and internal-only flags. +- Pricing-sensitive/internal-only event payloads continue to rely on source event minimization. +- Timeline query service is storage-ready; route/API exposure must still apply CRM resolved access, branch/product scope, ownership, and pricing visibility before becoming public. +- Timeline never mutates source domains and does not replace Audit Log. + +## Delivery Matrix + +Initial production path: + +| Business Event | Timeline Item | Workspace | Consumer | Projection | +| --- | --- | --- | --- | --- | +| `activity.created` | Activity created | Customer / Lead / Opportunity / Quotation / Activity | `timeline.projection.consumer` | `crm_timeline_projection` | +| `activity.completed` | Activity completed | Customer / Lead / Opportunity / Quotation / Activity | `timeline.projection.consumer` | `crm_timeline_projection` | +| `activity.cancelled` | Activity cancelled | Customer / Lead / Opportunity / Quotation / Activity | `timeline.projection.consumer` | `crm_timeline_projection` | + +Additional Activity events are supported in code and documented in the builder mapping. + +## UX Review + +- Layout: follows `LAYOUT.md` detail-card and timeline entry patterns. +- Density: Timeline cards stay compact, scannable, and fit existing CRM dashboard surfaces. +- Accessibility: items use semantic `article`, `time`, non-color badges, and text labels. +- Loading/empty states: `TimelineSkeleton` and `TimelineEmpty` included. +- Dark mode: tone classes include dark-mode variants. +- Date rendering: uses `src/lib/date-format.ts`. + +## Verification + +- `npm run typecheck` +- `node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types --test src/features/foundation/business-events/*.test.ts src/features/foundation/projections/*.test.ts src/features/crm/activities/server/business-events.test.ts src/features/crm/timeline/server/*.test.ts` +- `npx oxlint src/features/crm/timeline src/features/foundation/projections/registry.ts src/features/foundation/projections/consumers.ts src/features/foundation/projections/server/store.ts src/features/crm/activities/components/activity-detail.tsx src/db/schema.ts` +- `npm run db:generate` + +## Residual Risks / Follow-up + +- Public Timeline route handlers and server-side CRM resolved-access filtering remain future work before exposing Timeline broadly. +- Customer, Lead, Opportunity, and Quotation detail pages are not yet wired to fetch Timeline data; Activity detail has a backward-compatible render seam. +- Approval, quotation, opportunity, customer, and PO event mappings have generic fallback support but need specialized business copy when those domains publish durable events. +- Legacy adapters are contract-only in this slice; source-query rebuild implementation should be expanded as each workspace adopts Timeline. +- EP.1.6 Calendar can reuse the same projection runtime and Activity event delivery path. diff --git a/drizzle/0005_freezing_wendigo.sql b/drizzle/0005_freezing_wendigo.sql new file mode 100644 index 0000000..ae0c4e8 --- /dev/null +++ b/drizzle/0005_freezing_wendigo.sql @@ -0,0 +1,33 @@ +CREATE TABLE "crm_timeline_projection" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "branch_id" text, + "customer_id" text, + "lead_id" text, + "opportunity_id" text, + "quotation_id" text, + "activity_id" text, + "event_id" text NOT NULL, + "entity_type" text NOT NULL, + "entity_id" text NOT NULL, + "timeline_type" text NOT NULL, + "timeline_category" text NOT NULL, + "occurred_at" timestamp with time zone NOT NULL, + "actor_id" text, + "actor_display" text, + "title" text NOT NULL, + "summary" text, + "icon" text NOT NULL, + "color" text NOT NULL, + "priority" text DEFAULT 'normal' NOT NULL, + "visibility" jsonb NOT NULL, + "metadata" jsonb, + "source_event" jsonb NOT NULL, + "source_projection_version" integer DEFAULT 1 NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX "crm_timeline_projection_event_version_idx" ON "crm_timeline_projection" USING btree ("event_id","source_projection_version");--> statement-breakpoint +CREATE INDEX "crm_timeline_projection_org_occurred_idx" ON "crm_timeline_projection" USING btree ("organization_id","occurred_at");--> statement-breakpoint +CREATE INDEX "crm_timeline_projection_customer_occurred_idx" ON "crm_timeline_projection" USING btree ("organization_id","customer_id","occurred_at");--> statement-breakpoint +CREATE INDEX "crm_timeline_projection_activity_event_idx" ON "crm_timeline_projection" USING btree ("activity_id","event_id"); \ No newline at end of file diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000..7a1cec5 --- /dev/null +++ b/drizzle/meta/0005_snapshot.json @@ -0,0 +1,7795 @@ +{ + "id": "e00210f9-6176-4489-9289-f616755bacfa", + "prevId": "184ea587-d697-48d3-9d1a-1b81a598da72", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.app_notification_deliveries": { + "name": "app_notification_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_id": { + "name": "notification_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_address": { + "name": "recipient_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_notification_dispatches": { + "name": "app_notification_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_key": { + "name": "provider_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "recipient_to": { + "name": "recipient_to", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "recipient_cc": { + "name": "recipient_cc", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "recipient_bcc": { + "name": "recipient_bcc", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reply_to": { + "name": "reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attachments": { + "name": "attachments", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recipient_address": { + "name": "recipient_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_retry_count": { + "name": "max_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "app_notification_dispatches_org_event_channel_recipient_idx": { + "name": "app_notification_dispatches_org_event_channel_recipient_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_notification_events": { + "name": "app_notification_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "app_notification_events_org_dedupe_idx": { + "name": "app_notification_events_org_dedupe_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_notification_templates": { + "name": "app_notification_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_template": { + "name": "subject_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title_template": { + "name": "title_template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_template": { + "name": "body_template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_format": { + "name": "body_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'plain'" + }, + "link_template": { + "name": "link_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'th'" + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "app_notification_templates_org_event_channel_idx": { + "name": "app_notification_templates_org_event_channel_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_notifications": { + "name": "app_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "link_url": { + "name": "link_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unread'" + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "app_notifications_recipient_event_idx": { + "name": "app_notifications_recipient_event_idx", + "columns": [ + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.business_event_outbox": { + "name": "business_event_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "causation_id": { + "name": "causation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_envelope": { + "name": "event_envelope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_status": { + "name": "delivery_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failed_at": { + "name": "failed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dead_lettered_at": { + "name": "dead_lettered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "business_event_outbox_status_available_idx": { + "name": "business_event_outbox_status_available_idx", + "columns": [ + { + "expression": "delivery_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_event_outbox_claim_lease_idx": { + "name": "business_event_outbox_claim_lease_idx", + "columns": [ + { + "expression": "delivery_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claim_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_event_outbox_org_event_idx": { + "name": "business_event_outbox_org_event_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_activities": { + "name": "crm_activities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_entity_type": { + "name": "primary_entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_entity_id": { + "name": "primary_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "primary_record_label": { + "name": "primary_record_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_records": { + "name": "related_records", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lead_id": { + "name": "lead_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "opportunity_id": { + "name": "opportunity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activity_type": { + "name": "activity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_to_id": { + "name": "assigned_to_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_start_at": { + "name": "scheduled_start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scheduled_end_at": { + "name": "scheduled_end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "skipped_at": { + "name": "skipped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome_summary": { + "name": "outcome_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_internal_only": { + "name": "is_internal_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_pricing_sensitive": { + "name": "is_pricing_sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "parent_activity_id": { + "name": "parent_activity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_actions": { + "name": "crm_approval_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approval_request_id": { + "name": "approval_request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acted_by": { + "name": "acted_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acted_at": { + "name": "acted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_escalation_policies": { + "name": "crm_approval_escalation_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger_after_hours": { + "name": "trigger_after_hours", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_value": { + "name": "target_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_matrices": { + "name": "crm_approval_matrices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "min_amount": { + "name": "min_amount", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_amount": { + "name": "max_amount", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_reminder_policies": { + "name": "crm_approval_reminder_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sla_hours": { + "name": "sla_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + }, + "reminder_offsets_hours": { + "name": "reminder_offsets_hours", + "type": "integer[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "calendar_mode": { + "name": "calendar_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'calendar_days'" + }, + "timeout_action": { + "name": "timeout_action", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_approval_reminder_policies_workflow_step_idx": { + "name": "crm_approval_reminder_policies_workflow_step_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_requests": { + "name": "crm_approval_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_step": { + "name": "current_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "current_step_started_at": { + "name": "current_step_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_steps": { + "name": "crm_approval_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_code": { + "name": "role_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approval_mode": { + "name": "approval_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sequential'" + }, + "is_required": { + "name": "is_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sla_hours": { + "name": "sla_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + }, + "calendar_mode": { + "name": "calendar_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'calendar_days'" + }, + "timeout_action": { + "name": "timeout_action", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_approval_steps_workflow_step_idx": { + "name": "crm_approval_steps_workflow_step_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_approval_workflows": { + "name": "crm_approval_workflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_approval_workflows_org_code_idx": { + "name": "crm_approval_workflows_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_contact_shares": { + "name": "crm_contact_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_to_user_id": { + "name": "shared_to_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_by_user_id": { + "name": "shared_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_at": { + "name": "shared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_contact_shares_org_contact_shared_user_idx": { + "name": "crm_contact_shares_org_contact_shared_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "shared_to_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_customer_contacts": { + "name": "crm_customer_contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "department": { + "name": "department", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mobile": { + "name": "mobile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_customer_owner_history": { + "name": "crm_customer_owner_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "old_owner_user_id": { + "name": "old_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "new_owner_user_id": { + "name": "new_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_by": { + "name": "changed_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "changed_at": { + "name": "changed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_customers": { + "name": "crm_customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "abbr": { + "name": "abbr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tax_id": { + "name": "tax_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_type": { + "name": "customer_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_status": { + "name": "customer_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "province": { + "name": "province", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "district": { + "name": "district", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub_district": { + "name": "sub_district", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fax": { + "name": "fax", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lead_channel": { + "name": "lead_channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_group": { + "name": "customer_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_sub_group": { + "name": "customer_sub_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_assigned_at": { + "name": "owner_assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "owner_assigned_by": { + "name": "owner_assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_customers_org_code_idx": { + "name": "crm_customers_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_artifacts": { + "name": "crm_document_artifacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_type": { + "name": "document_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_type": { + "name": "artifact_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_version_id": { + "name": "template_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_provider": { + "name": "storage_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "generated_by": { + "name": "generated_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generated_at": { + "name": "generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "locked_by": { + "name": "locked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "voided_at": { + "name": "voided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "voided_by": { + "name": "voided_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "void_reason": { + "name": "void_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_libraries": { + "name": "crm_document_libraries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_type": { + "name": "document_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_document_libraries_org_code_idx": { + "name": "crm_document_libraries_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_library_versions": { + "name": "crm_document_library_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "library_id": { + "name": "library_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_provider": { + "name": "storage_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page_count": { + "name": "page_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_by": { + "name": "archived_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_document_library_versions_library_version_idx": { + "name": "crm_document_library_versions_library_version_idx", + "columns": [ + { + "expression": "library_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_template_mappings": { + "name": "crm_document_template_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_version_id": { + "name": "template_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "placeholder_key": { + "name": "placeholder_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data_type": { + "name": "data_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sheet_name": { + "name": "sheet_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_value": { + "name": "default_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format_mask": { + "name": "format_mask", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_document_template_mappings_version_placeholder_idx": { + "name": "crm_document_template_mappings_version_placeholder_idx", + "columns": [ + { + "expression": "template_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "placeholder_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_template_table_columns": { + "name": "crm_document_template_table_columns", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mapping_id": { + "name": "mapping_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "column_name": { + "name": "column_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_field": { + "name": "source_field", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "column_letter": { + "name": "column_letter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "format_mask": { + "name": "format_mask", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_document_template_table_columns_mapping_name_idx": { + "name": "crm_document_template_table_columns_mapping_name_idx", + "columns": [ + { + "expression": "mapping_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "column_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_template_versions": { + "name": "crm_document_template_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_json": { + "name": "schema_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "preview_image_url": { + "name": "preview_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_document_template_versions_template_version_idx": { + "name": "crm_document_template_versions_template_version_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_document_templates": { + "name": "crm_document_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_type": { + "name": "document_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_type": { + "name": "file_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_name": { + "name": "template_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_document_templates_org_doc_product_file_name_idx": { + "name": "crm_document_templates_org_doc_product_file_name_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "product_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_leads": { + "name": "crm_leads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_location": { + "name": "project_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lead_channel": { + "name": "lead_channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "estimated_value": { + "name": "estimated_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "awareness_id": { + "name": "awareness_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "followup_status": { + "name": "followup_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lost_reason": { + "name": "lost_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "owner_marketing_user_id": { + "name": "owner_marketing_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_sales_owner_id": { + "name": "assigned_sales_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignment_remark": { + "name": "assignment_remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_leads_org_code_idx": { + "name": "crm_leads_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_opportunities": { + "name": "crm_opportunities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lead_id": { + "name": "lead_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requirement": { + "name": "requirement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_location": { + "name": "project_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome_status": { + "name": "outcome_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lead_channel": { + "name": "lead_channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "estimated_value": { + "name": "estimated_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "chance_percent": { + "name": "chance_percent", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expected_close_date": { + "name": "expected_close_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "project_close_date": { + "name": "project_close_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivery_date": { + "name": "delivery_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "competitor": { + "name": "competitor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_hot_project": { + "name": "is_hot_project", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hot_project_auto_suggested": { + "name": "hot_project_auto_suggested", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hot_project_manually_overridden": { + "name": "hot_project_manually_overridden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "pipeline_stage": { + "name": "pipeline_stage", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'lead'" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_won_at": { + "name": "closed_won_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_lost_at": { + "name": "closed_lost_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_by_user_id": { + "name": "closed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "po_number": { + "name": "po_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "po_date": { + "name": "po_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "po_amount": { + "name": "po_amount", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "po_currency": { + "name": "po_currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lost_reason": { + "name": "lost_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lost_detail": { + "name": "lost_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lost_competitor": { + "name": "lost_competitor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lost_remark": { + "name": "lost_remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancel_reason": { + "name": "cancel_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "no_quotation_reason": { + "name": "no_quotation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_to_user_id": { + "name": "assigned_to_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignment_remark": { + "name": "assignment_remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_opportunities_org_code_idx": { + "name": "crm_opportunities_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_opportunity_attachments": { + "name": "crm_opportunity_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opportunity_id": { + "name": "opportunity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_file_name": { + "name": "original_file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_provider": { + "name": "storage_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_opportunity_customers": { + "name": "crm_opportunity_customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opportunity_id": { + "name": "opportunity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_opportunity_followups": { + "name": "crm_opportunity_followups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opportunity_id": { + "name": "opportunity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "followup_date": { + "name": "followup_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "followup_type": { + "name": "followup_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_followup_date": { + "name": "next_followup_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_attachments": { + "name": "crm_quotation_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_file_name": { + "name": "original_file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_customers": { + "name": "crm_quotation_customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_followups": { + "name": "crm_quotation_followups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "followup_date": { + "name": "followup_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "followup_type": { + "name": "followup_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_followup_date": { + "name": "next_followup_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_items": { + "name": "crm_quotation_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_number": { + "name": "item_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit_price": { + "name": "unit_price", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discount": { + "name": "discount", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discount_type": { + "name": "discount_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tax_rate": { + "name": "tax_rate", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_price": { + "name": "total_price", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_topic_items": { + "name": "crm_quotation_topic_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotation_topics": { + "name": "crm_quotation_topics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic_type": { + "name": "topic_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_quotations": { + "name": "crm_quotations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opportunity_id": { + "name": "opportunity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quotation_date": { + "name": "quotation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "valid_until": { + "name": "valid_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "quotation_type": { + "name": "quotation_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_location": { + "name": "project_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attention": { + "name": "attention", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "parent_quotation_id": { + "name": "parent_quotation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision_remark": { + "name": "revision_remark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "exchange_rate": { + "name": "exchange_rate", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "subtotal": { + "name": "subtotal", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discount": { + "name": "discount", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discount_type": { + "name": "discount_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tax_rate": { + "name": "tax_rate", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tax_amount": { + "name": "tax_amount", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_amount": { + "name": "total_amount", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "chance_percent": { + "name": "chance_percent", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "project_close_date": { + "name": "project_close_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivery_date": { + "name": "delivery_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "is_hot_project": { + "name": "is_hot_project", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hot_project_auto_suggested": { + "name": "hot_project_auto_suggested", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hot_project_manually_overridden": { + "name": "hot_project_manually_overridden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "competitor": { + "name": "competitor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "salesman_id": { + "name": "salesman_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_sent": { + "name": "is_sent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_via": { + "name": "sent_via", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "approved_artifact_id": { + "name": "approved_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_pdf_url": { + "name": "approved_pdf_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_snapshot": { + "name": "approved_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "approved_template_version_id": { + "name": "approved_template_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_quotations_org_code_idx": { + "name": "crm_quotations_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_report_definitions": { + "name": "crm_report_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "crm_report_definitions_org_code_idx": { + "name": "crm_report_definitions_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_role_profiles": { + "name": "crm_role_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "branch_scope_mode": { + "name": "branch_scope_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'assigned'" + }, + "product_scope_mode": { + "name": "product_scope_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'assigned'" + }, + "ownership_scope": { + "name": "ownership_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'own'" + }, + "approval_authority": { + "name": "approval_authority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "crm_role_profiles_org_code_idx": { + "name": "crm_role_profiles_org_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_timeline_projection": { + "name": "crm_timeline_projection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lead_id": { + "name": "lead_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "opportunity_id": { + "name": "opportunity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quotation_id": { + "name": "quotation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timeline_type": { + "name": "timeline_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timeline_category": { + "name": "timeline_category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_display": { + "name": "actor_display", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "visibility": { + "name": "visibility", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_event": { + "name": "source_event", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source_projection_version": { + "name": "source_projection_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "crm_timeline_projection_event_version_idx": { + "name": "crm_timeline_projection_event_version_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_projection_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "crm_timeline_projection_org_occurred_idx": { + "name": "crm_timeline_projection_org_occurred_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "crm_timeline_projection_customer_occurred_idx": { + "name": "crm_timeline_projection_customer_occurred_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "crm_timeline_projection_activity_event_idx": { + "name": "crm_timeline_projection_activity_event_idx", + "columns": [ + { + "expression": "activity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_user_role_assignments": { + "name": "crm_user_role_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_profile_id": { + "name": "role_profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_scope_mode": { + "name": "branch_scope_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inherit'" + }, + "branch_scope_ids": { + "name": "branch_scope_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "product_type_scope_mode": { + "name": "product_type_scope_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inherit'" + }, + "product_type_scope_ids": { + "name": "product_type_scope_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "crm_user_role_assignments_org_user_role_idx": { + "name": "crm_user_role_assignments_org_user_role_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_sequences": { + "name": "document_sequences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "product_type": { + "name": "product_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "document_type": { + "name": "document_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_number": { + "name": "current_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "padding_length": { + "name": "padding_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{prefix}{period}-{running}'" + }, + "reset_policy": { + "name": "reset_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'period'" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_sequences_org_branch_product_doc_period_idx": { + "name": "document_sequences_org_branch_product_doc_period_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "product_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_configurations": { + "name": "email_configurations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'smtp'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 587 + }, + "secure": { + "name": "secure", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_encrypted": { + "name": "password_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reply_to_email": { + "name": "reply_to_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_test_status": { + "name": "last_test_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_tested'" + }, + "last_tested_at": { + "name": "last_tested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_error": { + "name": "last_test_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_configurations_org_name_idx": { + "name": "email_configurations_org_name_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "business_role": { + "name": "business_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sales_support'" + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "branch_scope_ids": { + "name": "branch_scope_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "product_type_scope_ids": { + "name": "product_type_scope_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ms_options": { + "name": "ms_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ms_options_org_category_code_idx": { + "name": "ms_options_org_category_code_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_slug_idx": { + "name": "organizations_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.products": { + "name": "products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "products_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "photo_url": { + "name": "photo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projection_consumer_checkpoints": { + "name": "projection_consumer_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer_name": { + "name": "consumer_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projection_name": { + "name": "projection_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_attempted_at": { + "name": "first_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_duration_ms": { + "name": "processing_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projection_checkpoints_consumer_event_idx": { + "name": "projection_checkpoints_consumer_event_idx", + "columns": [ + { + "expression": "consumer_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "projection_checkpoints_status_retry_idx": { + "name": "projection_checkpoints_status_retry_idx", + "columns": [ + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_retry_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projection_dead_letters": { + "name": "projection_dead_letters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer_name": { + "name": "consumer_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projection_name": { + "name": "projection_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "failure_metadata": { + "name": "failure_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_reason": { + "name": "resolution_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projection_dead_letters_consumer_event_idx": { + "name": "projection_dead_letters_consumer_event_idx", + "columns": [ + { + "expression": "consumer_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "projection_dead_letters_org_status_idx": { + "name": "projection_dead_letters_org_status_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projection_health_snapshots": { + "name": "projection_health_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projection_name": { + "name": "projection_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer_name": { + "name": "consumer_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'healthy'" + }, + "last_processed_event_id": { + "name": "last_processed_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_successful_processing_at": { + "name": "last_successful_processing_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pending_count": { + "name": "pending_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dead_letter_count": { + "name": "dead_letter_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "unsupported_version_count": { + "name": "unsupported_version_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_lag_ms": { + "name": "processing_lag_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "average_processing_duration_ms": { + "name": "average_processing_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_rebuild_at": { + "name": "last_rebuild_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rebuild_source": { + "name": "rebuild_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projection_health_org_consumer_idx": { + "name": "projection_health_org_consumer_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "consumer_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projection_rebuild_runs": { + "name": "projection_rebuild_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projection_name": { + "name": "projection_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_from": { + "name": "occurred_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "occurred_to": { + "name": "occurred_to", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "reset_existing": { + "name": "reset_existing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "source_strategy": { + "name": "source_strategy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projection_rebuild_runs_org_projection_idx": { + "name": "projection_rebuild_runs_org_projection_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "projection_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seed_manifest_items": { + "name": "seed_manifest_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "seed_manifest_id": { + "name": "seed_manifest_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_key": { + "name": "item_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_file": { + "name": "source_file", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "imported_count": { + "name": "imported_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_count": { + "name": "updated_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_count": { + "name": "error_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "warning_count": { + "name": "warning_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "report_json": { + "name": "report_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "seed_manifest_items_manifest_item_idx": { + "name": "seed_manifest_items_manifest_item_idx", + "columns": [ + { + "expression": "seed_manifest_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seed_manifests": { + "name": "seed_manifests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "setup_run_id": { + "name": "setup_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_by": { + "name": "applied_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "report_json": { + "name": "report_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.setup_run_steps": { + "name": "setup_run_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "setup_run_id": { + "name": "setup_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_key": { + "name": "step_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_started'" + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_json": { + "name": "output_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "errors_json": { + "name": "errors_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "warnings_json": { + "name": "warnings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "setup_run_steps_run_step_idx": { + "name": "setup_run_steps_run_step_idx", + "columns": [ + { + "expression": "setup_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.setup_runs": { + "name": "setup_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_started'" + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'production'" + }, + "target_organization_id": { + "name": "target_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_by": { + "name": "started_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_by": { + "name": "completed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tr_audit_logs": { + "name": "tr_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_id": { + "name": "branch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "before_data": { + "name": "before_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_data": { + "name": "after_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_role": { + "name": "system_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index d4b257c..b4a1a2f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1783922653023, "tag": "0004_sharp_mercury", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1783925810194, + "tag": "0005_freezing_wendigo", + "breakpoints": true } ] } \ No newline at end of file diff --git a/plans/task-ep.1.5.md b/plans/task-ep.1.5.md new file mode 100644 index 0000000..4959068 --- /dev/null +++ b/plans/task-ep.1.5.md @@ -0,0 +1,878 @@ +# Task EP.1.5 – Timeline Projection Foundation + +Status: Completed + +Priority: Critical + +Type: Projection Platform / Timeline Read Model + +--- + +# Depends On + +* EP.1.1 Activity Domain Foundation +* EP.1.2 Activity Integration & Legacy Follow-up Adapter +* EP.1.3 Business Event Foundation +* EP.1.4 Projection Foundation & Delivery Reliability +* EP.1.4.1 Transactional Outbox Activation & Worker Runtime +* BU-R.0 Business Blueprint Freeze +* BU-R.0.1 Workspace & Activity Blueprint +* BU-R.1 Business Capability Audit +* AR.1 Architecture Transition Plan +* AR.2 Epic Technical Design +* ENG.0 Engineering Constitution + +--- + +# Objective + +Introduce the first production Business Projection of ALLA OS. + +This phase builds the Timeline Projection that becomes the official chronological business history of CRM. + +Timeline is **not** Activity. + +Timeline is **not** Audit Log. + +Timeline is **not** Follow-up. + +Timeline is a generated business projection that explains everything that happened to a Customer, Lead, Opportunity, Quotation, Activity, Approval, and PO in business language. + +Timeline becomes the foundation for: + +* Customer Workspace +* Lead Workspace +* Opportunity Workspace +* Quotation Workspace +* Relationship Workspace +* Manager Workspace +* Executive Workspace + +Future Calendar, My Day, Dashboard and Forecast continue consuming Business Events independently. + +Timeline never owns business state. + +--- + +# Background + +Current repository already contains + +* Audit Log +* Activity Domain +* Business Event Foundation +* Transactional Outbox +* Projection Runtime +* Legacy Follow-up Adapters + +What still does not exist is a single business timeline. + +Today users must inspect + +* Audit Log +* Lead Follow-up +* Opportunity Follow-up +* Quotation Follow-up +* Activity +* Approval History + +to understand one customer journey. + +Timeline consolidates these into one governed projection. + +--- + +# Timeline Principles + +## Timeline is Business History + +Timeline answers + +> "What happened?" + +not + +> "What changed in the database?" + +--- + +## Timeline is Read Only + +Timeline never edits data. + +Timeline never owns lifecycle. + +Timeline is always rebuildable. + +--- + +## Timeline is Projection + +Timeline is generated from + +* Business Events +* Activity +* Legacy Follow-up adapters +* Approval events +* Source queries during rebuild + +--- + +## Timeline is Human Friendly + +Example + +Instead of + +```text +activity.completed +``` + +Timeline shows + +```text +Completed site survey. + +Outcome: +Customer requested revised quotation. +``` + +--- + +## Timeline is Chronological + +Every Timeline Item has + +* occurredAt +* source +* actor +* entity +* business meaning + +--- + +# Review Required + +Review + +Governance + +* AGENTS.md +* Engineering Constitution +* Project Foundations +* Architecture Rules +* UI/UX Rules +* Task Review Checklist + +Business + +* Relationship Sales Workspace Blueprint +* BU-R.1 + +Architecture + +* AR.1 +* AR.2 + +Implementation + +* EP.1.2 +* EP.1.3 +* EP.1.4 +* EP.1.4.1 + +Existing + +* Activity Timeline Compatibility Layer +* Audit Log +* Approval History +* Customer Detail +* Lead Detail +* Opportunity Detail +* Quotation Detail + +Review + +layout.md + +Review + +ui-ux-rules.md + +Review + +ui-ux-pro-max + +Timeline UI must follow the Workspace UI Constitution. + +--- + +# Scope + +## Part 1 — Timeline Projection Schema + +Introduce + +crm_timeline_projection + +Minimum fields + +* id +* organizationId +* branchId +* customerId +* leadId +* opportunityId +* quotationId +* activityId +* eventId +* entityType +* entityId +* timelineType +* timelineCategory +* occurredAt +* actorId +* actorDisplay +* title +* summary +* icon +* color +* priority +* visibility +* metadata +* sourceEvent +* sourceProjectionVersion +* createdAt + +Timeline rows are rebuildable. + +Timeline rows are never source-of-truth. + +--- + +## Part 2 — Timeline Builder + +Implement + +TimelineProjectionBuilder + +Responsibilities + +* consume Business Events +* transform event +* enrich display +* resolve references +* write projection + +Builder must never mutate source domains. + +--- + +## Part 3 — Timeline Event Mapping + +Create mapping from + +Business Event + +↓ + +Timeline Item + +Example + +```text +activity.completed + +↓ + +Completed Site Survey +``` + +Another + +```text +quotation.approved + +↓ + +Quotation Approved +``` + +Every supported Business Event requires mapping. + +--- + +## Part 4 — Legacy Timeline Adapter + +Support timeline rebuild using + +Lead Follow-up + +Opportunity Follow-up + +Quotation Follow-up + +Activity + +Approval + +Audit + +No migration. + +Adapters remain read-only. + +--- + +## Part 5 — Timeline Projection Consumer + +Activate Timeline consumer. + +Consumer responsibilities + +* consume event +* build timeline item +* persist projection +* checkpoint +* retry +* rebuild safe + +Consumer uses Projection Runtime. + +--- + +## Part 6 — Timeline Categories + +Freeze categories + +Activity + +Communication + +Meeting + +Site Survey + +Approval + +Quotation + +Revision + +Assignment + +Status + +Forecast + +PO + +Customer + +System + +Categories must be configurable using Master Options later. + +--- + +## Part 7 — Timeline Rendering Model + +Freeze UI model + +Timeline Item + +contains + +Header + +Timestamp + +Actor + +Business Message + +Reference Links + +Status Badge + +Icon + +Optional Detail + +Attachment Indicator + +No editing inside Timeline. + +--- + +## Part 8 — Timeline Visibility + +Timeline follows CRM Authorization. + +Visibility + +Organization + +↓ + +Branch + +↓ + +Product + +↓ + +Pricing + +↓ + +Ownership + +↓ + +Internal Only + +Timeline never exposes hidden quotation pricing. + +--- + +## Part 9 — Timeline Projection Rebuild + +Implement + +Timeline rebuild + +Supports + +Organization + +Customer + +Lead + +Opportunity + +Quotation + +Date Range + +Dry Run + +Reset Existing + +Progress + +Restart + +--- + +## Part 10 — Timeline Projection Health + +Track + +Items + +Lag + +Last Event + +Checkpoint + +Rebuild Time + +Health + +Errors + +--- + +## Part 11 — Timeline UI Components + +Introduce reusable components + +TimelineList + +TimelineCard + +TimelineItem + +TimelineFilter + +TimelineGroup + +TimelineEmpty + +TimelineLoading + +TimelineSkeleton + +TimelineVirtualList + +Use + +shadcn/ui + +Follow + +layout.md + +Follow + +ui-ux-rules.md + +Apply + +ui-ux-pro-max + +Timeline must feel modern and readable. + +--- + +## Part 12 — Workspace Integration + +Embed Timeline into + +Customer Detail + +Lead Detail + +Opportunity Detail + +Quotation Detail + +Activity Detail + +Timeline replaces fragmented history panels over time. + +Existing panels remain until cutover. + +--- + +## Part 13 — Filtering + +Support + +Date + +Actor + +Timeline Category + +Entity + +Activity + +Approval + +Quotation + +Customer + +Opportunity + +Lead + +PO + +Attachment + +Search + +Unread + +Internal Only + +--- + +## Part 14 — Timeline Grouping + +Support grouping + +Today + +Yesterday + +Earlier This Week + +Last Week + +Earlier This Month + +Older + +Optional + +Group by Entity + +Group by Customer + +Group by Opportunity + +--- + +## Part 15 — Timeline Search + +Introduce + +Full text search + +Business Message + +Summary + +Actor + +Customer + +Opportunity + +Quotation + +Activity + +Category + +--- + +## Part 16 — Timeline Performance + +Support + +Virtual scrolling + +Incremental loading + +Cursor pagination + +Projection indexes + +Lazy detail expansion + +No N+1 queries. + +--- + +## Part 17 — Timeline Delivery Matrix + +Freeze + +Business Event + +↓ + +Timeline Item + +↓ + +Workspace + +↓ + +Consumer + +↓ + +Projection + +Every event path documented. + +--- + +## Part 18 — Timeline UX Review + +Before merge + +Review + +layout.md + +Review + +ui-ux-rules.md + +Review + +ui-ux-pro-max + +Verify + +Typography + +Spacing + +Hierarchy + +Icons + +Density + +Mobile + +Accessibility + +Keyboard + +Dark Mode + +Loading + +Empty State + +Skeleton + +Sticky Date Header + +Infinite Scroll + +--- + +# Deliverables + +1. Timeline Projection Schema + +2. Timeline Builder + +3. Timeline Projection Consumer + +4. Timeline Projection Registry + +5. Event-to-Timeline Mapping + +6. Legacy Timeline Adapter + +7. Timeline Projection Storage + +8. Timeline Rebuild + +9. Timeline Health + +10. Timeline UI Components + +11. Workspace Timeline Integration + +12. Timeline Filtering + +13. Timeline Search + +14. Timeline Grouping + +15. Timeline Delivery Matrix + +16. Timeline UX Review Report + +17. Projection Readiness Report + +--- + +# Constraints + +Must preserve + +* Activity +* Audit Log +* Approval History +* Follow-up APIs +* Dashboard +* Reports +* Notifications +* Current Detail Pages + +Timeline is additive. + +Do not replace + +Audit Log. + +Do not replace + +Activity. + +Do not replace + +Calendar. + +Do not replace + +Notification. + +Do not replace + +Dashboard. + +No business state mutation. + +No workflow changes. + +No permission changes. + +--- + +# Testing + +Projection + +Consumer + +Builder + +Rebuild + +Retry + +Checkpoint + +Timeline UI + +Search + +Grouping + +Filtering + +Pagination + +Virtual List + +Authorization + +Pricing Visibility + +Legacy Adapter + +Workspace Integration + +Dark Mode + +Accessibility + +Performance + +--- + +# Acceptance Criteria + +* Timeline Projection is generated entirely from Business Events and approved adapters. +* Timeline never mutates source domains. +* Timeline can be rebuilt safely. +* Timeline respects all CRM authorization rules. +* Timeline integrates into Customer, Lead, Opportunity, Quotation, and Activity detail pages without replacing existing history panels. +* Legacy Follow-up history appears through adapters without data migration. +* Timeline UI follows `layout.md`, `ui-ux-rules.md`, and `ui-ux-pro-max`. +* Timeline supports filtering, grouping, search, cursor pagination, and virtual scrolling. +* Projection health, rebuild, retry, and checkpoints integrate with the Projection Runtime. +* Existing APIs, Dashboard, Notifications, Reports, and Approval flows remain unchanged. + +--- + +# Success Criteria + +ALLA OS gains its first production-grade Business Projection. + +Users can understand the complete customer and sales journey from one unified Timeline instead of opening multiple follow-up tabs, audit logs, and approval histories. + +Timeline becomes the canonical business history used by future Calendar, My Day, Manager Workspace, Executive Workspace, Relationship Health, Forecast, and Customer 360 features. + +Ready for: + +# EP.1.6 – Calendar Projection Foundation diff --git a/src/db/schema.ts b/src/db/schema.ts index bd1f290..dce9162 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -625,6 +625,57 @@ export const projectionHealthSnapshots = pgTable( }) ); +export const crmTimelineProjection = pgTable( + 'crm_timeline_projection', + { + id: text('id').primaryKey(), + organizationId: text('organization_id').notNull(), + branchId: text('branch_id'), + customerId: text('customer_id'), + leadId: text('lead_id'), + opportunityId: text('opportunity_id'), + quotationId: text('quotation_id'), + activityId: text('activity_id'), + eventId: text('event_id').notNull(), + entityType: text('entity_type').notNull(), + entityId: text('entity_id').notNull(), + timelineType: text('timeline_type').notNull(), + timelineCategory: text('timeline_category').notNull(), + occurredAt: timestamp('occurred_at', { withTimezone: true }).notNull(), + actorId: text('actor_id'), + actorDisplay: text('actor_display'), + title: text('title').notNull(), + summary: text('summary'), + icon: text('icon').notNull(), + color: text('color').notNull(), + priority: text('priority').default('normal').notNull(), + visibility: jsonb('visibility').notNull(), + metadata: jsonb('metadata'), + sourceEvent: jsonb('source_event').notNull(), + sourceProjectionVersion: integer('source_projection_version').default(1).notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull() + }, + (table) => ({ + eventVersionIdx: uniqueIndex('crm_timeline_projection_event_version_idx').on( + table.eventId, + table.sourceProjectionVersion + ), + organizationOccurredIdx: index('crm_timeline_projection_org_occurred_idx').on( + table.organizationId, + table.occurredAt + ), + customerOccurredIdx: index('crm_timeline_projection_customer_occurred_idx').on( + table.organizationId, + table.customerId, + table.occurredAt + ), + activityEventIdx: index('crm_timeline_projection_activity_event_idx').on( + table.activityId, + table.eventId + ) + }) +); + export const crmCustomers = pgTable( 'crm_customers', { diff --git a/src/features/crm/activities/components/activity-detail.tsx b/src/features/crm/activities/components/activity-detail.tsx index 890d296..62fa09d 100644 --- a/src/features/crm/activities/components/activity-detail.tsx +++ b/src/features/crm/activities/components/activity-detail.tsx @@ -2,6 +2,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Separator } from '@/components/ui/separator'; import { formatDateTime } from '@/lib/date-format'; import type { ActivityRecord } from '../api/types'; +import { TimelineList } from '@/features/crm/timeline/components/timeline-list'; +import type { TimelineItem } from '@/features/crm/timeline/api/types'; import { ActivityBadge } from './activity-badge'; import { ActivityPriorityBadge } from './activity-priority-badge'; import { ActivityStatusBadge } from './activity-status-badge'; @@ -15,7 +17,13 @@ function Field({ label, value }: { label: string; value: string | null | undefin ); } -export function ActivityDetail({ activity }: { activity: ActivityRecord | null }) { +export function ActivityDetail({ + activity, + timelineItems +}: { + activity: ActivityRecord | null; + timelineItems?: TimelineItem[]; +}) { if (!activity) { return ( @@ -27,6 +35,7 @@ export function ActivityDetail({ activity }: { activity: ActivityRecord | null } } return ( +
@@ -61,5 +70,14 @@ export function ActivityDetail({ activity }: { activity: ActivityRecord | null }
+ {timelineItems ? ( + + ) : null} +
); } diff --git a/src/features/crm/timeline/api/types.ts b/src/features/crm/timeline/api/types.ts new file mode 100644 index 0000000..03c5c3e --- /dev/null +++ b/src/features/crm/timeline/api/types.ts @@ -0,0 +1,74 @@ +export type TimelineCategory = + | 'activity' + | 'communication' + | 'meeting' + | 'site_survey' + | 'approval' + | 'quotation' + | 'revision' + | 'assignment' + | 'status' + | 'forecast' + | 'po' + | 'customer' + | 'system'; + +export type TimelinePriority = 'low' | 'normal' | 'high' | 'critical'; + +export interface TimelineVisibility { + productType?: string | null; + pricingSensitive: boolean; + internalOnly: boolean; +} + +export interface TimelineItem { + id: string; + organizationId: string; + branchId: string | null; + customerId: string | null; + leadId: string | null; + opportunityId: string | null; + quotationId: string | null; + activityId: string | null; + eventId: string; + entityType: string; + entityId: string; + timelineType: string; + timelineCategory: TimelineCategory; + occurredAt: string; + actorId: string | null; + actorDisplay: string | null; + title: string; + summary: string | null; + icon: string; + color: string; + priority: TimelinePriority; + visibility: TimelineVisibility; + metadata: Record | null; + sourceProjectionVersion: number; + createdAt: string; +} + +export interface TimelineListFilters { + organizationId: string; + customerId?: string; + leadId?: string; + opportunityId?: string; + quotationId?: string; + activityId?: string; + eventId?: string; + category?: TimelineCategory; + actorId?: string; + search?: string; + occurredFrom?: string; + occurredTo?: string; + includeInternalOnly?: boolean; + limit?: number; + cursor?: string; +} + +export interface TimelineListResponse { + items: TimelineItem[]; + nextCursor: string | null; + totalItems: number; +} diff --git a/src/features/crm/timeline/components/timeline-list.tsx b/src/features/crm/timeline/components/timeline-list.tsx new file mode 100644 index 0000000..706aa57 --- /dev/null +++ b/src/features/crm/timeline/components/timeline-list.tsx @@ -0,0 +1,162 @@ +'use client'; + +import { Icons } from '@/components/icons'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { formatDateTime } from '@/lib/date-format'; +import { cn } from '@/lib/utils'; +import type { TimelineItem } from '../api/types'; + +interface TimelineListProps { + items: TimelineItem[]; + title?: string; + description?: string; + emptyMessage?: string; + className?: string; +} + +const categoryLabels: Record = { + activity: 'Activity', + communication: 'Communication', + meeting: 'Meeting', + site_survey: 'Site Survey', + approval: 'Approval', + quotation: 'Quotation', + revision: 'Revision', + assignment: 'Assignment', + status: 'Status', + forecast: 'Forecast', + po: 'PO', + customer: 'Customer', + system: 'System' +}; + +const toneClassNames: Record = { + success: 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900 dark:bg-emerald-950/40 dark:text-emerald-300', + warning: 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-300', + danger: 'border-red-200 bg-red-50 text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300', + info: 'border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-900 dark:bg-sky-950/40 dark:text-sky-300', + muted: 'border-muted bg-muted/40 text-muted-foreground', + neutral: 'border-border bg-background text-foreground' +}; + +export function TimelineList({ + items, + title = 'Timeline', + description = 'Business-readable history generated from events and approved adapters.', + emptyMessage = 'No timeline items yet.', + className +}: TimelineListProps) { + return ( + + +
+
+ {title} + {description} +
+ {items.length} items +
+
+ + {!items.length ? ( + + ) : ( +
+ + )} + + + ); +} + +export function TimelineCard({ item }: { item: TimelineItem }) { + const Icon = iconForItem(item.icon); + const toneClassName = toneClassNames[item.color] ?? toneClassNames.neutral; + + return ( +
+ +
+
+
+
+

{item.title}

+ + {categoryLabels[item.timelineCategory] ?? item.timelineCategory} + + {item.visibility.internalOnly ? Internal : null} + {item.visibility.pricingSensitive ? Pricing guarded : null} +
+ {item.summary ? ( +

{item.summary}

+ ) : null} +
+ +
+
+ {item.actorDisplay ?? item.actorId ?? 'System'} + {item.entityType}:{item.entityId} + {item.timelineType} +
+
+
+ ); +} + +export function TimelineEmpty({ message }: { message: string }) { + return ( +
+ + {message} +
+ ); +} + +export function TimelineSkeleton() { + return ( + + + + + + + {Array.from({ length: 3 }).map((_, index) => ( +
+ +
+ + + +
+
+ ))} +
+
+ ); +} + +function iconForItem(icon: string) { + if (icon === 'check') return Icons.check; + if (icon === 'close') return Icons.close; + if (icon === 'calendar') return Icons.calendar; + if (icon === 'user') return Icons.user; + if (icon === 'trash') return Icons.trash; + if (icon === 'clock') return Icons.clock; + return Icons.info; +} diff --git a/src/features/crm/timeline/index.ts b/src/features/crm/timeline/index.ts new file mode 100644 index 0000000..1d7947a --- /dev/null +++ b/src/features/crm/timeline/index.ts @@ -0,0 +1,2 @@ +export * from './api/types'; +export * from './components/timeline-list'; diff --git a/src/features/crm/timeline/server/builder.test.ts b/src/features/crm/timeline/server/builder.test.ts new file mode 100644 index 0000000..4845143 --- /dev/null +++ b/src/features/crm/timeline/server/builder.test.ts @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createBusinessEvent } from '../../../foundation/business-events/publisher.ts'; +import { createTimelineProjectionConsumer } from './consumer.ts'; +import { buildTimelineItemFromEvent } from './builder.ts'; +import type { TimelineItem } from '../api/types.ts'; + +function activityCompletedEvent() { + return createBusinessEvent({ + eventType: 'activity.completed', + organizationId: 'org-1', + branchId: 'branch-1', + entityType: 'activity', + entityId: 'activity-1', + primaryRecord: { type: 'opportunity', id: 'opp-1' }, + relatedRecords: [ + { type: 'customer', id: 'customer-1' }, + { type: 'quotation', id: 'quotation-1' } + ], + actorUserId: 'user-1', + occurredAt: '2026-07-13T00:00:00.000Z', + correlationId: 'corr-1', + visibility: { + productType: 'product-a', + pricingSensitive: true, + internalOnly: false + }, + payload: { + activityId: 'activity-1', + subject: 'Completed site survey', + outcomeSummary: 'Customer requested revised quotation.', + nextAction: 'Prepare revision', + priority: 'high', + contentRedacted: false + } + }); +} + +test('timeline builder maps activity completed into business history item', () => { + const item = buildTimelineItemFromEvent(activityCompletedEvent()); + + assert.equal(item.title, 'Activity completed'); + assert.equal(item.timelineCategory, 'activity'); + assert.equal(item.customerId, 'customer-1'); + assert.equal(item.opportunityId, null); + assert.equal(item.quotationId, 'quotation-1'); + assert.equal(item.activityId, 'activity-1'); + assert.equal(item.priority, 'high'); + assert.equal(item.visibility.pricingSensitive, true); + assert.match(item.summary ?? '', /Customer requested revised quotation/); +}); + +test('timeline consumer persists built item through injected writer', async () => { + const persisted: TimelineItem[] = []; + const consumer = createTimelineProjectionConsumer({ + async persist(item) { + persisted.push(item); + } + }); + + const result = await consumer.handle(activityCompletedEvent()); + + assert.equal(result.status, 'processed'); + assert.equal(result.sideEffectCount, 1); + assert.equal(persisted.length, 1); + assert.equal(persisted[0]?.timelineType, 'activity.completed'); +}); diff --git a/src/features/crm/timeline/server/builder.ts b/src/features/crm/timeline/server/builder.ts new file mode 100644 index 0000000..d2f514f --- /dev/null +++ b/src/features/crm/timeline/server/builder.ts @@ -0,0 +1,207 @@ +import type { BusinessEvent } from '../../../foundation/business-events/types.ts'; +import type { TimelineCategory, TimelineItem, TimelinePriority } from '../api/types'; + +export const TIMELINE_PROJECTION_VERSION = 1; + +interface TimelineEventMapping { + title: string; + category: TimelineCategory; + icon: string; + color: string; + priority?: TimelinePriority; + summary?: (payload: Record) => string | null; +} + +const ACTIVITY_EVENT_MAPPINGS: Record = { + 'activity.created': { + title: 'Activity created', + category: 'activity', + icon: 'calendar', + color: 'neutral', + summary: (payload) => readableJoin([text(payload.subject), text(payload.description)]) + }, + 'activity.assigned': { + title: 'Activity assigned', + category: 'assignment', + icon: 'user', + color: 'info', + summary: (payload) => assignedSummary(payload) + }, + 'activity.reassigned': { + title: 'Activity reassigned', + category: 'assignment', + icon: 'user', + color: 'warning', + summary: (payload) => assignedSummary(payload) + }, + 'activity.started': { + title: 'Activity started', + category: 'status', + icon: 'clock', + color: 'info', + summary: (payload) => text(payload.subject) + }, + 'activity.rescheduled': { + title: 'Activity rescheduled', + category: 'activity', + icon: 'calendar', + color: 'warning', + summary: (payload) => + readableJoin([text(payload.subject), scheduleSummary(payload)]) + }, + 'activity.completed': { + title: 'Activity completed', + category: 'activity', + icon: 'check', + color: 'success', + priority: 'high', + summary: (payload) => + readableJoin([text(payload.subject), text(payload.outcomeSummary), text(payload.nextAction)]) + }, + 'activity.cancelled': { + title: 'Activity cancelled', + category: 'activity', + icon: 'close', + color: 'danger', + summary: (payload) => readableJoin([text(payload.subject), text(payload.cancellationReason)]) + }, + 'activity.deleted': { + title: 'Activity deleted', + category: 'system', + icon: 'trash', + color: 'muted', + summary: (payload) => text(payload.subject) + } +}; + +const GENERIC_EVENT_MAPPING: TimelineEventMapping = { + title: 'Business event recorded', + category: 'system', + icon: 'info', + color: 'neutral' +}; + +export function buildTimelineItemFromEvent(event: BusinessEvent): TimelineItem { + const payload = event.payload as Record; + const mapping = ACTIVITY_EVENT_MAPPINGS[event.eventType] ?? genericMapping(event.eventType); + const entityRefs = resolveEntityRefs(event, payload); + + return { + id: crypto.randomUUID(), + organizationId: event.organizationId, + branchId: event.branchId ?? null, + customerId: entityRefs.customerId, + leadId: entityRefs.leadId, + opportunityId: entityRefs.opportunityId, + quotationId: entityRefs.quotationId, + activityId: entityRefs.activityId, + eventId: event.eventId, + entityType: event.entityType, + entityId: event.entityId, + timelineType: event.eventType, + timelineCategory: mapping.category, + occurredAt: event.occurredAt, + actorId: event.actorUserId, + actorDisplay: null, + title: mapping.title, + summary: mapping.summary?.(payload) ?? text(payload.subject) ?? null, + icon: mapping.icon, + color: mapping.color, + priority: mapping.priority ?? priorityFromPayload(payload), + visibility: { + productType: event.visibility.productType ?? null, + pricingSensitive: event.visibility.pricingSensitive ?? false, + internalOnly: event.visibility.internalOnly ?? false + }, + metadata: { + primaryRecord: event.primaryRecord, + relatedRecords: event.relatedRecords, + correlationId: event.correlationId, + causationId: event.causationId ?? null, + contentRedacted: Boolean(payload.contentRedacted) + }, + sourceProjectionVersion: TIMELINE_PROJECTION_VERSION, + createdAt: new Date().toISOString() + }; +} + +export const TIMELINE_EVENT_DELIVERY_MATRIX = [ + { + eventType: 'activity.created', + timelineItem: 'Activity created', + workspace: 'Customer, Lead, Opportunity, Quotation, Activity', + consumer: 'timeline.projection.consumer', + projection: 'crm_timeline_projection' + }, + { + eventType: 'activity.completed', + timelineItem: 'Activity completed', + workspace: 'Customer, Lead, Opportunity, Quotation, Activity', + consumer: 'timeline.projection.consumer', + projection: 'crm_timeline_projection' + }, + { + eventType: 'activity.cancelled', + timelineItem: 'Activity cancelled', + workspace: 'Customer, Lead, Opportunity, Quotation, Activity', + consumer: 'timeline.projection.consumer', + projection: 'crm_timeline_projection' + } +] as const; + +function genericMapping(eventType: string): TimelineEventMapping { + return { + ...GENERIC_EVENT_MAPPING, + title: eventType + .split('.') + .map((part) => part.replaceAll('-', ' ')) + .join(' ') + }; +} + +function resolveEntityRefs(event: BusinessEvent, payload: Record) { + const refs = new Map(event.relatedRecords.map((record) => [record.type, record.id])); + return { + customerId: text(payload.customerId) ?? refs.get('customer') ?? null, + leadId: text(payload.leadId) ?? refs.get('lead') ?? null, + opportunityId: text(payload.opportunityId) ?? refs.get('opportunity') ?? null, + quotationId: text(payload.quotationId) ?? refs.get('quotation') ?? null, + activityId: + text(payload.activityId) ?? + (event.entityType === 'activity' ? event.entityId : refs.get('activity') ?? null) + }; +} + +function priorityFromPayload(payload: Record): TimelinePriority { + const priority = text(payload.priority); + if (priority === 'critical' || priority === 'high' || priority === 'low') { + return priority; + } + return 'normal'; +} + +function assignedSummary(payload: Record): string | null { + return readableJoin([ + text(payload.subject), + text(payload.previousAssignedToId) + ? `Previous assignee: ${text(payload.previousAssignedToId)}` + : null, + text(payload.assignedToId) ? `Assignee: ${text(payload.assignedToId)}` : null + ]); +} + +function scheduleSummary(payload: Record): string | null { + return readableJoin([ + text(payload.scheduledStartAt) ? `Scheduled: ${text(payload.scheduledStartAt)}` : null, + text(payload.dueAt) ? `Due: ${text(payload.dueAt)}` : null + ]); +} + +function readableJoin(values: Array): string | null { + const parts = values.filter((value): value is string => Boolean(value)); + return parts.length ? parts.join(' · ') : null; +} + +function text(value: unknown): string | null { + return typeof value === 'string' && value.trim() ? value.trim() : null; +} diff --git a/src/features/crm/timeline/server/consumer.ts b/src/features/crm/timeline/server/consumer.ts new file mode 100644 index 0000000..17edcab --- /dev/null +++ b/src/features/crm/timeline/server/consumer.ts @@ -0,0 +1,45 @@ +import type { BusinessEvent } from '../../../foundation/business-events/types.ts'; +import type { + ProjectionConsumer, + ProjectionHandleResult +} from '../../../foundation/projections/types.ts'; +import { getProjectionDefinition } from '../../../foundation/projections/registry.ts'; +import { buildTimelineItemFromEvent } from './builder.ts'; + +const definition = getProjectionDefinition('Timeline'); + +interface TimelineProjectionConsumerOptions { + persist?: (item: ReturnType) => Promise; +} + +export function createTimelineProjectionConsumer( + options: TimelineProjectionConsumerOptions = {} +): ProjectionConsumer { + const persist = options.persist ?? persistTimelineItem; + + return { + consumerName: definition.consumerName, + projectionName: definition.projectionName, + supportedEventTypes: definition.consumedEventTypes, + supportedVersions: definition.supportedVersions, + async handle(event: BusinessEvent): Promise { + const item = buildTimelineItemFromEvent(event); + await persist(item); + + return { + status: 'processed', + message: 'Timeline projection item persisted', + sideEffectCount: 1 + }; + } +}; +} + +export const timelineProjectionConsumer = createTimelineProjectionConsumer(); + +async function persistTimelineItem( + item: ReturnType +): Promise { + const { upsertTimelineProjectionItem } = await import('./service.ts'); + await upsertTimelineProjectionItem(item); +} diff --git a/src/features/crm/timeline/server/legacy-adapters.ts b/src/features/crm/timeline/server/legacy-adapters.ts new file mode 100644 index 0000000..1f0fd3e --- /dev/null +++ b/src/features/crm/timeline/server/legacy-adapters.ts @@ -0,0 +1,35 @@ +import type { TimelineItem } from '../api/types.ts'; + +export interface TimelineLegacyAdapterRequest { + organizationId: string; + customerId?: string; + leadId?: string; + opportunityId?: string; + quotationId?: string; + occurredFrom?: string; + occurredTo?: string; + dryRun?: boolean; +} + +export interface TimelineLegacyAdapter { + readonly adapterName: string; + readonly source: 'lead_followup' | 'opportunity_followup' | 'quotation_followup' | 'activity' | 'approval' | 'audit'; + collect(request: TimelineLegacyAdapterRequest): Promise; +} + +export const TIMELINE_LEGACY_ADAPTER_SOURCES = [ + 'lead_followup', + 'opportunity_followup', + 'quotation_followup', + 'activity', + 'approval', + 'audit' +] as const; + +export const timelineLegacyAdapterContract = { + rebuildOnly: true, + migrationRequired: false, + sourceMutationsAllowed: false, + notes: + 'Legacy adapters are read-only rebuild inputs. They must not migrate, update, or delete source records.' +} as const; diff --git a/src/features/crm/timeline/server/rebuild.ts b/src/features/crm/timeline/server/rebuild.ts new file mode 100644 index 0000000..55fefd7 --- /dev/null +++ b/src/features/crm/timeline/server/rebuild.ts @@ -0,0 +1,38 @@ +import { createProjectionRebuildPlan } from '../../../foundation/projections/rebuild.ts'; +import type { ProjectionRebuildRequest } from '../../../foundation/projections/types.ts'; +import type { TimelineListResponse } from '../api/types.ts'; +import { listTimelineItems, resetTimelineProjectionForOrganization } from './service.ts'; + +export interface TimelineRebuildRequest extends ProjectionRebuildRequest { + projectionName: 'Timeline'; +} + +export async function planTimelineRebuild(request: TimelineRebuildRequest) { + return createProjectionRebuildPlan({ + ...request, + projectionName: 'Timeline', + dryRun: request.dryRun ?? true + }); +} + +export async function dryRunTimelineRebuild( + request: TimelineRebuildRequest +): Promise { + return listTimelineItems({ + organizationId: request.organizationId, + customerId: request.entityType === 'customer' ? request.entityId : undefined, + leadId: request.entityType === 'lead' ? request.entityId : undefined, + opportunityId: request.entityType === 'opportunity' ? request.entityId : undefined, + quotationId: request.entityType === 'quotation' ? request.entityId : undefined, + occurredFrom: request.occurredFrom, + occurredTo: request.occurredTo, + limit: 100 + }); +} + +export async function resetTimelineProjection(request: TimelineRebuildRequest): Promise { + if (!request.resetExisting) { + throw new Error('Timeline projection reset requires resetExisting=true'); + } + await resetTimelineProjectionForOrganization(request.organizationId); +} diff --git a/src/features/crm/timeline/server/registration.ts b/src/features/crm/timeline/server/registration.ts new file mode 100644 index 0000000..8ba480a --- /dev/null +++ b/src/features/crm/timeline/server/registration.ts @@ -0,0 +1,13 @@ +import { + calendarProjectionConsumer, + dashboardProjectionConsumer, + notificationProjectionConsumer +} from '../../../foundation/projections/consumers.ts'; +import { timelineProjectionConsumer } from './consumer.ts'; + +export const CRM_TIMELINE_PROJECTION_CONSUMERS = [ + timelineProjectionConsumer, + calendarProjectionConsumer, + dashboardProjectionConsumer, + notificationProjectionConsumer +]; diff --git a/src/features/crm/timeline/server/service.ts b/src/features/crm/timeline/server/service.ts new file mode 100644 index 0000000..ed48156 --- /dev/null +++ b/src/features/crm/timeline/server/service.ts @@ -0,0 +1,137 @@ +import { and, desc, eq, ilike, lte, gte, type SQL } from 'drizzle-orm'; +import { crmTimelineProjection } from '../../../../db/schema.ts'; +import { db } from '../../../../lib/db.ts'; +import type { TimelineItem, TimelineListFilters, TimelineListResponse } from '../api/types.ts'; + +export type TimelineProjectionRow = typeof crmTimelineProjection.$inferSelect; +export type InsertTimelineProjectionRow = typeof crmTimelineProjection.$inferInsert; +type TimelineDbClient = Pick; + +export async function upsertTimelineProjectionItem( + item: TimelineItem, + client: TimelineDbClient = db +): Promise { + await client + .insert(crmTimelineProjection) + .values({ + id: item.id, + organizationId: item.organizationId, + branchId: item.branchId, + customerId: item.customerId, + leadId: item.leadId, + opportunityId: item.opportunityId, + quotationId: item.quotationId, + activityId: item.activityId, + eventId: item.eventId, + entityType: item.entityType, + entityId: item.entityId, + timelineType: item.timelineType, + timelineCategory: item.timelineCategory, + occurredAt: new Date(item.occurredAt), + actorId: item.actorId, + actorDisplay: item.actorDisplay, + title: item.title, + summary: item.summary, + icon: item.icon, + color: item.color, + priority: item.priority, + visibility: item.visibility, + metadata: item.metadata, + sourceEvent: { + eventId: item.eventId, + timelineType: item.timelineType, + entityType: item.entityType, + entityId: item.entityId + }, + sourceProjectionVersion: item.sourceProjectionVersion + }) + .onConflictDoNothing(); +} + +export async function listTimelineItems( + filters: TimelineListFilters, + client: TimelineDbClient = db +): Promise { + const limit = Math.min(filters.limit ?? 25, 100); + const where = buildTimelineWhere(filters); + + const rows = await client + .select() + .from(crmTimelineProjection) + .where(and(...where)) + .orderBy(desc(crmTimelineProjection.occurredAt), desc(crmTimelineProjection.id)) + .limit(limit + 1); + + const visibleRows = rows.slice(0, limit); + + return { + items: visibleRows.map(mapTimelineRow), + nextCursor: rows.length > limit ? visibleRows.at(-1)?.id ?? null : null, + totalItems: visibleRows.length + }; +} + +export async function resetTimelineProjectionForOrganization( + organizationId: string, + client: TimelineDbClient = db +): Promise { + await client + .delete(crmTimelineProjection) + .where(eq(crmTimelineProjection.organizationId, organizationId)); +} + +function buildTimelineWhere(filters: TimelineListFilters): SQL[] { + const where: SQL[] = [eq(crmTimelineProjection.organizationId, filters.organizationId)]; + + if (filters.customerId) where.push(eq(crmTimelineProjection.customerId, filters.customerId)); + if (filters.leadId) where.push(eq(crmTimelineProjection.leadId, filters.leadId)); + if (filters.opportunityId) { + where.push(eq(crmTimelineProjection.opportunityId, filters.opportunityId)); + } + if (filters.quotationId) where.push(eq(crmTimelineProjection.quotationId, filters.quotationId)); + if (filters.activityId) where.push(eq(crmTimelineProjection.activityId, filters.activityId)); + if (filters.eventId) where.push(eq(crmTimelineProjection.eventId, filters.eventId)); + if (filters.category) where.push(eq(crmTimelineProjection.timelineCategory, filters.category)); + if (filters.actorId) where.push(eq(crmTimelineProjection.actorId, filters.actorId)); + if (filters.search) { + where.push(ilike(crmTimelineProjection.title, `%${filters.search}%`)); + } + if (filters.occurredFrom) { + where.push(gte(crmTimelineProjection.occurredAt, new Date(filters.occurredFrom))); + } + if (filters.occurredTo) { + where.push(lte(crmTimelineProjection.occurredAt, new Date(filters.occurredTo))); + } + + return where; +} + +function mapTimelineRow(row: TimelineProjectionRow): TimelineItem { + return { + id: row.id, + organizationId: row.organizationId, + branchId: row.branchId, + customerId: row.customerId, + leadId: row.leadId, + opportunityId: row.opportunityId, + quotationId: row.quotationId, + activityId: row.activityId, + eventId: row.eventId, + entityType: row.entityType, + entityId: row.entityId, + timelineType: row.timelineType, + timelineCategory: row.timelineCategory as TimelineItem['timelineCategory'], + occurredAt: row.occurredAt.toISOString(), + actorId: row.actorId, + actorDisplay: row.actorDisplay, + title: row.title, + summary: row.summary, + icon: row.icon, + color: row.color, + priority: row.priority as TimelineItem['priority'], + visibility: row.visibility as TimelineItem['visibility'], + metadata: row.metadata as Record | null, + sourceProjectionVersion: row.sourceProjectionVersion, + createdAt: row.createdAt.toISOString() + }; +} diff --git a/src/features/foundation/projections/registry.ts b/src/features/foundation/projections/registry.ts index 6f3b657..83ad270 100644 --- a/src/features/foundation/projections/registry.ts +++ b/src/features/foundation/projections/registry.ts @@ -23,12 +23,12 @@ export const PROJECTION_REGISTRY: ProjectionDefinition[] = [ consumedEventTypes: ACTIVITY_EVENTS, supportedVersions: ACTIVITY_EVENT_VERSIONS, rebuildStrategy: 'hybrid', - persistence: 'cached-read-model', + persistence: 'materialized', securityClassification: 'organization-scoped; source-visibility-governed', idempotencyKey: 'consumerName + eventId', retryPolicy: 'projection-default-v1', retentionPolicy: 'event checkpoints retained with projection audit horizon', - implementationStatus: 'contract-only' + implementationStatus: 'active' }, { projectionName: 'Calendar', diff --git a/src/features/foundation/projections/server/store.ts b/src/features/foundation/projections/server/store.ts index d392157..380e112 100644 --- a/src/features/foundation/projections/server/store.ts +++ b/src/features/foundation/projections/server/store.ts @@ -81,8 +81,8 @@ export class DrizzleProjectionRuntimeStore implements ProjectionRuntimeStore { RETURNING business_event_outbox.* `); - const rows = Array.isArray(result) ? result : result.rows; - return rows.map((row) => mapDelivery(row as typeof businessEventOutbox.$inferSelect)); + const rows = result as unknown as Array; + return rows.map((row) => mapDelivery(row)); } async markDeliveryProcessing(eventId: string, now: Date): Promise {