import { boolean, doublePrecision, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; export const users = pgTable( 'users', { id: text('id').primaryKey(), name: text('name').notNull(), email: text('email').notNull(), passwordHash: text('password_hash').notNull(), systemRole: text('system_role').default('user').notNull(), image: text('image'), activeOrganizationId: text('active_organization_id'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ emailIdx: uniqueIndex('users_email_idx').on(table.email) }) ); export const organizations = pgTable( 'organizations', { id: text('id').primaryKey(), name: text('name').notNull(), slug: text('slug').notNull(), imageUrl: text('image_url'), plan: text('plan').default('free').notNull(), createdBy: text('created_by').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ slugIdx: uniqueIndex('organizations_slug_idx').on(table.slug) }) ); export const memberships = pgTable('memberships', { id: text('id').primaryKey(), userId: text('user_id').notNull(), organizationId: text('organization_id').notNull(), role: text('role').default('user').notNull(), businessRole: text('business_role').default('sales_support').notNull(), permissions: text('permissions').array().default([]).notNull(), branchScopeIds: text('branch_scope_ids').array().default([]).notNull(), productTypeScopeIds: text('product_type_scope_ids').array().default([]).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }); export const setupRuns = pgTable('setup_runs', { id: text('id').primaryKey(), status: text('status').default('not_started').notNull(), mode: text('mode').default('production').notNull(), targetOrganizationId: text('target_organization_id'), startedBy: text('started_by').notNull(), completedBy: text('completed_by'), startedAt: timestamp('started_at', { withTimezone: true }).defaultNow().notNull(), completedAt: timestamp('completed_at', { withTimezone: true }), lastError: text('last_error'), metadata: jsonb('metadata').$type | null>(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }); export const setupRunSteps = pgTable( 'setup_run_steps', { id: text('id').primaryKey(), setupRunId: text('setup_run_id').notNull(), stepKey: text('step_key').notNull(), status: text('status').default('not_started').notNull(), inputHash: text('input_hash'), outputJson: jsonb('output_json').$type | null>(), errorsJson: jsonb('errors_json').$type(), warningsJson: jsonb('warnings_json').$type(), startedAt: timestamp('started_at', { withTimezone: true }), completedAt: timestamp('completed_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ setupRunStepIdx: uniqueIndex('setup_run_steps_run_step_idx').on( table.setupRunId, table.stepKey ) }) ); export const seedManifests = pgTable('seed_manifests', { id: text('id').primaryKey(), setupRunId: text('setup_run_id'), organizationId: text('organization_id'), name: text('name').notNull(), version: text('version').notNull(), type: text('type').notNull(), checksum: text('checksum').notNull(), status: text('status').default('pending').notNull(), source: text('source').notNull(), appliedBy: text('applied_by'), appliedAt: timestamp('applied_at', { withTimezone: true }), reportJson: jsonb('report_json').$type | null>(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }); export const seedManifestItems = pgTable( 'seed_manifest_items', { id: text('id').primaryKey(), seedManifestId: text('seed_manifest_id').notNull(), itemKey: text('item_key').notNull(), sourceFile: text('source_file').notNull(), checksum: text('checksum').notNull(), status: text('status').default('pending').notNull(), importedCount: integer('imported_count').default(0).notNull(), updatedCount: integer('updated_count').default(0).notNull(), skippedCount: integer('skipped_count').default(0).notNull(), errorCount: integer('error_count').default(0).notNull(), warningCount: integer('warning_count').default(0).notNull(), reportJson: jsonb('report_json').$type | null>(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ seedManifestItemIdx: uniqueIndex('seed_manifest_items_manifest_item_idx').on( table.seedManifestId, table.itemKey ) }) ); export const crmRoleProfiles = pgTable( 'crm_role_profiles', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), code: text('code').notNull(), name: text('name').notNull(), description: text('description'), permissions: text('permissions').array().default([]).notNull(), branchScopeMode: text('branch_scope_mode').default('assigned').notNull(), productScopeMode: text('product_scope_mode').default('assigned').notNull(), ownershipScope: text('ownership_scope').default('own').notNull(), approvalAuthority: text('approval_authority').default('none').notNull(), isSystem: boolean('is_system').default(false).notNull(), isActive: boolean('is_active').default(true).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull() }, (table) => ({ organizationRoleCodeIdx: uniqueIndex('crm_role_profiles_org_code_idx').on( table.organizationId, table.code ) }) ); export const crmUserRoleAssignments = pgTable( 'crm_user_role_assignments', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), userId: text('user_id').notNull(), roleProfileId: text('role_profile_id').notNull(), branchScopeMode: text('branch_scope_mode').default('inherit').notNull(), branchScopeIds: text('branch_scope_ids').array().default([]).notNull(), productTypeScopeMode: text('product_type_scope_mode').default('inherit').notNull(), productTypeScopeIds: text('product_type_scope_ids').array().default([]).notNull(), isPrimary: boolean('is_primary').default(false).notNull(), isActive: boolean('is_active').default(true).notNull(), assignedBy: text('assigned_by').notNull(), assignedAt: timestamp('assigned_at', { withTimezone: true }).defaultNow().notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }, (table) => ({ organizationUserRoleProfileIdx: uniqueIndex('crm_user_role_assignments_org_user_role_idx').on( table.organizationId, table.userId, table.roleProfileId ) }) ); export const products = pgTable('products', { id: integer('id').primaryKey().generatedAlwaysAsIdentity(), organizationId: text('organization_id').notNull(), name: text('name').notNull(), category: text('category').notNull(), description: text('description').notNull(), photoUrl: text('photo_url').notNull(), price: doublePrecision('price').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }); export const msOptions = pgTable( 'ms_options', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), category: text('category').notNull(), code: text('code').notNull(), label: text('label').notNull(), value: text('value'), parentId: text('parent_id'), sortOrder: integer('sort_order').default(0).notNull(), isActive: boolean('is_active').default(true).notNull(), metadata: jsonb('metadata'), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ organizationCategoryCodeIdx: uniqueIndex('ms_options_org_category_code_idx').on( table.organizationId, table.category, table.code ) }) ); export const documentSequences = pgTable( 'document_sequences', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), branchId: text('branch_id').default('').notNull(), productType: text('product_type').default('all').notNull(), documentType: text('document_type').notNull(), prefix: text('prefix').notNull(), period: text('period').notNull(), currentNumber: integer('current_number').default(0).notNull(), paddingLength: integer('padding_length').default(3).notNull(), format: text('format').default('{prefix}{period}-{running}').notNull(), resetPolicy: text('reset_policy').default('period').notNull(), isActive: boolean('is_active').default(true).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ organizationBranchProductDocumentPeriodIdx: uniqueIndex( 'document_sequences_org_branch_product_doc_period_idx' ).on( table.organizationId, table.branchId, table.productType, table.documentType, table.period ) }) ); export const emailConfigurations = pgTable( 'email_configurations', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), providerType: text('provider_type').default('smtp').notNull(), name: text('name').notNull(), host: text('host').notNull(), port: integer('port').default(587).notNull(), secure: boolean('secure').default(false).notNull(), username: text('username'), passwordEncrypted: text('password_encrypted'), fromEmail: text('from_email').notNull(), fromName: text('from_name'), replyToEmail: text('reply_to_email'), isDefault: boolean('is_default').default(false).notNull(), isActive: boolean('is_active').default(true).notNull(), lastTestStatus: text('last_test_status').default('not_tested').notNull(), lastTestedAt: timestamp('last_tested_at', { withTimezone: true }), lastTestError: text('last_test_error'), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ organizationNameIdx: uniqueIndex('email_configurations_org_name_idx').on( table.organizationId, table.name ) }) ); export const trAuditLogs = pgTable('tr_audit_logs', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), branchId: text('branch_id'), userId: text('user_id').notNull(), entityType: text('entity_type').notNull(), entityId: text('entity_id').notNull(), action: text('action').notNull(), beforeData: jsonb('before_data'), afterData: jsonb('after_data'), requestId: text('request_id'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull() }); export const appNotificationEvents = pgTable( 'app_notification_events', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), eventType: text('event_type').notNull(), entityType: text('entity_type').notNull(), entityId: text('entity_id').notNull(), actorUserId: text('actor_user_id'), payload: jsonb('payload'), dedupeKey: text('dedupe_key'), status: text('status').default('pending').notNull(), lastError: text('last_error'), publishedAt: timestamp('published_at', { withTimezone: true }).defaultNow().notNull(), processedAt: timestamp('processed_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ organizationDedupeIdx: uniqueIndex('app_notification_events_org_dedupe_idx').on( table.organizationId, table.dedupeKey ) }) ); export const appNotifications = pgTable( 'app_notifications', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), recipientUserId: text('recipient_user_id').notNull(), eventId: text('event_id').notNull(), title: text('title').notNull(), body: text('body').notNull(), linkUrl: text('link_url'), severity: text('severity').default('info').notNull(), status: text('status').default('unread').notNull(), readAt: timestamp('read_at', { withTimezone: true }), archivedAt: timestamp('archived_at', { withTimezone: true }), metadata: jsonb('metadata'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ recipientEventIdx: uniqueIndex('app_notifications_recipient_event_idx').on( table.recipientUserId, table.eventId ) }) ); export const appNotificationTemplates = pgTable( 'app_notification_templates', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), eventType: text('event_type').notNull(), code: text('code').default('').notNull(), name: text('name').default('').notNull(), channel: text('channel').notNull(), subjectTemplate: text('subject_template'), titleTemplate: text('title_template').notNull(), bodyTemplate: text('body_template').notNull(), bodyFormat: text('body_format').default('plain').notNull(), linkTemplate: text('link_template'), language: text('language').default('th').notNull(), variables: jsonb('variables'), version: integer('version').default(1).notNull(), isActive: boolean('is_active').default(true).notNull(), metadata: jsonb('metadata'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull() }, (table) => ({ organizationEventChannelIdx: uniqueIndex('app_notification_templates_org_event_channel_idx').on( table.organizationId, table.eventType, table.channel ) }) ); export const appNotificationDispatches = pgTable( 'app_notification_dispatches', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), eventId: text('event_id').notNull(), templateId: text('template_id'), entityType: text('entity_type').notNull(), entityId: text('entity_id').notNull(), channel: text('channel').notNull(), providerKey: text('provider_key').notNull(), status: text('status').default('pending').notNull(), recipientTo: jsonb('recipient_to').notNull(), recipientCc: jsonb('recipient_cc'), recipientBcc: jsonb('recipient_bcc'), replyTo: text('reply_to'), subject: text('subject').notNull(), bodyText: text('body_text').notNull(), bodyHtml: text('body_html'), attachments: jsonb('attachments'), recipientUserId: text('recipient_user_id'), recipientAddress: text('recipient_address'), requestedBy: text('requested_by').notNull(), metadata: jsonb('metadata'), lastError: text('last_error'), retryCount: integer('retry_count').default(0).notNull(), maxRetryCount: integer('max_retry_count').default(3).notNull(), lastAttemptAt: timestamp('last_attempt_at', { withTimezone: true }), sentAt: timestamp('sent_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ organizationEventChannelRecipientIdx: uniqueIndex( 'app_notification_dispatches_org_event_channel_recipient_idx' ).on(table.organizationId, table.eventId, table.channel, table.recipientAddress) }) ); export const appNotificationDeliveries = pgTable('app_notification_deliveries', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), notificationId: text('notification_id').notNull(), channel: text('channel').notNull(), recipientAddress: text('recipient_address'), status: text('status').default('pending').notNull(), attemptCount: integer('attempt_count').default(0).notNull(), lastError: text('last_error'), sentAt: timestamp('sent_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }); export const businessEventOutbox = pgTable( 'business_event_outbox', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), eventType: text('event_type').notNull(), schemaVersion: integer('schema_version').notNull(), entityType: text('entity_type').notNull(), entityId: text('entity_id').notNull(), correlationId: text('correlation_id').notNull(), causationId: text('causation_id'), eventEnvelope: jsonb('event_envelope').notNull(), deliveryStatus: text('delivery_status').default('pending').notNull(), attemptCount: integer('attempt_count').default(0).notNull(), availableAt: timestamp('available_at', { withTimezone: true }).defaultNow().notNull(), claimedBy: text('claimed_by'), claimedAt: timestamp('claimed_at', { withTimezone: true }), claimExpiresAt: timestamp('claim_expires_at', { withTimezone: true }), processingStartedAt: timestamp('processing_started_at', { withTimezone: true }), dispatchedAt: timestamp('dispatched_at', { withTimezone: true }), failedAt: timestamp('failed_at', { withTimezone: true }), deadLetteredAt: timestamp('dead_lettered_at', { withTimezone: true }), lastErrorCode: text('last_error_code'), lastErrorMessage: text('last_error_message'), occurredAt: timestamp('occurred_at', { withTimezone: true }).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ statusAvailableIdx: index('business_event_outbox_status_available_idx').on( table.deliveryStatus, table.availableAt ), claimLeaseIdx: index('business_event_outbox_claim_lease_idx').on( table.deliveryStatus, table.claimExpiresAt ), organizationEventIdx: index('business_event_outbox_org_event_idx').on( table.organizationId, table.eventType ) }) ); export const projectionConsumerCheckpoints = pgTable( 'projection_consumer_checkpoints', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), consumerName: text('consumer_name').notNull(), projectionName: text('projection_name').notNull(), eventId: text('event_id').notNull(), eventType: text('event_type').notNull(), schemaVersion: integer('schema_version').notNull(), processingStatus: text('processing_status').default('pending').notNull(), attemptCount: integer('attempt_count').default(0).notNull(), firstAttemptedAt: timestamp('first_attempted_at', { withTimezone: true }), lastAttemptedAt: timestamp('last_attempted_at', { withTimezone: true }), completedAt: timestamp('completed_at', { withTimezone: true }), nextRetryAt: timestamp('next_retry_at', { withTimezone: true }), errorCode: text('error_code'), errorMessage: text('error_message'), processingDurationMs: integer('processing_duration_ms'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ consumerEventIdx: uniqueIndex('projection_checkpoints_consumer_event_idx').on( table.consumerName, table.eventId ), statusRetryIdx: index('projection_checkpoints_status_retry_idx').on( table.processingStatus, table.nextRetryAt ) }) ); export const projectionDeadLetters = pgTable( 'projection_dead_letters', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), consumerName: text('consumer_name').notNull(), projectionName: text('projection_name').notNull(), eventId: text('event_id').notNull(), eventType: text('event_type').notNull(), schemaVersion: integer('schema_version').notNull(), attemptCount: integer('attempt_count').notNull(), errorCode: text('error_code').notNull(), errorMessage: text('error_message').notNull(), failureMetadata: jsonb('failure_metadata'), status: text('status').default('open').notNull(), resolvedAt: timestamp('resolved_at', { withTimezone: true }), resolvedBy: text('resolved_by'), resolutionReason: text('resolution_reason'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ consumerEventIdx: uniqueIndex('projection_dead_letters_consumer_event_idx').on( table.consumerName, table.eventId ), organizationStatusIdx: index('projection_dead_letters_org_status_idx').on( table.organizationId, table.status ) }) ); export const projectionRebuildRuns = pgTable( 'projection_rebuild_runs', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), projectionName: text('projection_name').notNull(), entityType: text('entity_type'), entityId: text('entity_id'), occurredFrom: timestamp('occurred_from', { withTimezone: true }), occurredTo: timestamp('occurred_to', { withTimezone: true }), dryRun: boolean('dry_run').default(true).notNull(), resetExisting: boolean('reset_existing').default(false).notNull(), sourceStrategy: text('source_strategy').notNull(), status: text('status').default('pending').notNull(), processedCount: integer('processed_count').default(0).notNull(), skippedCount: integer('skipped_count').default(0).notNull(), failedCount: integer('failed_count').default(0).notNull(), errorCode: text('error_code'), errorMessage: text('error_message'), requestedBy: text('requested_by').notNull(), startedAt: timestamp('started_at', { withTimezone: true }), completedAt: timestamp('completed_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ organizationProjectionIdx: index('projection_rebuild_runs_org_projection_idx').on( table.organizationId, table.projectionName ) }) ); export const projectionHealthSnapshots = pgTable( 'projection_health_snapshots', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), projectionName: text('projection_name').notNull(), consumerName: text('consumer_name').notNull(), status: text('status').default('healthy').notNull(), lastProcessedEventId: text('last_processed_event_id'), lastSuccessfulProcessingAt: timestamp('last_successful_processing_at', { withTimezone: true }), pendingCount: integer('pending_count').default(0).notNull(), retryCount: integer('retry_count').default(0).notNull(), deadLetterCount: integer('dead_letter_count').default(0).notNull(), unsupportedVersionCount: integer('unsupported_version_count').default(0).notNull(), processingLagMs: integer('processing_lag_ms').default(0).notNull(), averageProcessingDurationMs: integer('average_processing_duration_ms').default(0).notNull(), lastRebuildAt: timestamp('last_rebuild_at', { withTimezone: true }), rebuildSource: text('rebuild_source'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ organizationConsumerIdx: uniqueIndex('projection_health_org_consumer_idx').on( table.organizationId, table.consumerName ) }) ); 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 crmCalendarProjection = pgTable( 'crm_calendar_projection', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), branchId: text('branch_id'), productType: text('product_type'), customerId: text('customer_id'), leadId: text('lead_id'), opportunityId: text('opportunity_id'), quotationId: text('quotation_id'), approvalId: text('approval_id'), activityId: text('activity_id'), ownerId: text('owner_id'), assigneeId: text('assignee_id'), eventId: text('event_id').notNull(), entityType: text('entity_type').notNull(), entityId: text('entity_id').notNull(), title: text('title').notNull(), summary: text('summary'), location: text('location'), startAt: timestamp('start_at', { withTimezone: true }).notNull(), endAt: timestamp('end_at', { withTimezone: true }).notNull(), timezone: text('timezone').default('Asia/Bangkok').notNull(), priority: text('priority').default('normal').notNull(), category: text('category').notNull(), activityType: text('activity_type'), editable: boolean('editable').default(false).notNull(), milestone: boolean('milestone').default(false).notNull(), overdue: boolean('overdue').default(false).notNull(), hotProject: boolean('hot_project').default(false).notNull(), allDay: boolean('all_day').default(false).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(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ eventVersionIdx: uniqueIndex('crm_calendar_projection_event_version_idx').on( table.eventId, table.sourceProjectionVersion ), organizationStartIdx: index('crm_calendar_projection_org_start_idx').on( table.organizationId, table.startAt ), organizationAssigneeStartIdx: index('crm_calendar_projection_org_assignee_start_idx').on( table.organizationId, table.assigneeId, table.startAt ), organizationOwnerStartIdx: index('crm_calendar_projection_org_owner_start_idx').on( table.organizationId, table.ownerId, table.startAt ), organizationBranchStartIdx: index('crm_calendar_projection_org_branch_start_idx').on( table.organizationId, table.branchId, table.startAt ), organizationCategoryStartIdx: index('crm_calendar_projection_org_category_start_idx').on( table.organizationId, table.category, table.startAt ), customerStartIdx: index('crm_calendar_projection_customer_start_idx').on( table.organizationId, table.customerId, table.startAt ), activityEventIdx: index('crm_calendar_projection_activity_event_idx').on( table.organizationId, table.activityId, table.eventId ) }) ); export const crmCustomers = pgTable( 'crm_customers', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), branchId: text('branch_id'), code: text('code').notNull(), name: text('name').notNull(), abbr: text('abbr'), taxId: text('tax_id'), customerType: text('customer_type').notNull(), customerStatus: text('customer_status').notNull(), address: text('address'), province: text('province'), district: text('district'), subDistrict: text('sub_district'), postalCode: text('postal_code'), country: text('country'), phone: text('phone'), fax: text('fax'), email: text('email'), website: text('website'), leadChannel: text('lead_channel'), customerGroup: text('customer_group'), customerSubGroup: text('customer_sub_group'), ownerUserId: text('owner_user_id'), ownerAssignedAt: timestamp('owner_assigned_at', { withTimezone: true }), ownerAssignedBy: text('owner_assigned_by'), notes: text('notes'), isActive: boolean('is_active').default(true).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull() }, (table) => ({ organizationCodeIdx: uniqueIndex('crm_customers_org_code_idx').on( table.organizationId, table.code ) }) ); export const crmCustomerOwnerHistory = pgTable('crm_customer_owner_history', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), customerId: text('customer_id').notNull(), oldOwnerUserId: text('old_owner_user_id'), newOwnerUserId: text('new_owner_user_id'), changedBy: text('changed_by').notNull(), changedAt: timestamp('changed_at', { withTimezone: true }).defaultNow().notNull(), remark: text('remark'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }); export const crmCustomerContacts = pgTable('crm_customer_contacts', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), customerId: text('customer_id').notNull(), name: text('name').notNull(), position: text('position'), department: text('department'), phone: text('phone'), mobile: text('mobile'), email: text('email'), isPrimary: boolean('is_primary').default(false).notNull(), notes: text('notes'), isActive: boolean('is_active').default(true).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull() }); export const crmLeads = pgTable( 'crm_leads', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), branchId: text('branch_id'), code: text('code').notNull(), customerId: text('customer_id'), contactId: text('contact_id'), description: text('description'), projectName: text('project_name'), projectLocation: text('project_location'), leadChannel: text('lead_channel'), productType: text('product_type'), priority: text('priority'), estimatedValue: doublePrecision('estimated_value'), awarenessId: text('awareness_id'), status: text('status').notNull(), followupStatus: text('followup_status'), lostReason: text('lost_reason'), outcome: text('outcome').default('open').notNull(), ownerMarketingUserId: text('owner_marketing_user_id'), assignedSalesOwnerId: text('assigned_sales_owner_id'), assignedAt: timestamp('assigned_at', { withTimezone: true }), assignedBy: text('assigned_by'), assignmentRemark: text('assignment_remark'), createdBy: text('created_by').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }, (table) => ({ organizationCodeIdx: uniqueIndex('crm_leads_org_code_idx').on(table.organizationId, table.code) }) ); export const crmContactShares = pgTable( 'crm_contact_shares', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), contactId: text('contact_id').notNull(), sharedToUserId: text('shared_to_user_id').notNull(), sharedByUserId: text('shared_by_user_id').notNull(), sharedAt: timestamp('shared_at', { withTimezone: true }).defaultNow().notNull(), isActive: boolean('is_active').default(true).notNull(), remark: text('remark'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }, (table) => ({ contactSharedUserIdx: uniqueIndex('crm_contact_shares_org_contact_shared_user_idx').on( table.organizationId, table.contactId, table.sharedToUserId ) }) ); export const crmOpportunities = pgTable( 'crm_opportunities', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), branchId: text('branch_id'), leadId: text('lead_id'), code: text('code').notNull(), customerId: text('customer_id').notNull(), contactId: text('contact_id'), title: text('title').notNull(), description: text('description'), requirement: text('requirement'), projectName: text('project_name'), projectLocation: text('project_location'), productType: text('product_type').notNull(), status: text('status').notNull(), outcomeStatus: text('outcome_status').default('open').notNull(), priority: text('priority').notNull(), leadChannel: text('lead_channel'), estimatedValue: doublePrecision('estimated_value'), chancePercent: integer('chance_percent'), expectedCloseDate: timestamp('expected_close_date', { withTimezone: true }), projectCloseDate: timestamp('project_close_date', { withTimezone: true }), deliveryDate: timestamp('delivery_date', { withTimezone: true }), competitor: text('competitor'), source: text('source'), notes: text('notes'), isHotProject: boolean('is_hot_project').default(false).notNull(), hotProjectAutoSuggested: boolean('hot_project_auto_suggested').default(false).notNull(), hotProjectManuallyOverridden: boolean('hot_project_manually_overridden') .default(false) .notNull(), isActive: boolean('is_active').default(true).notNull(), pipelineStage: text('pipeline_stage').default('lead').notNull(), closedAt: timestamp('closed_at', { withTimezone: true }), closedWonAt: timestamp('closed_won_at', { withTimezone: true }), closedLostAt: timestamp('closed_lost_at', { withTimezone: true }), closedByUserId: text('closed_by_user_id'), poNumber: text('po_number'), poDate: timestamp('po_date', { withTimezone: true }), poAmount: doublePrecision('po_amount'), poCurrency: text('po_currency'), lostReason: text('lost_reason'), lostDetail: text('lost_detail'), lostCompetitor: text('lost_competitor'), lostRemark: text('lost_remark'), cancelReason: text('cancel_reason'), noQuotationReason: text('no_quotation_reason'), assignedToUserId: text('assigned_to_user_id'), assignedAt: timestamp('assigned_at', { withTimezone: true }), assignedBy: text('assigned_by'), assignmentRemark: text('assignment_remark'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull() }, (table) => ({ organizationCodeIdx: uniqueIndex('crm_opportunities_org_code_idx').on( table.organizationId, table.code ) }) ); export const crmOpportunityFollowups = pgTable('crm_opportunity_followups', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), opportunityId: text('opportunity_id').notNull(), followupDate: timestamp('followup_date', { withTimezone: true }).notNull(), followupType: text('followup_type').notNull(), contactId: text('contact_id'), outcome: text('outcome'), notes: text('notes'), nextFollowupDate: timestamp('next_followup_date', { withTimezone: true }), nextAction: text('next_action'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull() }); export const crmActivities = pgTable('crm_activities', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), primaryEntityType: text('primary_entity_type').notNull(), primaryEntityId: text('primary_entity_id'), primaryRecordLabel: text('primary_record_label'), relatedRecords: jsonb('related_records').default([]).notNull(), customerId: text('customer_id'), contactId: text('contact_id'), leadId: text('lead_id'), opportunityId: text('opportunity_id'), quotationId: text('quotation_id'), activityType: text('activity_type').notNull(), subject: text('subject').notNull(), description: text('description'), ownerId: text('owner_id').notNull(), assignedToId: text('assigned_to_id'), scheduledStartAt: timestamp('scheduled_start_at', { withTimezone: true }), scheduledEndAt: timestamp('scheduled_end_at', { withTimezone: true }), dueAt: timestamp('due_at', { withTimezone: true }), completedAt: timestamp('completed_at', { withTimezone: true }), cancelledAt: timestamp('cancelled_at', { withTimezone: true }), skippedAt: timestamp('skipped_at', { withTimezone: true }), status: text('status').notNull(), priority: text('priority').notNull(), outcomeSummary: text('outcome_summary'), cancellationReason: text('cancellation_reason'), skipReason: text('skip_reason'), nextAction: text('next_action'), branchId: text('branch_id'), productType: text('product_type'), isInternalOnly: boolean('is_internal_only').default(false).notNull(), isPricingSensitive: boolean('is_pricing_sensitive').default(false).notNull(), parentActivityId: text('parent_activity_id'), metadata: jsonb('metadata'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull() }); export const crmOpportunityCustomers = pgTable('crm_opportunity_customers', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), opportunityId: text('opportunity_id').notNull(), customerId: text('customer_id').notNull(), role: text('role').notNull(), remark: text('remark'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }); export const crmOpportunityAttachments = pgTable('crm_opportunity_attachments', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), opportunityId: text('opportunity_id').notNull(), category: text('category').notNull(), fileName: text('file_name').notNull(), originalFileName: text('original_file_name').notNull(), storageProvider: text('storage_provider').notNull(), storageKey: text('storage_key').notNull(), fileSize: integer('file_size'), fileType: text('file_type'), description: text('description'), uploadedAt: timestamp('uploaded_at', { withTimezone: true }).defaultNow().notNull(), uploadedBy: text('uploaded_by').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }); export const crmReportDefinitions = pgTable( 'crm_report_definitions', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), code: text('code').notNull(), name: text('name').notNull(), description: text('description'), category: text('category').notNull(), isSystem: boolean('is_system').default(true).notNull(), isActive: boolean('is_active').default(true).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => ({ organizationCodeIdx: uniqueIndex('crm_report_definitions_org_code_idx').on( table.organizationId, table.code ) }) ); export const crmQuotations = pgTable( 'crm_quotations', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), branchId: text('branch_id'), code: text('code').notNull(), opportunityId: text('opportunity_id'), customerId: text('customer_id').notNull(), contactId: text('contact_id'), quotationDate: timestamp('quotation_date', { withTimezone: true }).notNull(), validUntil: timestamp('valid_until', { withTimezone: true }), quotationType: text('quotation_type').notNull(), projectName: text('project_name'), projectLocation: text('project_location'), attention: text('attention'), reference: text('reference'), notes: text('notes'), status: text('status').notNull(), revision: integer('revision').default(0).notNull(), parentQuotationId: text('parent_quotation_id'), revisionRemark: text('revision_remark'), currency: text('currency').notNull(), exchangeRate: doublePrecision('exchange_rate').default(1).notNull(), subtotal: doublePrecision('subtotal').default(0).notNull(), discount: doublePrecision('discount').default(0).notNull(), discountType: text('discount_type'), taxRate: doublePrecision('tax_rate').default(0).notNull(), taxAmount: doublePrecision('tax_amount').default(0).notNull(), totalAmount: doublePrecision('total_amount').default(0).notNull(), chancePercent: integer('chance_percent'), projectCloseDate: timestamp('project_close_date', { withTimezone: true }), deliveryDate: timestamp('delivery_date', { withTimezone: true }), isHotProject: boolean('is_hot_project').default(false).notNull(), hotProjectAutoSuggested: boolean('hot_project_auto_suggested').default(false).notNull(), hotProjectManuallyOverridden: boolean('hot_project_manually_overridden') .default(false) .notNull(), competitor: text('competitor'), salesmanId: text('salesman_id'), isSent: boolean('is_sent').default(false).notNull(), sentAt: timestamp('sent_at', { withTimezone: true }), sentVia: text('sent_via'), approvedAt: timestamp('approved_at', { withTimezone: true }), approvedArtifactId: text('approved_artifact_id'), approvedPdfUrl: text('approved_pdf_url'), approvedSnapshot: jsonb('approved_snapshot'), approvedTemplateVersionId: text('approved_template_version_id'), acceptedAt: timestamp('accepted_at', { withTimezone: true }), rejectedAt: timestamp('rejected_at', { withTimezone: true }), rejectionReason: text('rejection_reason'), isActive: boolean('is_active').default(true).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull() }, (table) => ({ organizationCodeIdx: uniqueIndex('crm_quotations_org_code_idx').on( table.organizationId, table.code ) }) ); export const crmQuotationItems = pgTable('crm_quotation_items', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), quotationId: text('quotation_id').notNull(), itemNumber: integer('item_number').notNull(), productType: text('product_type').notNull(), description: text('description').notNull(), quantity: doublePrecision('quantity').default(0).notNull(), unit: text('unit'), unitPrice: doublePrecision('unit_price').default(0).notNull(), discount: doublePrecision('discount').default(0).notNull(), discountType: text('discount_type'), taxRate: doublePrecision('tax_rate').default(0).notNull(), totalPrice: doublePrecision('total_price').default(0).notNull(), notes: text('notes'), sortOrder: integer('sort_order').default(0).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }); export const crmQuotationCustomers = pgTable('crm_quotation_customers', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), quotationId: text('quotation_id').notNull(), customerId: text('customer_id').notNull(), role: text('role').notNull(), isPrimary: boolean('is_primary').default(false).notNull(), remark: text('remark'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }); export const crmQuotationTopics = pgTable('crm_quotation_topics', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), quotationId: text('quotation_id').notNull(), topicType: text('topic_type').notNull(), title: text('title').notNull(), sortOrder: integer('sort_order').default(0).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }); export const crmQuotationTopicItems = pgTable('crm_quotation_topic_items', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), topicId: text('topic_id').notNull(), content: text('content').notNull(), sortOrder: integer('sort_order').default(0).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }); export const crmQuotationFollowups = pgTable('crm_quotation_followups', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), quotationId: text('quotation_id').notNull(), followupDate: timestamp('followup_date', { withTimezone: true }).notNull(), followupType: text('followup_type').notNull(), contactId: text('contact_id'), outcome: text('outcome'), notes: text('notes'), nextFollowupDate: timestamp('next_followup_date', { withTimezone: true }), nextAction: text('next_action'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull() }); export const crmQuotationAttachments = pgTable('crm_quotation_attachments', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), quotationId: text('quotation_id').notNull(), fileName: text('file_name').notNull(), originalFileName: text('original_file_name').notNull(), filePath: text('file_path').notNull(), fileSize: integer('file_size'), fileType: text('file_type'), description: text('description'), uploadedAt: timestamp('uploaded_at', { withTimezone: true }).defaultNow().notNull(), uploadedBy: text('uploaded_by').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }); export const crmDocumentArtifacts = pgTable('crm_document_artifacts', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), entityType: text('entity_type').notNull(), entityId: text('entity_id').notNull(), documentType: text('document_type').notNull(), artifactType: text('artifact_type').notNull(), templateVersionId: text('template_version_id'), storageProvider: text('storage_provider').notNull(), storageKey: text('storage_key').notNull(), fileName: text('file_name').notNull(), contentType: text('content_type').notNull(), fileSize: integer('file_size'), checksum: text('checksum'), status: text('status').default('active').notNull(), generatedBy: text('generated_by').notNull(), generatedAt: timestamp('generated_at', { withTimezone: true }).defaultNow().notNull(), lockedAt: timestamp('locked_at', { withTimezone: true }), lockedBy: text('locked_by'), voidedAt: timestamp('voided_at', { withTimezone: true }), voidedBy: text('voided_by'), voidReason: text('void_reason'), metadata: jsonb('metadata'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }); export const crmApprovalWorkflows = pgTable( 'crm_approval_workflows', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), code: text('code').notNull(), name: text('name').notNull(), entityType: text('entity_type').notNull(), description: text('description'), isSystem: boolean('is_system').default(false).notNull(), isActive: boolean('is_active').default(true).notNull(), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }, (table) => ({ organizationCodeIdx: uniqueIndex('crm_approval_workflows_org_code_idx').on( table.organizationId, table.code ) }) ); export const crmApprovalSteps = pgTable( 'crm_approval_steps', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), workflowId: text('workflow_id').notNull(), stepNumber: integer('step_number').notNull(), roleCode: text('role_code').notNull(), roleName: text('role_name').notNull(), approvalMode: text('approval_mode').default('sequential').notNull(), isRequired: boolean('is_required').default(true).notNull(), slaHours: integer('sla_hours').default(24).notNull(), calendarMode: text('calendar_mode').default('calendar_days').notNull(), timeoutAction: text('timeout_action').default('none').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }, (table) => ({ workflowStepIdx: uniqueIndex('crm_approval_steps_workflow_step_idx').on( table.workflowId, table.stepNumber ) }) ); export const crmApprovalMatrices = pgTable('crm_approval_matrices', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), entityType: text('entity_type').notNull(), workflowId: text('workflow_id').notNull(), branchId: text('branch_id'), productType: text('product_type'), minAmount: doublePrecision('min_amount'), maxAmount: doublePrecision('max_amount'), currency: text('currency'), priority: integer('priority').default(100).notNull(), isDefault: boolean('is_default').default(false).notNull(), isActive: boolean('is_active').default(true).notNull(), description: text('description'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull() }); export const crmApprovalRequests = pgTable('crm_approval_requests', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), workflowId: text('workflow_id').notNull(), entityType: text('entity_type').notNull(), entityId: text('entity_id').notNull(), currentStep: integer('current_step').default(1).notNull(), currentStepStartedAt: timestamp('current_step_started_at', { withTimezone: true }) .defaultNow() .notNull(), status: text('status').notNull(), requestedBy: text('requested_by').notNull(), requestedAt: timestamp('requested_at', { withTimezone: true }).defaultNow().notNull(), completedAt: timestamp('completed_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }); export const crmApprovalReminderPolicies = pgTable( 'crm_approval_reminder_policies', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), workflowId: text('workflow_id').notNull(), stepNumber: integer('step_number').notNull(), slaHours: integer('sla_hours').default(24).notNull(), reminderOffsetsHours: integer('reminder_offsets_hours').array().default([]).notNull(), calendarMode: text('calendar_mode').default('calendar_days').notNull(), timeoutAction: text('timeout_action').default('none').notNull(), isActive: boolean('is_active').default(true).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull() }, (table) => ({ workflowStepIdx: uniqueIndex('crm_approval_reminder_policies_workflow_step_idx').on( table.workflowId, table.stepNumber ) }) ); export const crmApprovalEscalationPolicies = pgTable('crm_approval_escalation_policies', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), workflowId: text('workflow_id').notNull(), stepNumber: integer('step_number').notNull(), triggerAfterHours: integer('trigger_after_hours').notNull(), targetType: text('target_type').notNull(), targetValue: text('target_value'), isActive: boolean('is_active').default(true).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull() }); export const crmApprovalActions = pgTable('crm_approval_actions', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), approvalRequestId: text('approval_request_id').notNull(), stepNumber: integer('step_number').notNull(), action: text('action').notNull(), remark: text('remark'), actedBy: text('acted_by').notNull(), actedAt: timestamp('acted_at', { withTimezone: true }).defaultNow().notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }); export const crmDocumentTemplates = pgTable( 'crm_document_templates', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), documentType: text('document_type').notNull(), productType: text('product_type').notNull(), fileType: text('file_type').notNull(), templateName: text('template_name').notNull(), description: text('description'), isDefault: boolean('is_default').default(false).notNull(), isActive: boolean('is_active').default(true).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull() }, (table) => ({ organizationTemplateIdx: uniqueIndex('crm_document_templates_org_doc_product_file_name_idx').on( table.organizationId, table.documentType, table.productType, table.fileType, table.templateName ) }) ); export const crmDocumentTemplateVersions = pgTable( 'crm_document_template_versions', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), templateId: text('template_id').notNull(), version: text('version').notNull(), filePath: text('file_path'), schemaJson: jsonb('schema_json').notNull(), metadataJson: jsonb('metadata_json'), previewImageUrl: text('preview_image_url'), isActive: boolean('is_active').default(true).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull() }, (table) => ({ templateVersionIdx: uniqueIndex('crm_document_template_versions_template_version_idx').on( table.templateId, table.version ) }) ); export const crmDocumentLibraries = pgTable( 'crm_document_libraries', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), code: text('code').notNull(), name: text('name').notNull(), documentType: text('document_type').notNull(), description: text('description'), brand: text('brand').notNull(), language: text('language').notNull(), productType: text('product_type').notNull(), status: text('status').default('active').notNull(), isActive: boolean('is_active').default(true).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull() }, (table) => ({ organizationCodeIdx: uniqueIndex('crm_document_libraries_org_code_idx').on( table.organizationId, table.code ) }) ); export const crmDocumentLibraryVersions = pgTable( 'crm_document_library_versions', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), libraryId: text('library_id').notNull(), version: text('version').notNull(), fileName: text('file_name').notNull(), storageProvider: text('storage_provider').notNull(), storageKey: text('storage_key').notNull(), mimeType: text('mime_type').notNull(), fileSize: integer('file_size').notNull(), checksum: text('checksum').notNull(), pageCount: integer('page_count').notNull(), status: text('status').default('draft').notNull(), isActive: boolean('is_active').default(false).notNull(), publishedAt: timestamp('published_at', { withTimezone: true }), publishedBy: text('published_by'), archivedAt: timestamp('archived_at', { withTimezone: true }), archivedBy: text('archived_by'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdBy: text('created_by').notNull() }, (table) => ({ libraryVersionIdx: uniqueIndex('crm_document_library_versions_library_version_idx').on( table.libraryId, table.version ) }) ); export const crmDocumentTemplateMappings = pgTable( 'crm_document_template_mappings', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), templateVersionId: text('template_version_id').notNull(), placeholderKey: text('placeholder_key').notNull(), sourcePath: text('source_path').notNull(), dataType: text('data_type').notNull(), sheetName: text('sheet_name'), defaultValue: text('default_value'), formatMask: text('format_mask'), sortOrder: integer('sort_order').default(0).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }, (table) => ({ templatePlaceholderIdx: uniqueIndex( 'crm_document_template_mappings_version_placeholder_idx' ).on(table.templateVersionId, table.placeholderKey) }) ); export const crmDocumentTemplateTableColumns = pgTable( 'crm_document_template_table_columns', { id: text('id').primaryKey(), organizationId: text('organization_id').notNull(), mappingId: text('mapping_id').notNull(), columnName: text('column_name').notNull(), sourceField: text('source_field').notNull(), columnLetter: text('column_letter'), sortOrder: integer('sort_order').default(0).notNull(), formatMask: text('format_mask'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true }) }, (table) => ({ mappingColumnIdx: uniqueIndex('crm_document_template_table_columns_mapping_name_idx').on( table.mappingId, table.columnName ) }) );