Files
alla-allaos-fullstack/src/db/schema.ts
phaichayon ab56852c47 task-ep.1.6
2026-07-13 16:29:40 +07:00

1572 lines
65 KiB
TypeScript

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<Record<string, unknown> | 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<Record<string, unknown> | null>(),
errorsJson: jsonb('errors_json').$type<unknown[] | null>(),
warningsJson: jsonb('warnings_json').$type<unknown[] | null>(),
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<Record<string, unknown> | 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<Record<string, unknown> | 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
)
})
);