From 8c010221fbc83ebdf887fe43b023ae485f35fa90 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Tue, 27 Jan 2026 09:06:11 -0600 Subject: [PATCH] feat: Propagate core entities from erp-construccion Modules added: - audit (7 entities): audit logs, config changes, entity changes - billing-usage (14 entities): subscriptions, invoices, coupons - core (13 entities): countries, currencies, UoMs, sequences - invoices (4 entities): invoices, payments, allocations - notifications (6 entities): notifications, templates, channels Total: 44 new entity files Build: Clean (0 TypeScript errors) Co-Authored-By: Claude Opus 4.5 --- .../audit/entities/audit-log.entity.ts | 116 +++++++++++ .../audit/entities/config-change.entity.ts | 55 ++++++ .../audit/entities/data-export.entity.ts | 88 +++++++++ .../audit/entities/entity-change.entity.ts | 63 ++++++ src/modules/audit/entities/index.ts | 11 ++ .../audit/entities/login-history.entity.ts | 114 +++++++++++ .../entities/permission-change.entity.ts | 71 +++++++ .../entities/sensitive-data-access.entity.ts | 70 +++++++ .../entities/billing-alert.entity.ts | 72 +++++++ .../entities/coupon-redemption.entity.ts | 44 +++++ .../billing-usage/entities/coupon.entity.ts | 72 +++++++ src/modules/billing-usage/entities/index.ts | 13 ++ .../entities/invoice-item.entity.ts | 65 ++++++ .../billing-usage/entities/invoice.entity.ts | 17 ++ .../entities/payment-method.entity.ts | 85 ++++++++ .../entities/plan-feature.entity.ts | 61 ++++++ .../entities/plan-limit.entity.ts | 52 +++++ .../entities/stripe-event.entity.ts | 43 ++++ .../entities/subscription-plan.entity.ts | 83 ++++++++ .../entities/tenant-subscription.entity.ts | 132 +++++++++++++ .../entities/usage-event.entity.ts | 73 +++++++ .../entities/usage-tracking.entity.ts | 91 +++++++++ src/modules/core/entities/country.entity.ts | 35 ++++ .../core/entities/currency-rate.entity.ts | 55 ++++++ src/modules/core/entities/currency.entity.ts | 43 ++++ .../core/entities/discount-rule.entity.ts | 163 +++++++++++++++ src/modules/core/entities/index.ts | 19 ++ .../core/entities/payment-term.entity.ts | 144 ++++++++++++++ .../core/entities/product-category.entity.ts | 79 ++++++++ src/modules/core/entities/sequence.entity.ts | 83 ++++++++ src/modules/core/entities/state.entity.ts | 45 +++++ src/modules/core/entities/tenant.entity.ts | 50 +++++ .../core/entities/uom-category.entity.ts | 45 +++++ src/modules/core/entities/uom.entity.ts | 89 +++++++++ src/modules/core/entities/user.entity.ts | 78 ++++++++ src/modules/invoices/entities/index.ts | 8 + .../invoices/entities/invoice-item.entity.ts | 95 +++++++++ .../invoices/entities/invoice.entity.ts | 187 ++++++++++++++++++ .../entities/payment-allocation.entity.ts | 53 +++++ .../invoices/entities/payment.entity.ts | 89 +++++++++ .../notifications/entities/channel.entity.ts | 65 ++++++ .../entities/in-app-notification.entity.ts | 85 ++++++++ src/modules/notifications/entities/index.ts | 10 + .../entities/notification-batch.entity.ts | 96 +++++++++ .../entities/notification.entity.ts | 138 +++++++++++++ .../entities/preference.entity.ts | 82 ++++++++ .../notifications/entities/template.entity.ts | 126 ++++++++++++ 47 files changed, 3453 insertions(+) create mode 100644 src/modules/audit/entities/audit-log.entity.ts create mode 100644 src/modules/audit/entities/config-change.entity.ts create mode 100644 src/modules/audit/entities/data-export.entity.ts create mode 100644 src/modules/audit/entities/entity-change.entity.ts create mode 100644 src/modules/audit/entities/index.ts create mode 100644 src/modules/audit/entities/login-history.entity.ts create mode 100644 src/modules/audit/entities/permission-change.entity.ts create mode 100644 src/modules/audit/entities/sensitive-data-access.entity.ts create mode 100644 src/modules/billing-usage/entities/billing-alert.entity.ts create mode 100644 src/modules/billing-usage/entities/coupon-redemption.entity.ts create mode 100644 src/modules/billing-usage/entities/coupon.entity.ts create mode 100644 src/modules/billing-usage/entities/index.ts create mode 100644 src/modules/billing-usage/entities/invoice-item.entity.ts create mode 100644 src/modules/billing-usage/entities/invoice.entity.ts create mode 100644 src/modules/billing-usage/entities/payment-method.entity.ts create mode 100644 src/modules/billing-usage/entities/plan-feature.entity.ts create mode 100644 src/modules/billing-usage/entities/plan-limit.entity.ts create mode 100644 src/modules/billing-usage/entities/stripe-event.entity.ts create mode 100644 src/modules/billing-usage/entities/subscription-plan.entity.ts create mode 100644 src/modules/billing-usage/entities/tenant-subscription.entity.ts create mode 100644 src/modules/billing-usage/entities/usage-event.entity.ts create mode 100644 src/modules/billing-usage/entities/usage-tracking.entity.ts create mode 100644 src/modules/core/entities/country.entity.ts create mode 100644 src/modules/core/entities/currency-rate.entity.ts create mode 100644 src/modules/core/entities/currency.entity.ts create mode 100644 src/modules/core/entities/discount-rule.entity.ts create mode 100644 src/modules/core/entities/index.ts create mode 100644 src/modules/core/entities/payment-term.entity.ts create mode 100644 src/modules/core/entities/product-category.entity.ts create mode 100644 src/modules/core/entities/sequence.entity.ts create mode 100644 src/modules/core/entities/state.entity.ts create mode 100644 src/modules/core/entities/tenant.entity.ts create mode 100644 src/modules/core/entities/uom-category.entity.ts create mode 100644 src/modules/core/entities/uom.entity.ts create mode 100644 src/modules/core/entities/user.entity.ts create mode 100644 src/modules/invoices/entities/index.ts create mode 100644 src/modules/invoices/entities/invoice-item.entity.ts create mode 100644 src/modules/invoices/entities/invoice.entity.ts create mode 100644 src/modules/invoices/entities/payment-allocation.entity.ts create mode 100644 src/modules/invoices/entities/payment.entity.ts create mode 100644 src/modules/notifications/entities/channel.entity.ts create mode 100644 src/modules/notifications/entities/in-app-notification.entity.ts create mode 100644 src/modules/notifications/entities/index.ts create mode 100644 src/modules/notifications/entities/notification-batch.entity.ts create mode 100644 src/modules/notifications/entities/notification.entity.ts create mode 100644 src/modules/notifications/entities/preference.entity.ts create mode 100644 src/modules/notifications/entities/template.entity.ts diff --git a/src/modules/audit/entities/audit-log.entity.ts b/src/modules/audit/entities/audit-log.entity.ts new file mode 100644 index 0000000..7ff4c9a --- /dev/null +++ b/src/modules/audit/entities/audit-log.entity.ts @@ -0,0 +1,116 @@ +/** + * AuditLog Entity + * General activity tracking with full request context + * Compatible with erp-core audit-log.entity + * + * @module Audit + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; + +export type AuditAction = 'create' | 'read' | 'update' | 'delete' | 'login' | 'logout' | 'export'; +export type AuditCategory = 'data' | 'auth' | 'system' | 'config' | 'billing'; +export type AuditStatus = 'success' | 'failure' | 'partial'; + +@Entity({ name: 'audit_logs', schema: 'audit' }) +export class AuditLog { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Index() + @Column({ name: 'user_id', type: 'uuid', nullable: true }) + userId: string; + + @Column({ name: 'user_email', type: 'varchar', length: 255, nullable: true }) + userEmail: string; + + @Column({ name: 'user_name', type: 'varchar', length: 200, nullable: true }) + userName: string; + + @Column({ name: 'session_id', type: 'uuid', nullable: true }) + sessionId: string; + + @Column({ name: 'impersonator_id', type: 'uuid', nullable: true }) + impersonatorId: string; + + @Index() + @Column({ name: 'action', type: 'varchar', length: 50 }) + action: AuditAction; + + @Index() + @Column({ name: 'action_category', type: 'varchar', length: 50, nullable: true }) + actionCategory: AuditCategory; + + @Index() + @Column({ name: 'resource_type', type: 'varchar', length: 100 }) + resourceType: string; + + @Column({ name: 'resource_id', type: 'uuid', nullable: true }) + resourceId: string; + + @Column({ name: 'resource_name', type: 'varchar', length: 255, nullable: true }) + resourceName: string; + + @Column({ name: 'old_values', type: 'jsonb', nullable: true }) + oldValues: Record; + + @Column({ name: 'new_values', type: 'jsonb', nullable: true }) + newValues: Record; + + @Column({ name: 'changed_fields', type: 'text', array: true, nullable: true }) + changedFields: string[]; + + @Column({ name: 'ip_address', type: 'inet', nullable: true }) + ipAddress: string; + + @Column({ name: 'user_agent', type: 'text', nullable: true }) + userAgent: string; + + @Column({ name: 'device_info', type: 'jsonb', default: {} }) + deviceInfo: Record; + + @Column({ name: 'location', type: 'jsonb', default: {} }) + location: Record; + + @Column({ name: 'request_id', type: 'varchar', length: 100, nullable: true }) + requestId: string; + + @Column({ name: 'request_method', type: 'varchar', length: 10, nullable: true }) + requestMethod: string; + + @Column({ name: 'request_path', type: 'text', nullable: true }) + requestPath: string; + + @Column({ name: 'request_params', type: 'jsonb', default: {} }) + requestParams: Record; + + @Index() + @Column({ name: 'status', type: 'varchar', length: 20, default: 'success' }) + status: AuditStatus; + + @Column({ name: 'error_message', type: 'text', nullable: true }) + errorMessage: string; + + @Column({ name: 'duration_ms', type: 'int', nullable: true }) + durationMs: number; + + @Column({ name: 'metadata', type: 'jsonb', default: {} }) + metadata: Record; + + @Column({ name: 'tags', type: 'text', array: true, default: [] }) + tags: string[]; + + @Index() + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; +} diff --git a/src/modules/audit/entities/config-change.entity.ts b/src/modules/audit/entities/config-change.entity.ts new file mode 100644 index 0000000..ecb212d --- /dev/null +++ b/src/modules/audit/entities/config-change.entity.ts @@ -0,0 +1,55 @@ +/** + * ConfigChange Entity + * System configuration change auditing + * Compatible with erp-core config-change.entity + * + * @module Audit + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + Index, +} from 'typeorm'; + +export type ConfigType = 'tenant_settings' | 'user_settings' | 'system_settings' | 'feature_flags'; + +@Entity({ name: 'config_changes', schema: 'audit' }) +export class ConfigChange { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid', nullable: true }) + tenantId: string; + + @Column({ name: 'changed_by', type: 'uuid' }) + changedBy: string; + + @Index() + @Column({ name: 'config_type', type: 'varchar', length: 50 }) + configType: ConfigType; + + @Column({ name: 'config_key', type: 'varchar', length: 100 }) + configKey: string; + + @Column({ name: 'config_path', type: 'text', nullable: true }) + configPath: string; + + @Column({ name: 'old_value', type: 'jsonb', nullable: true }) + oldValue: Record; + + @Column({ name: 'new_value', type: 'jsonb', nullable: true }) + newValue: Record; + + @Column({ name: 'reason', type: 'text', nullable: true }) + reason: string; + + @Column({ name: 'ticket_id', type: 'varchar', length: 50, nullable: true }) + ticketId: string; + + @Index() + @Column({ name: 'changed_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) + changedAt: Date; +} diff --git a/src/modules/audit/entities/data-export.entity.ts b/src/modules/audit/entities/data-export.entity.ts new file mode 100644 index 0000000..54f38a8 --- /dev/null +++ b/src/modules/audit/entities/data-export.entity.ts @@ -0,0 +1,88 @@ +/** + * DataExport Entity + * GDPR/reporting data export request management + * Compatible with erp-core data-export.entity + * + * @module Audit + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + Index, +} from 'typeorm'; + +export type ExportType = 'report' | 'backup' | 'gdpr_request' | 'bulk_export'; +export type ExportFormat = 'csv' | 'xlsx' | 'pdf' | 'json'; +export type ExportStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'expired'; + +@Entity({ name: 'data_exports', schema: 'audit' }) +export class DataExport { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Index() + @Column({ name: 'user_id', type: 'uuid' }) + userId: string; + + @Column({ name: 'export_type', type: 'varchar', length: 50 }) + exportType: ExportType; + + @Column({ name: 'export_format', type: 'varchar', length: 20, nullable: true }) + exportFormat: ExportFormat; + + @Column({ name: 'entity_types', type: 'text', array: true }) + entityTypes: string[]; + + @Column({ name: 'filters', type: 'jsonb', default: {} }) + filters: Record; + + @Column({ name: 'date_range_start', type: 'timestamptz', nullable: true }) + dateRangeStart: Date; + + @Column({ name: 'date_range_end', type: 'timestamptz', nullable: true }) + dateRangeEnd: Date; + + @Column({ name: 'record_count', type: 'int', nullable: true }) + recordCount: number; + + @Column({ name: 'file_size_bytes', type: 'bigint', nullable: true }) + fileSizeBytes: number; + + @Column({ name: 'file_hash', type: 'varchar', length: 64, nullable: true }) + fileHash: string; + + @Index() + @Column({ name: 'status', type: 'varchar', length: 20, default: 'pending' }) + status: ExportStatus; + + @Column({ name: 'download_url', type: 'text', nullable: true }) + downloadUrl: string; + + @Column({ name: 'download_expires_at', type: 'timestamptz', nullable: true }) + downloadExpiresAt: Date; + + @Column({ name: 'download_count', type: 'int', default: 0 }) + downloadCount: number; + + @Column({ name: 'ip_address', type: 'inet', nullable: true }) + ipAddress: string; + + @Column({ name: 'user_agent', type: 'text', nullable: true }) + userAgent: string; + + @Index() + @Column({ name: 'requested_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) + requestedAt: Date; + + @Column({ name: 'completed_at', type: 'timestamptz', nullable: true }) + completedAt: Date; + + @Column({ name: 'expires_at', type: 'timestamptz', nullable: true }) + expiresAt: Date; +} diff --git a/src/modules/audit/entities/entity-change.entity.ts b/src/modules/audit/entities/entity-change.entity.ts new file mode 100644 index 0000000..6b73ce6 --- /dev/null +++ b/src/modules/audit/entities/entity-change.entity.ts @@ -0,0 +1,63 @@ +/** + * EntityChange Entity + * Data modification versioning and change history + * Compatible with erp-core entity-change.entity + * + * @module Audit + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + Index, +} from 'typeorm'; + +export type ChangeType = 'create' | 'update' | 'delete' | 'restore'; + +@Entity({ name: 'entity_changes', schema: 'audit' }) +export class EntityChange { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Index() + @Column({ name: 'entity_type', type: 'varchar', length: 100 }) + entityType: string; + + @Index() + @Column({ name: 'entity_id', type: 'uuid' }) + entityId: string; + + @Column({ name: 'entity_name', type: 'varchar', length: 255, nullable: true }) + entityName: string; + + @Column({ name: 'version', type: 'int', default: 1 }) + version: number; + + @Column({ name: 'previous_version', type: 'int', nullable: true }) + previousVersion: number; + + @Column({ name: 'data_snapshot', type: 'jsonb' }) + dataSnapshot: Record; + + @Column({ name: 'changes', type: 'jsonb', default: [] }) + changes: Record[]; + + @Index() + @Column({ name: 'changed_by', type: 'uuid', nullable: true }) + changedBy: string; + + @Column({ name: 'change_reason', type: 'text', nullable: true }) + changeReason: string; + + @Column({ name: 'change_type', type: 'varchar', length: 20 }) + changeType: ChangeType; + + @Index() + @Column({ name: 'changed_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) + changedAt: Date; +} diff --git a/src/modules/audit/entities/index.ts b/src/modules/audit/entities/index.ts new file mode 100644 index 0000000..feda39f --- /dev/null +++ b/src/modules/audit/entities/index.ts @@ -0,0 +1,11 @@ +/** + * Audit Entities - Export + */ + +export { AuditLog, AuditAction, AuditCategory, AuditStatus } from './audit-log.entity'; +export { EntityChange, ChangeType } from './entity-change.entity'; +export { LoginHistory, LoginStatus, AuthMethod, MfaMethod } from './login-history.entity'; +export { SensitiveDataAccess, DataType, AccessType } from './sensitive-data-access.entity'; +export { DataExport, ExportType, ExportFormat, ExportStatus } from './data-export.entity'; +export { PermissionChange, PermissionChangeType, PermissionScope } from './permission-change.entity'; +export { ConfigChange, ConfigType } from './config-change.entity'; diff --git a/src/modules/audit/entities/login-history.entity.ts b/src/modules/audit/entities/login-history.entity.ts new file mode 100644 index 0000000..8fc339e --- /dev/null +++ b/src/modules/audit/entities/login-history.entity.ts @@ -0,0 +1,114 @@ +/** + * LoginHistory Entity + * Authentication event tracking with device, location and risk scoring + * Compatible with erp-core login-history.entity + * + * @module Audit + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + Index, +} from 'typeorm'; + +export type LoginStatus = 'success' | 'failed' | 'blocked' | 'mfa_required' | 'mfa_failed'; +export type AuthMethod = 'password' | 'sso' | 'oauth' | 'mfa' | 'magic_link' | 'biometric'; +export type MfaMethod = 'totp' | 'sms' | 'email' | 'push'; + +@Entity({ name: 'login_history', schema: 'audit' }) +export class LoginHistory { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid', nullable: true }) + tenantId: string; + + @Index() + @Column({ name: 'user_id', type: 'uuid', nullable: true }) + userId: string; + + @Column({ name: 'email', type: 'varchar', length: 255, nullable: true }) + email: string; + + @Column({ name: 'username', type: 'varchar', length: 100, nullable: true }) + username: string; + + @Index() + @Column({ name: 'status', type: 'varchar', length: 20 }) + status: LoginStatus; + + @Column({ name: 'auth_method', type: 'varchar', length: 30, nullable: true }) + authMethod: AuthMethod; + + @Column({ name: 'oauth_provider', type: 'varchar', length: 30, nullable: true }) + oauthProvider: string; + + @Column({ name: 'mfa_method', type: 'varchar', length: 20, nullable: true }) + mfaMethod: MfaMethod; + + @Column({ name: 'mfa_verified', type: 'boolean', nullable: true }) + mfaVerified: boolean; + + @Column({ name: 'device_id', type: 'uuid', nullable: true }) + deviceId: string; + + @Column({ name: 'device_fingerprint', type: 'varchar', length: 255, nullable: true }) + deviceFingerprint: string; + + @Column({ name: 'device_type', type: 'varchar', length: 30, nullable: true }) + deviceType: string; + + @Column({ name: 'device_os', type: 'varchar', length: 50, nullable: true }) + deviceOs: string; + + @Column({ name: 'device_browser', type: 'varchar', length: 50, nullable: true }) + deviceBrowser: string; + + @Index() + @Column({ name: 'ip_address', type: 'inet', nullable: true }) + ipAddress: string; + + @Column({ name: 'user_agent', type: 'text', nullable: true }) + userAgent: string; + + @Column({ name: 'country_code', type: 'varchar', length: 2, nullable: true }) + countryCode: string; + + @Column({ name: 'city', type: 'varchar', length: 100, nullable: true }) + city: string; + + @Column({ name: 'latitude', type: 'decimal', precision: 10, scale: 8, nullable: true }) + latitude: number; + + @Column({ name: 'longitude', type: 'decimal', precision: 11, scale: 8, nullable: true }) + longitude: number; + + @Column({ name: 'risk_score', type: 'int', nullable: true }) + riskScore: number; + + @Column({ name: 'risk_factors', type: 'jsonb', default: [] }) + riskFactors: string[]; + + @Index() + @Column({ name: 'is_suspicious', type: 'boolean', default: false }) + isSuspicious: boolean; + + @Column({ name: 'is_new_device', type: 'boolean', default: false }) + isNewDevice: boolean; + + @Column({ name: 'is_new_location', type: 'boolean', default: false }) + isNewLocation: boolean; + + @Column({ name: 'failure_reason', type: 'varchar', length: 100, nullable: true }) + failureReason: string; + + @Column({ name: 'failure_count', type: 'int', nullable: true }) + failureCount: number; + + @Index() + @Column({ name: 'attempted_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) + attemptedAt: Date; +} diff --git a/src/modules/audit/entities/permission-change.entity.ts b/src/modules/audit/entities/permission-change.entity.ts new file mode 100644 index 0000000..6f075d3 --- /dev/null +++ b/src/modules/audit/entities/permission-change.entity.ts @@ -0,0 +1,71 @@ +/** + * PermissionChange Entity + * Access control change auditing + * Compatible with erp-core permission-change.entity + * + * @module Audit + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + Index, +} from 'typeorm'; + +export type PermissionChangeType = 'role_assigned' | 'role_revoked' | 'permission_granted' | 'permission_revoked'; +export type PermissionScope = 'global' | 'tenant' | 'branch'; + +@Entity({ name: 'permission_changes', schema: 'audit' }) +export class PermissionChange { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Column({ name: 'changed_by', type: 'uuid' }) + changedBy: string; + + @Index() + @Column({ name: 'target_user_id', type: 'uuid' }) + targetUserId: string; + + @Column({ name: 'target_user_email', type: 'varchar', length: 255, nullable: true }) + targetUserEmail: string; + + @Column({ name: 'change_type', type: 'varchar', length: 30 }) + changeType: PermissionChangeType; + + @Column({ name: 'role_id', type: 'uuid', nullable: true }) + roleId: string; + + @Column({ name: 'role_code', type: 'varchar', length: 50, nullable: true }) + roleCode: string; + + @Column({ name: 'permission_id', type: 'uuid', nullable: true }) + permissionId: string; + + @Column({ name: 'permission_code', type: 'varchar', length: 100, nullable: true }) + permissionCode: string; + + @Column({ name: 'branch_id', type: 'uuid', nullable: true }) + branchId: string; + + @Column({ name: 'scope', type: 'varchar', length: 30, nullable: true }) + scope: PermissionScope; + + @Column({ name: 'previous_roles', type: 'text', array: true, nullable: true }) + previousRoles: string[]; + + @Column({ name: 'previous_permissions', type: 'text', array: true, nullable: true }) + previousPermissions: string[]; + + @Column({ name: 'reason', type: 'text', nullable: true }) + reason: string; + + @Index() + @Column({ name: 'changed_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) + changedAt: Date; +} diff --git a/src/modules/audit/entities/sensitive-data-access.entity.ts b/src/modules/audit/entities/sensitive-data-access.entity.ts new file mode 100644 index 0000000..5dbba5a --- /dev/null +++ b/src/modules/audit/entities/sensitive-data-access.entity.ts @@ -0,0 +1,70 @@ +/** + * SensitiveDataAccess Entity + * Security/compliance logging for PII, financial, medical and credential access + * Compatible with erp-core sensitive-data-access.entity + * + * @module Audit + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + Index, +} from 'typeorm'; + +export type DataType = 'pii' | 'financial' | 'medical' | 'credentials'; +export type AccessType = 'view' | 'export' | 'modify' | 'decrypt'; + +@Entity({ name: 'sensitive_data_access', schema: 'audit' }) +export class SensitiveDataAccess { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Index() + @Column({ name: 'user_id', type: 'uuid' }) + userId: string; + + @Column({ name: 'session_id', type: 'uuid', nullable: true }) + sessionId: string; + + @Index() + @Column({ name: 'data_type', type: 'varchar', length: 100 }) + dataType: DataType; + + @Column({ name: 'data_category', type: 'varchar', length: 100, nullable: true }) + dataCategory: string; + + @Column({ name: 'entity_type', type: 'varchar', length: 100, nullable: true }) + entityType: string; + + @Column({ name: 'entity_id', type: 'uuid', nullable: true }) + entityId: string; + + @Column({ name: 'access_type', type: 'varchar', length: 30 }) + accessType: AccessType; + + @Column({ name: 'access_reason', type: 'text', nullable: true }) + accessReason: string; + + @Column({ name: 'ip_address', type: 'inet', nullable: true }) + ipAddress: string; + + @Column({ name: 'user_agent', type: 'text', nullable: true }) + userAgent: string; + + @Index() + @Column({ name: 'was_authorized', type: 'boolean', default: true }) + wasAuthorized: boolean; + + @Column({ name: 'denial_reason', type: 'text', nullable: true }) + denialReason: string; + + @Index() + @Column({ name: 'accessed_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) + accessedAt: Date; +} diff --git a/src/modules/billing-usage/entities/billing-alert.entity.ts b/src/modules/billing-usage/entities/billing-alert.entity.ts new file mode 100644 index 0000000..b6afbdc --- /dev/null +++ b/src/modules/billing-usage/entities/billing-alert.entity.ts @@ -0,0 +1,72 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +export type BillingAlertType = + | 'usage_limit' + | 'payment_due' + | 'payment_failed' + | 'trial_ending' + | 'subscription_ending'; + +export type AlertSeverity = 'info' | 'warning' | 'critical'; +export type AlertStatus = 'active' | 'acknowledged' | 'resolved'; + +/** + * Entidad para alertas de facturacion y limites de uso. + * Mapea a billing.billing_alerts (DDL: 05-billing-usage.sql) + */ +@Entity({ name: 'billing_alerts', schema: 'billing' }) +export class BillingAlert { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + // Tipo de alerta + @Index() + @Column({ name: 'alert_type', type: 'varchar', length: 30 }) + alertType: BillingAlertType; + + // Detalles + @Column({ type: 'varchar', length: 200 }) + title: string; + + @Column({ type: 'text', nullable: true }) + message: string; + + @Column({ type: 'varchar', length: 20, default: 'info' }) + severity: AlertSeverity; + + // Estado + @Index() + @Column({ type: 'varchar', length: 20, default: 'active' }) + status: AlertStatus; + + // Notificacion + @Column({ name: 'notified_at', type: 'timestamptz', nullable: true }) + notifiedAt: Date; + + @Column({ name: 'acknowledged_at', type: 'timestamptz', nullable: true }) + acknowledgedAt: Date; + + @Column({ name: 'acknowledged_by', type: 'uuid', nullable: true }) + acknowledgedBy: string; + + // Metadata + @Column({ type: 'jsonb', default: {} }) + metadata: Record; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/billing-usage/entities/coupon-redemption.entity.ts b/src/modules/billing-usage/entities/coupon-redemption.entity.ts new file mode 100644 index 0000000..1395ddf --- /dev/null +++ b/src/modules/billing-usage/entities/coupon-redemption.entity.ts @@ -0,0 +1,44 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + CreateDateColumn, + Unique, +} from 'typeorm'; +import { Coupon } from './coupon.entity'; +import { TenantSubscription } from './tenant-subscription.entity'; + +@Entity({ name: 'coupon_redemptions', schema: 'billing' }) +@Unique(['couponId', 'tenantId']) +export class CouponRedemption { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ name: 'coupon_id', type: 'uuid' }) + couponId!: string; + + @ManyToOne(() => Coupon, (coupon) => coupon.redemptions) + @JoinColumn({ name: 'coupon_id' }) + coupon!: Coupon; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId!: string; + + @Column({ name: 'subscription_id', type: 'uuid', nullable: true }) + subscriptionId?: string; + + @ManyToOne(() => TenantSubscription, { nullable: true }) + @JoinColumn({ name: 'subscription_id' }) + subscription?: TenantSubscription; + + @Column({ name: 'discount_amount', type: 'decimal', precision: 10, scale: 2 }) + discountAmount!: number; + + @CreateDateColumn({ name: 'redeemed_at', type: 'timestamptz' }) + redeemedAt!: Date; + + @Column({ name: 'expires_at', type: 'timestamptz', nullable: true }) + expiresAt?: Date; +} diff --git a/src/modules/billing-usage/entities/coupon.entity.ts b/src/modules/billing-usage/entities/coupon.entity.ts new file mode 100644 index 0000000..b5b371e --- /dev/null +++ b/src/modules/billing-usage/entities/coupon.entity.ts @@ -0,0 +1,72 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + OneToMany, +} from 'typeorm'; +import { CouponRedemption } from './coupon-redemption.entity'; + +export type DiscountType = 'percentage' | 'fixed'; +export type DurationPeriod = 'once' | 'forever' | 'months'; + +@Entity({ name: 'coupons', schema: 'billing' }) +export class Coupon { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ type: 'varchar', length: 50, unique: true }) + code!: string; + + @Column({ type: 'varchar', length: 255 }) + name!: string; + + @Column({ type: 'text', nullable: true }) + description?: string; + + @Column({ name: 'discount_type', type: 'varchar', length: 20 }) + discountType!: DiscountType; + + @Column({ name: 'discount_value', type: 'decimal', precision: 10, scale: 2 }) + discountValue!: number; + + @Column({ type: 'varchar', length: 3, default: 'MXN' }) + currency!: string; + + @Column({ name: 'applicable_plans', type: 'uuid', array: true, default: [] }) + applicablePlans!: string[]; + + @Column({ name: 'min_amount', type: 'decimal', precision: 10, scale: 2, default: 0 }) + minAmount!: number; + + @Column({ name: 'duration_period', type: 'varchar', length: 20, default: 'once' }) + durationPeriod!: DurationPeriod; + + @Column({ name: 'duration_months', type: 'integer', nullable: true }) + durationMonths?: number; + + @Column({ name: 'max_redemptions', type: 'integer', nullable: true }) + maxRedemptions?: number; + + @Column({ name: 'current_redemptions', type: 'integer', default: 0 }) + currentRedemptions!: number; + + @Column({ name: 'valid_from', type: 'timestamptz', nullable: true }) + validFrom?: Date; + + @Column({ name: 'valid_until', type: 'timestamptz', nullable: true }) + validUntil?: Date; + + @Column({ name: 'is_active', type: 'boolean', default: true }) + isActive!: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt!: Date; + + @OneToMany(() => CouponRedemption, (redemption) => redemption.coupon) + redemptions!: CouponRedemption[]; +} diff --git a/src/modules/billing-usage/entities/index.ts b/src/modules/billing-usage/entities/index.ts new file mode 100644 index 0000000..520f364 --- /dev/null +++ b/src/modules/billing-usage/entities/index.ts @@ -0,0 +1,13 @@ +export { SubscriptionPlan, PlanType } from './subscription-plan.entity'; +export { TenantSubscription, BillingCycle, SubscriptionStatus } from './tenant-subscription.entity'; +export { UsageTracking } from './usage-tracking.entity'; +export { UsageEvent, EventCategory } from './usage-event.entity'; +export { Invoice, InvoiceStatus, InvoiceContext, InvoiceType, InvoiceItem } from './invoice.entity'; +export { InvoiceItemType } from './invoice-item.entity'; +export { BillingPaymentMethod, PaymentProvider, PaymentMethodType } from './payment-method.entity'; +export { BillingAlert, BillingAlertType, AlertSeverity, AlertStatus } from './billing-alert.entity'; +export { PlanFeature } from './plan-feature.entity'; +export { PlanLimit, LimitType } from './plan-limit.entity'; +export { Coupon, DiscountType, DurationPeriod } from './coupon.entity'; +export { CouponRedemption } from './coupon-redemption.entity'; +export { StripeEvent } from './stripe-event.entity'; diff --git a/src/modules/billing-usage/entities/invoice-item.entity.ts b/src/modules/billing-usage/entities/invoice-item.entity.ts new file mode 100644 index 0000000..b9abd68 --- /dev/null +++ b/src/modules/billing-usage/entities/invoice-item.entity.ts @@ -0,0 +1,65 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Invoice } from '../../invoices/entities/invoice.entity'; + +export type InvoiceItemType = 'subscription' | 'user' | 'profile' | 'overage' | 'addon'; + +@Entity({ name: 'invoice_items', schema: 'billing' }) +export class InvoiceItem { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'invoice_id', type: 'uuid' }) + invoiceId: string; + + // Descripcion + @Column({ type: 'varchar', length: 500 }) + description: string; + + @Index() + @Column({ name: 'item_type', type: 'varchar', length: 30 }) + itemType: InvoiceItemType; + + // Cantidades + @Column({ type: 'integer', default: 1 }) + quantity: number; + + @Column({ name: 'unit_price', type: 'decimal', precision: 12, scale: 2 }) + unitPrice: number; + + @Column({ type: 'decimal', precision: 12, scale: 2 }) + subtotal: number; + + // Detalles adicionales + @Column({ name: 'profile_code', type: 'varchar', length: 10, nullable: true }) + profileCode: string; + + @Column({ type: 'varchar', length: 20, nullable: true }) + platform: string; + + @Column({ name: 'period_start', type: 'date', nullable: true }) + periodStart: Date; + + @Column({ name: 'period_end', type: 'date', nullable: true }) + periodEnd: Date; + + // Metadata + @Column({ type: 'jsonb', default: {} }) + metadata: Record; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + // Relaciones + @ManyToOne(() => Invoice, (invoice) => invoice.items, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'invoice_id' }) + invoice: Invoice; +} diff --git a/src/modules/billing-usage/entities/invoice.entity.ts b/src/modules/billing-usage/entities/invoice.entity.ts new file mode 100644 index 0000000..4c4c0b7 --- /dev/null +++ b/src/modules/billing-usage/entities/invoice.entity.ts @@ -0,0 +1,17 @@ +/** + * @deprecated Use Invoice from 'modules/invoices/entities' instead. + * + * This entity has been unified with the commercial Invoice entity. + * Both SaaS billing and commercial invoices now use the same table. + * + * Migration guide: + * - Import from: import { Invoice, InvoiceStatus, InvoiceContext } from '../../invoices/entities/invoice.entity'; + * - Set invoiceContext: 'saas' for SaaS billing invoices + * - Use subscriptionId, periodStart, periodEnd for SaaS-specific fields + */ + +// Re-export from unified invoice entity +export { Invoice, InvoiceStatus, InvoiceContext, InvoiceType } from '../../invoices/entities/invoice.entity'; + +// Re-export InvoiceItem as well since it's used together +export { InvoiceItem } from './invoice-item.entity'; diff --git a/src/modules/billing-usage/entities/payment-method.entity.ts b/src/modules/billing-usage/entities/payment-method.entity.ts new file mode 100644 index 0000000..2f2e819 --- /dev/null +++ b/src/modules/billing-usage/entities/payment-method.entity.ts @@ -0,0 +1,85 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + DeleteDateColumn, + Index, +} from 'typeorm'; + +export type PaymentProvider = 'stripe' | 'mercadopago' | 'bank_transfer'; +export type PaymentMethodType = 'card' | 'bank_account' | 'wallet'; + +/** + * Entidad para metodos de pago guardados por tenant. + * Almacena informacion tokenizada/encriptada de metodos de pago. + * Mapea a billing.payment_methods (DDL: 05-billing-usage.sql) + */ +@Entity({ name: 'payment_methods', schema: 'billing' }) +export class BillingPaymentMethod { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + // Proveedor + @Index() + @Column({ type: 'varchar', length: 30 }) + provider: PaymentProvider; + + // Tipo + @Column({ name: 'method_type', type: 'varchar', length: 20 }) + methodType: PaymentMethodType; + + // Datos tokenizados del proveedor + @Column({ name: 'provider_customer_id', type: 'varchar', length: 255, nullable: true }) + providerCustomerId: string; + + @Column({ name: 'provider_method_id', type: 'varchar', length: 255, nullable: true }) + providerMethodId: string; + + // Display info (no sensible) + @Column({ name: 'display_name', type: 'varchar', length: 100, nullable: true }) + displayName: string; + + @Column({ name: 'card_brand', type: 'varchar', length: 20, nullable: true }) + cardBrand: string; + + @Column({ name: 'card_last_four', type: 'varchar', length: 4, nullable: true }) + cardLastFour: string; + + @Column({ name: 'card_exp_month', type: 'integer', nullable: true }) + cardExpMonth: number; + + @Column({ name: 'card_exp_year', type: 'integer', nullable: true }) + cardExpYear: number; + + @Column({ name: 'bank_name', type: 'varchar', length: 100, nullable: true }) + bankName: string; + + @Column({ name: 'bank_last_four', type: 'varchar', length: 4, nullable: true }) + bankLastFour: string; + + // Estado + @Index() + @Column({ name: 'is_default', type: 'boolean', default: false }) + isDefault: boolean; + + @Column({ name: 'is_active', type: 'boolean', default: true }) + isActive: boolean; + + @Column({ name: 'is_verified', type: 'boolean', default: false }) + isVerified: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + @DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true }) + deletedAt: Date; +} diff --git a/src/modules/billing-usage/entities/plan-feature.entity.ts b/src/modules/billing-usage/entities/plan-feature.entity.ts new file mode 100644 index 0000000..2c88839 --- /dev/null +++ b/src/modules/billing-usage/entities/plan-feature.entity.ts @@ -0,0 +1,61 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { SubscriptionPlan } from './subscription-plan.entity'; + +/** + * PlanFeature Entity + * Maps to billing.plan_features DDL table + * Features disponibles por plan de suscripcion + * Propagated from template-saas HU-REFACT-005 + */ +@Entity({ schema: 'billing', name: 'plan_features' }) +@Index('idx_plan_features_plan', ['planId']) +@Index('idx_plan_features_key', ['featureKey']) +export class PlanFeature { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'plan_id' }) + planId: string; + + @Column({ type: 'varchar', length: 100, nullable: false, name: 'feature_key' }) + featureKey: string; + + @Column({ type: 'varchar', length: 255, nullable: false, name: 'feature_name' }) + featureName: string; + + @Column({ type: 'varchar', length: 100, nullable: true }) + category: string | null; + + @Column({ type: 'boolean', default: true }) + enabled: boolean; + + @Column({ type: 'jsonb', default: {} }) + configuration: Record; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ type: 'jsonb', default: {} }) + metadata: Record; + + // Relaciones + @ManyToOne(() => SubscriptionPlan, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'plan_id' }) + plan: SubscriptionPlan; + + // Timestamps + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/billing-usage/entities/plan-limit.entity.ts b/src/modules/billing-usage/entities/plan-limit.entity.ts new file mode 100644 index 0000000..144f909 --- /dev/null +++ b/src/modules/billing-usage/entities/plan-limit.entity.ts @@ -0,0 +1,52 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; +import { SubscriptionPlan } from './subscription-plan.entity'; + +export type LimitType = 'monthly' | 'daily' | 'total' | 'per_user'; + +@Entity({ name: 'plan_limits', schema: 'billing' }) +export class PlanLimit { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ name: 'plan_id', type: 'uuid' }) + planId!: string; + + @ManyToOne(() => SubscriptionPlan, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'plan_id' }) + plan!: SubscriptionPlan; + + @Column({ name: 'limit_key', type: 'varchar', length: 100 }) + limitKey!: string; + + @Column({ name: 'limit_name', type: 'varchar', length: 255 }) + limitName!: string; + + @Column({ name: 'limit_value', type: 'integer' }) + limitValue!: number; + + @Column({ name: 'limit_type', type: 'varchar', length: 50, default: 'monthly' }) + limitType!: LimitType; + + @Column({ name: 'allow_overage', type: 'boolean', default: false }) + allowOverage!: boolean; + + @Column({ name: 'overage_unit_price', type: 'decimal', precision: 10, scale: 4, default: 0 }) + overageUnitPrice!: number; + + @Column({ name: 'overage_currency', type: 'varchar', length: 3, default: 'MXN' }) + overageCurrency!: string; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt!: Date; +} diff --git a/src/modules/billing-usage/entities/stripe-event.entity.ts b/src/modules/billing-usage/entities/stripe-event.entity.ts new file mode 100644 index 0000000..d11eb11 --- /dev/null +++ b/src/modules/billing-usage/entities/stripe-event.entity.ts @@ -0,0 +1,43 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; + +@Entity({ name: 'stripe_events', schema: 'billing' }) +export class StripeEvent { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ name: 'stripe_event_id', type: 'varchar', length: 255, unique: true }) + @Index() + stripeEventId!: string; + + @Column({ name: 'event_type', type: 'varchar', length: 100 }) + @Index() + eventType!: string; + + @Column({ name: 'api_version', type: 'varchar', length: 20, nullable: true }) + apiVersion?: string; + + @Column({ type: 'jsonb' }) + data!: Record; + + @Column({ type: 'boolean', default: false }) + @Index() + processed!: boolean; + + @Column({ name: 'processed_at', type: 'timestamptz', nullable: true }) + processedAt?: Date; + + @Column({ name: 'error_message', type: 'text', nullable: true }) + errorMessage?: string; + + @Column({ name: 'retry_count', type: 'integer', default: 0 }) + retryCount!: number; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt!: Date; +} diff --git a/src/modules/billing-usage/entities/subscription-plan.entity.ts b/src/modules/billing-usage/entities/subscription-plan.entity.ts new file mode 100644 index 0000000..324e7c3 --- /dev/null +++ b/src/modules/billing-usage/entities/subscription-plan.entity.ts @@ -0,0 +1,83 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + DeleteDateColumn, + Index, +} from 'typeorm'; + +export type PlanType = 'saas' | 'on_premise' | 'hybrid'; + +@Entity({ name: 'subscription_plans', schema: 'billing' }) +export class SubscriptionPlan { + @PrimaryGeneratedColumn('uuid') + id: string; + + // Identificacion + @Index({ unique: true }) + @Column({ type: 'varchar', length: 30 }) + code: string; + + @Column({ type: 'varchar', length: 100 }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string; + + // Tipo + @Column({ name: 'plan_type', type: 'varchar', length: 20, default: 'saas' }) + planType: PlanType; + + // Precios base + @Column({ name: 'base_monthly_price', type: 'decimal', precision: 12, scale: 2, default: 0 }) + baseMonthlyPrice: number; + + @Column({ name: 'base_annual_price', type: 'decimal', precision: 12, scale: 2, nullable: true }) + baseAnnualPrice: number; + + @Column({ name: 'setup_fee', type: 'decimal', precision: 12, scale: 2, default: 0 }) + setupFee: number; + + // Limites base + @Column({ name: 'max_users', type: 'integer', default: 5 }) + maxUsers: number; + + @Column({ name: 'max_branches', type: 'integer', default: 1 }) + maxBranches: number; + + @Column({ name: 'storage_gb', type: 'integer', default: 10 }) + storageGb: number; + + @Column({ name: 'api_calls_monthly', type: 'integer', default: 10000 }) + apiCallsMonthly: number; + + // Modulos incluidos + @Column({ name: 'included_modules', type: 'text', array: true, default: [] }) + includedModules: string[]; + + // Plataformas incluidas + @Column({ name: 'included_platforms', type: 'text', array: true, default: ['web'] }) + includedPlatforms: string[]; + + // Features + @Column({ type: 'jsonb', default: {} }) + features: Record; + + // Estado + @Column({ name: 'is_active', type: 'boolean', default: true }) + isActive: boolean; + + @Column({ name: 'is_public', type: 'boolean', default: true }) + isPublic: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + @DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true }) + deletedAt: Date; +} diff --git a/src/modules/billing-usage/entities/tenant-subscription.entity.ts b/src/modules/billing-usage/entities/tenant-subscription.entity.ts new file mode 100644 index 0000000..1973259 --- /dev/null +++ b/src/modules/billing-usage/entities/tenant-subscription.entity.ts @@ -0,0 +1,132 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, + Unique, +} from 'typeorm'; +import { SubscriptionPlan } from './subscription-plan.entity'; + +export type BillingCycle = 'monthly' | 'annual'; +export type SubscriptionStatus = 'trial' | 'active' | 'past_due' | 'cancelled' | 'suspended'; + +@Entity({ name: 'tenant_subscriptions', schema: 'billing' }) +@Unique(['tenantId']) +export class TenantSubscription { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Index() + @Column({ name: 'plan_id', type: 'uuid' }) + planId: string; + + // Periodo + @Column({ name: 'billing_cycle', type: 'varchar', length: 20, default: 'monthly' }) + billingCycle: BillingCycle; + + @Column({ name: 'current_period_start', type: 'timestamptz' }) + currentPeriodStart: Date; + + @Column({ name: 'current_period_end', type: 'timestamptz' }) + currentPeriodEnd: Date; + + // Estado + @Index() + @Column({ type: 'varchar', length: 20, default: 'active' }) + status: SubscriptionStatus; + + // Trial + @Column({ name: 'trial_start', type: 'timestamptz', nullable: true }) + trialStart: Date; + + @Column({ name: 'trial_end', type: 'timestamptz', nullable: true }) + trialEnd: Date; + + // Configuracion de facturacion + @Column({ name: 'billing_email', type: 'varchar', length: 255, nullable: true }) + billingEmail: string; + + @Column({ name: 'billing_name', type: 'varchar', length: 200, nullable: true }) + billingName: string; + + @Column({ name: 'billing_address', type: 'jsonb', default: {} }) + billingAddress: Record; + + @Column({ name: 'tax_id', type: 'varchar', length: 20, nullable: true }) + taxId: string; // RFC para Mexico + + // Metodo de pago + @Column({ name: 'payment_method_id', type: 'uuid', nullable: true }) + paymentMethodId: string; + + @Column({ name: 'payment_provider', type: 'varchar', length: 30, nullable: true }) + paymentProvider: string; // stripe, mercadopago, bank_transfer + + // Stripe integration + @Index() + @Column({ name: 'stripe_customer_id', type: 'varchar', length: 255, nullable: true }) + stripeCustomerId?: string; + + @Index() + @Column({ name: 'stripe_subscription_id', type: 'varchar', length: 255, nullable: true }) + stripeSubscriptionId?: string; + + @Column({ name: 'last_payment_at', type: 'timestamptz', nullable: true }) + lastPaymentAt?: Date; + + @Column({ name: 'last_payment_amount', type: 'decimal', precision: 12, scale: 2, nullable: true }) + lastPaymentAmount?: number; + + // Precios actuales + @Column({ name: 'current_price', type: 'decimal', precision: 12, scale: 2 }) + currentPrice: number; + + @Column({ name: 'discount_percent', type: 'decimal', precision: 5, scale: 2, default: 0 }) + discountPercent: number; + + @Column({ name: 'discount_reason', type: 'varchar', length: 100, nullable: true }) + discountReason: string; + + // Uso contratado + @Column({ name: 'contracted_users', type: 'integer', nullable: true }) + contractedUsers: number; + + @Column({ name: 'contracted_branches', type: 'integer', nullable: true }) + contractedBranches: number; + + // Facturacion automatica + @Column({ name: 'auto_renew', type: 'boolean', default: true }) + autoRenew: boolean; + + @Column({ name: 'next_invoice_date', type: 'date', nullable: true }) + nextInvoiceDate: Date; + + // Cancelacion + @Column({ name: 'cancel_at_period_end', type: 'boolean', default: false }) + cancelAtPeriodEnd: boolean; + + @Column({ name: 'cancelled_at', type: 'timestamptz', nullable: true }) + cancelledAt: Date; + + @Column({ name: 'cancellation_reason', type: 'text', nullable: true }) + cancellationReason: string; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + // Relaciones + @ManyToOne(() => SubscriptionPlan) + @JoinColumn({ name: 'plan_id' }) + plan: SubscriptionPlan; +} diff --git a/src/modules/billing-usage/entities/usage-event.entity.ts b/src/modules/billing-usage/entities/usage-event.entity.ts new file mode 100644 index 0000000..ab29f61 --- /dev/null +++ b/src/modules/billing-usage/entities/usage-event.entity.ts @@ -0,0 +1,73 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; + +export type EventCategory = 'user' | 'api' | 'storage' | 'transaction' | 'mobile'; + +/** + * Entidad para eventos de uso en tiempo real. + * Utilizada para calculo de billing y tracking granular. + * Mapea a billing.usage_events (DDL: 05-billing-usage.sql) + */ +@Entity({ name: 'usage_events', schema: 'billing' }) +export class UsageEvent { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Column({ name: 'user_id', type: 'uuid', nullable: true }) + userId: string; + + @Column({ name: 'device_id', type: 'uuid', nullable: true }) + deviceId: string; + + @Column({ name: 'branch_id', type: 'uuid', nullable: true }) + branchId: string; + + // Evento + @Index() + @Column({ name: 'event_type', type: 'varchar', length: 50 }) + eventType: string; // login, api_call, document_upload, sale, invoice, sync + + @Index() + @Column({ name: 'event_category', type: 'varchar', length: 30 }) + eventCategory: EventCategory; + + // Detalles + @Column({ name: 'profile_code', type: 'varchar', length: 10, nullable: true }) + profileCode: string; + + @Column({ type: 'varchar', length: 20, nullable: true }) + platform: string; + + @Column({ name: 'resource_id', type: 'uuid', nullable: true }) + resourceId: string; + + @Column({ name: 'resource_type', type: 'varchar', length: 50, nullable: true }) + resourceType: string; + + // Metricas + @Column({ type: 'integer', default: 1 }) + quantity: number; + + @Column({ name: 'bytes_used', type: 'bigint', default: 0 }) + bytesUsed: number; + + @Column({ name: 'duration_ms', type: 'integer', nullable: true }) + durationMs: number; + + // Metadata + @Column({ type: 'jsonb', default: {} }) + metadata: Record; + + @Index() + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; +} diff --git a/src/modules/billing-usage/entities/usage-tracking.entity.ts b/src/modules/billing-usage/entities/usage-tracking.entity.ts new file mode 100644 index 0000000..d5ad4b3 --- /dev/null +++ b/src/modules/billing-usage/entities/usage-tracking.entity.ts @@ -0,0 +1,91 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + Unique, +} from 'typeorm'; + +@Entity({ name: 'usage_tracking', schema: 'billing' }) +@Unique(['tenantId', 'periodStart']) +export class UsageTracking { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + // Periodo + @Index() + @Column({ name: 'period_start', type: 'date' }) + periodStart: Date; + + @Column({ name: 'period_end', type: 'date' }) + periodEnd: Date; + + // Usuarios + @Column({ name: 'active_users', type: 'integer', default: 0 }) + activeUsers: number; + + @Column({ name: 'peak_concurrent_users', type: 'integer', default: 0 }) + peakConcurrentUsers: number; + + // Por perfil + @Column({ name: 'users_by_profile', type: 'jsonb', default: {} }) + usersByProfile: Record; // {"ADM": 2, "VNT": 5, "ALM": 3} + + // Por plataforma + @Column({ name: 'users_by_platform', type: 'jsonb', default: {} }) + usersByPlatform: Record; // {"web": 8, "mobile": 5, "desktop": 0} + + // Sucursales + @Column({ name: 'active_branches', type: 'integer', default: 0 }) + activeBranches: number; + + // Storage + @Column({ name: 'storage_used_gb', type: 'decimal', precision: 10, scale: 2, default: 0 }) + storageUsedGb: number; + + @Column({ name: 'documents_count', type: 'integer', default: 0 }) + documentsCount: number; + + // API + @Column({ name: 'api_calls', type: 'integer', default: 0 }) + apiCalls: number; + + @Column({ name: 'api_errors', type: 'integer', default: 0 }) + apiErrors: number; + + // Transacciones + @Column({ name: 'sales_count', type: 'integer', default: 0 }) + salesCount: number; + + @Column({ name: 'sales_amount', type: 'decimal', precision: 14, scale: 2, default: 0 }) + salesAmount: number; + + @Column({ name: 'invoices_generated', type: 'integer', default: 0 }) + invoicesGenerated: number; + + // Mobile + @Column({ name: 'mobile_sessions', type: 'integer', default: 0 }) + mobileSessions: number; + + @Column({ name: 'offline_syncs', type: 'integer', default: 0 }) + offlineSyncs: number; + + @Column({ name: 'payment_transactions', type: 'integer', default: 0 }) + paymentTransactions: number; + + // Calculado + @Column({ name: 'total_billable_amount', type: 'decimal', precision: 12, scale: 2, default: 0 }) + totalBillableAmount: number; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/core/entities/country.entity.ts b/src/modules/core/entities/country.entity.ts new file mode 100644 index 0000000..e3a6384 --- /dev/null +++ b/src/modules/core/entities/country.entity.ts @@ -0,0 +1,35 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; + +@Entity({ schema: 'core', name: 'countries' }) +@Index('idx_countries_code', ['code'], { unique: true }) +export class Country { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 2, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'varchar', length: 10, nullable: true, name: 'phone_code' }) + phoneCode: string | null; + + @Column({ + type: 'varchar', + length: 3, + nullable: true, + name: 'currency_code', + }) + currencyCode: string | null; + + // Audit fields + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; +} diff --git a/src/modules/core/entities/currency-rate.entity.ts b/src/modules/core/entities/currency-rate.entity.ts new file mode 100644 index 0000000..30f4f65 --- /dev/null +++ b/src/modules/core/entities/currency-rate.entity.ts @@ -0,0 +1,55 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Currency } from './currency.entity'; + +export type RateSource = 'manual' | 'banxico' | 'xe' | 'openexchange'; + +@Entity({ schema: 'core', name: 'currency_rates' }) +@Index('idx_currency_rates_tenant', ['tenantId']) +@Index('idx_currency_rates_from', ['fromCurrencyId']) +@Index('idx_currency_rates_to', ['toCurrencyId']) +@Index('idx_currency_rates_date', ['rateDate']) +@Index('idx_currency_rates_lookup', ['fromCurrencyId', 'toCurrencyId', 'rateDate']) +export class CurrencyRate { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', name: 'tenant_id', nullable: true }) + tenantId: string | null; + + @Column({ type: 'uuid', name: 'from_currency_id', nullable: false }) + fromCurrencyId: string; + + @ManyToOne(() => Currency) + @JoinColumn({ name: 'from_currency_id' }) + fromCurrency: Currency; + + @Column({ type: 'uuid', name: 'to_currency_id', nullable: false }) + toCurrencyId: string; + + @ManyToOne(() => Currency) + @JoinColumn({ name: 'to_currency_id' }) + toCurrency: Currency; + + @Column({ type: 'decimal', precision: 18, scale: 8, nullable: false }) + rate: number; + + @Column({ type: 'date', name: 'rate_date', nullable: false }) + rateDate: Date; + + @Column({ type: 'varchar', length: 50, default: 'manual' }) + source: RateSource; + + @Column({ type: 'uuid', name: 'created_by', nullable: true }) + createdBy: string | null; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; +} diff --git a/src/modules/core/entities/currency.entity.ts b/src/modules/core/entities/currency.entity.ts new file mode 100644 index 0000000..f322222 --- /dev/null +++ b/src/modules/core/entities/currency.entity.ts @@ -0,0 +1,43 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; + +@Entity({ schema: 'core', name: 'currencies' }) +@Index('idx_currencies_code', ['code'], { unique: true }) +@Index('idx_currencies_active', ['active']) +export class Currency { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 3, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + name: string; + + @Column({ type: 'varchar', length: 10, nullable: false }) + symbol: string; + + @Column({ type: 'integer', nullable: false, default: 2, name: 'decimals' }) + decimals: number; + + @Column({ + type: 'decimal', + precision: 12, + scale: 6, + nullable: true, + default: 0.01, + }) + rounding: number; + + @Column({ type: 'boolean', nullable: false, default: true }) + active: boolean; + + // Audit fields + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; +} diff --git a/src/modules/core/entities/discount-rule.entity.ts b/src/modules/core/entities/discount-rule.entity.ts new file mode 100644 index 0000000..fb2b36c --- /dev/null +++ b/src/modules/core/entities/discount-rule.entity.ts @@ -0,0 +1,163 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + DeleteDateColumn, + Index, +} from 'typeorm'; + +/** + * Tipo de descuento + */ +export enum DiscountType { + PERCENTAGE = 'percentage', // Porcentaje del total + FIXED = 'fixed', // Monto fijo + PRICE_OVERRIDE = 'price_override', // Precio especial +} + +/** + * Aplicacion del descuento + */ +export enum DiscountAppliesTo { + ALL = 'all', // Todos los productos + CATEGORY = 'category', // Categoria especifica + PRODUCT = 'product', // Producto especifico + CUSTOMER = 'customer', // Cliente especifico + CUSTOMER_GROUP = 'customer_group', // Grupo de clientes +} + +/** + * Condicion de activacion + */ +export enum DiscountCondition { + NONE = 'none', // Sin condicion + MIN_QUANTITY = 'min_quantity', // Cantidad minima + MIN_AMOUNT = 'min_amount', // Monto minimo + DATE_RANGE = 'date_range', // Rango de fechas + FIRST_PURCHASE = 'first_purchase', // Primera compra +} + +/** + * Regla de descuento + */ +@Entity({ schema: 'core', name: 'discount_rules' }) +@Index('idx_discount_rules_tenant_id', ['tenantId']) +@Index('idx_discount_rules_code_tenant', ['tenantId', 'code'], { unique: true }) +@Index('idx_discount_rules_active', ['tenantId', 'isActive']) +@Index('idx_discount_rules_dates', ['tenantId', 'startDate', 'endDate']) +@Index('idx_discount_rules_priority', ['tenantId', 'priority']) +export class DiscountRule { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'uuid', nullable: true, name: 'company_id' }) + companyId: string | null; + + @Column({ type: 'varchar', length: 50, nullable: false }) + code: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ + type: 'enum', + enum: DiscountType, + default: DiscountType.PERCENTAGE, + name: 'discount_type', + }) + discountType: DiscountType; + + @Column({ + type: 'decimal', + precision: 15, + scale: 4, + nullable: false, + name: 'discount_value', + }) + discountValue: number; + + @Column({ + type: 'decimal', + precision: 15, + scale: 2, + nullable: true, + name: 'max_discount_amount', + }) + maxDiscountAmount: number | null; + + @Column({ + type: 'enum', + enum: DiscountAppliesTo, + default: DiscountAppliesTo.ALL, + name: 'applies_to', + }) + appliesTo: DiscountAppliesTo; + + @Column({ type: 'uuid', nullable: true, name: 'applies_to_id' }) + appliesToId: string | null; + + @Column({ + type: 'enum', + enum: DiscountCondition, + default: DiscountCondition.NONE, + name: 'condition_type', + }) + conditionType: DiscountCondition; + + @Column({ + type: 'decimal', + precision: 15, + scale: 4, + nullable: true, + name: 'condition_value', + }) + conditionValue: number | null; + + @Column({ type: 'timestamp', nullable: true, name: 'start_date' }) + startDate: Date | null; + + @Column({ type: 'timestamp', nullable: true, name: 'end_date' }) + endDate: Date | null; + + @Column({ type: 'integer', nullable: false, default: 10 }) + priority: number; + + @Column({ type: 'boolean', nullable: false, default: true, name: 'combinable' }) + combinable: boolean; + + @Column({ type: 'integer', nullable: true, name: 'usage_limit' }) + usageLimit: number | null; + + @Column({ type: 'integer', nullable: false, default: 0, name: 'usage_count' }) + usageCount: number; + + @Column({ type: 'boolean', nullable: false, default: true, name: 'is_active' }) + isActive: boolean; + + // Audit fields + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; + + @Column({ type: 'uuid', nullable: true, name: 'created_by' }) + createdBy: string | null; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', nullable: true }) + updatedAt: Date | null; + + @Column({ type: 'uuid', nullable: true, name: 'updated_by' }) + updatedBy: string | null; + + @DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true }) + deletedAt: Date | null; + + @Column({ type: 'uuid', nullable: true, name: 'deleted_by' }) + deletedBy: string | null; +} diff --git a/src/modules/core/entities/index.ts b/src/modules/core/entities/index.ts new file mode 100644 index 0000000..584325c --- /dev/null +++ b/src/modules/core/entities/index.ts @@ -0,0 +1,19 @@ +/** + * Core Entities Index + */ + +// Existing entities +export { Tenant } from './tenant.entity'; +export { User } from './user.entity'; + +// Catalog entities (propagated from erp-core) +export { Country } from './country.entity'; +export { Currency } from './currency.entity'; +export { CurrencyRate, RateSource } from './currency-rate.entity'; +export { DiscountRule, DiscountType, DiscountAppliesTo, DiscountCondition } from './discount-rule.entity'; +export { PaymentTerm, PaymentTermLine, PaymentTermLineType } from './payment-term.entity'; +export { ProductCategory } from './product-category.entity'; +export { Sequence, ResetPeriod } from './sequence.entity'; +export { State } from './state.entity'; +export { Uom, UomType } from './uom.entity'; +export { UomCategory } from './uom-category.entity'; diff --git a/src/modules/core/entities/payment-term.entity.ts b/src/modules/core/entities/payment-term.entity.ts new file mode 100644 index 0000000..733e3f8 --- /dev/null +++ b/src/modules/core/entities/payment-term.entity.ts @@ -0,0 +1,144 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + DeleteDateColumn, + Index, + OneToMany, +} from 'typeorm'; + +/** + * Tipo de calculo para la linea del termino de pago + */ +export enum PaymentTermLineType { + BALANCE = 'balance', // Saldo restante + PERCENT = 'percent', // Porcentaje del total + FIXED = 'fixed', // Monto fijo +} + +/** + * Linea de termino de pago (para terminos con multiples vencimientos) + */ +@Entity({ schema: 'core', name: 'payment_term_lines' }) +@Index('idx_payment_term_lines_term', ['paymentTermId']) +export class PaymentTermLine { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'payment_term_id' }) + paymentTermId: string; + + @Column({ type: 'integer', nullable: false, default: 1 }) + sequence: number; + + @Column({ + type: 'enum', + enum: PaymentTermLineType, + default: PaymentTermLineType.BALANCE, + name: 'line_type', + }) + lineType: PaymentTermLineType; + + @Column({ + type: 'decimal', + precision: 5, + scale: 2, + nullable: true, + name: 'value_percent', + }) + valuePercent: number | null; + + @Column({ + type: 'decimal', + precision: 15, + scale: 2, + nullable: true, + name: 'value_amount', + }) + valueAmount: number | null; + + @Column({ type: 'integer', nullable: false, default: 0 }) + days: number; + + @Column({ type: 'integer', nullable: true, name: 'day_of_month' }) + dayOfMonth: number | null; + + @Column({ type: 'boolean', nullable: false, default: false, name: 'end_of_month' }) + endOfMonth: boolean; +} + +/** + * Termino de pago (Net 30, 50% advance + 50% on delivery, etc.) + */ +@Entity({ schema: 'core', name: 'payment_terms' }) +@Index('idx_payment_terms_tenant_id', ['tenantId']) +@Index('idx_payment_terms_code_tenant', ['tenantId', 'code'], { unique: true }) +@Index('idx_payment_terms_active', ['tenantId', 'isActive']) +export class PaymentTerm { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'uuid', nullable: true, name: 'company_id' }) + companyId: string | null; + + @Column({ type: 'varchar', length: 50, nullable: false }) + code: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ type: 'integer', nullable: false, default: 0, name: 'due_days' }) + dueDays: number; + + @Column({ + type: 'decimal', + precision: 5, + scale: 2, + nullable: true, + default: 0, + name: 'discount_percent', + }) + discountPercent: number | null; + + @Column({ type: 'integer', nullable: true, default: 0, name: 'discount_days' }) + discountDays: number | null; + + @Column({ type: 'boolean', nullable: false, default: false, name: 'is_immediate' }) + isImmediate: boolean; + + @Column({ type: 'boolean', nullable: false, default: true, name: 'is_active' }) + isActive: boolean; + + @Column({ type: 'integer', nullable: false, default: 0 }) + sequence: number; + + @OneToMany(() => PaymentTermLine, (line) => line.paymentTermId, { eager: true }) + lines: PaymentTermLine[]; + + // Audit fields + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; + + @Column({ type: 'uuid', nullable: true, name: 'created_by' }) + createdBy: string | null; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', nullable: true }) + updatedAt: Date | null; + + @Column({ type: 'uuid', nullable: true, name: 'updated_by' }) + updatedBy: string | null; + + @DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true }) + deletedAt: Date | null; + + @Column({ type: 'uuid', nullable: true, name: 'deleted_by' }) + deletedBy: string | null; +} diff --git a/src/modules/core/entities/product-category.entity.ts b/src/modules/core/entities/product-category.entity.ts new file mode 100644 index 0000000..d9fdd08 --- /dev/null +++ b/src/modules/core/entities/product-category.entity.ts @@ -0,0 +1,79 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, + OneToMany, +} from 'typeorm'; + +@Entity({ schema: 'core', name: 'product_categories' }) +@Index('idx_product_categories_tenant_id', ['tenantId']) +@Index('idx_product_categories_parent_id', ['parentId']) +@Index('idx_product_categories_code_tenant', ['tenantId', 'code'], { + unique: true, +}) +@Index('idx_product_categories_active', ['tenantId', 'active'], { + where: 'deleted_at IS NULL', +}) +export class ProductCategory { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'varchar', length: 50, nullable: true }) + code: string | null; + + @Column({ type: 'uuid', nullable: true, name: 'parent_id' }) + parentId: string | null; + + @Column({ type: 'text', nullable: true, name: 'full_path' }) + fullPath: string | null; + + @Column({ type: 'text', nullable: true }) + notes: string | null; + + @Column({ type: 'boolean', nullable: false, default: true }) + active: boolean; + + // Relations + @ManyToOne(() => ProductCategory, (category) => category.children, { + nullable: true, + }) + @JoinColumn({ name: 'parent_id' }) + parent: ProductCategory | null; + + @OneToMany(() => ProductCategory, (category) => category.parent) + children: ProductCategory[]; + + // Audit fields + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; + + @Column({ type: 'uuid', nullable: true, name: 'created_by' }) + createdBy: string | null; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + }) + updatedAt: Date | null; + + @Column({ type: 'uuid', nullable: true, name: 'updated_by' }) + updatedBy: string | null; + + @Column({ type: 'timestamp', nullable: true, name: 'deleted_at' }) + deletedAt: Date | null; + + @Column({ type: 'uuid', nullable: true, name: 'deleted_by' }) + deletedBy: string | null; +} diff --git a/src/modules/core/entities/sequence.entity.ts b/src/modules/core/entities/sequence.entity.ts new file mode 100644 index 0000000..cc28829 --- /dev/null +++ b/src/modules/core/entities/sequence.entity.ts @@ -0,0 +1,83 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +export enum ResetPeriod { + NONE = 'none', + YEAR = 'year', + MONTH = 'month', +} + +@Entity({ schema: 'core', name: 'sequences' }) +@Index('idx_sequences_tenant_id', ['tenantId']) +@Index('idx_sequences_code_tenant', ['tenantId', 'code'], { unique: true }) +@Index('idx_sequences_active', ['tenantId', 'isActive']) +export class Sequence { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'uuid', nullable: true, name: 'company_id' }) + companyId: string | null; + + @Column({ type: 'varchar', length: 100, nullable: false }) + code: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'varchar', length: 50, nullable: true }) + prefix: string | null; + + @Column({ type: 'varchar', length: 50, nullable: true }) + suffix: string | null; + + @Column({ type: 'integer', nullable: false, default: 1, name: 'next_number' }) + nextNumber: number; + + @Column({ type: 'integer', nullable: false, default: 4 }) + padding: number; + + @Column({ + type: 'enum', + enum: ResetPeriod, + nullable: true, + default: ResetPeriod.NONE, + name: 'reset_period', + }) + resetPeriod: ResetPeriod | null; + + @Column({ + type: 'timestamp', + nullable: true, + name: 'last_reset_date', + }) + lastResetDate: Date | null; + + @Column({ type: 'boolean', nullable: false, default: true, name: 'is_active' }) + isActive: boolean; + + // Audit fields + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; + + @Column({ type: 'uuid', nullable: true, name: 'created_by' }) + createdBy: string | null; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + }) + updatedAt: Date | null; + + @Column({ type: 'uuid', nullable: true, name: 'updated_by' }) + updatedBy: string | null; +} diff --git a/src/modules/core/entities/state.entity.ts b/src/modules/core/entities/state.entity.ts new file mode 100644 index 0000000..a7d36c5 --- /dev/null +++ b/src/modules/core/entities/state.entity.ts @@ -0,0 +1,45 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Country } from './country.entity'; + +@Entity({ schema: 'core', name: 'states' }) +@Index('idx_states_country', ['countryId']) +@Index('idx_states_code', ['code']) +@Index('idx_states_country_code', ['countryId', 'code'], { unique: true }) +export class State { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', name: 'country_id', nullable: false }) + countryId: string; + + @ManyToOne(() => Country, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'country_id' }) + country: Country; + + @Column({ type: 'varchar', length: 10, nullable: false }) + code: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'varchar', length: 50, nullable: true }) + timezone: string | null; + + @Column({ type: 'boolean', name: 'is_active', default: true }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/core/entities/tenant.entity.ts b/src/modules/core/entities/tenant.entity.ts new file mode 100644 index 0000000..ccb8d0e --- /dev/null +++ b/src/modules/core/entities/tenant.entity.ts @@ -0,0 +1,50 @@ +/** + * Tenant Entity + * Entidad para multi-tenancy + * + * @module Core + * @table core.tenants + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + OneToMany, + Index, +} from 'typeorm'; +import { User } from './user.entity'; + +@Entity({ schema: 'auth', name: 'tenants' }) +export class Tenant { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 50, unique: true }) + @Index() + code: string; + + @Column({ type: 'varchar', length: 200 }) + name: string; + + @Column({ name: 'is_active', type: 'boolean', default: true }) + isActive: boolean; + + @Column({ type: 'jsonb', default: {} }) + settings: Record; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) + deletedAt: Date; + + // Relations + @OneToMany(() => User, (user) => user.tenant) + users: User[]; +} diff --git a/src/modules/core/entities/uom-category.entity.ts b/src/modules/core/entities/uom-category.entity.ts new file mode 100644 index 0000000..6b7e95c --- /dev/null +++ b/src/modules/core/entities/uom-category.entity.ts @@ -0,0 +1,45 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + OneToMany, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Uom } from './uom.entity'; +import { Tenant } from './tenant.entity'; + +@Entity({ schema: 'core', name: 'uom_categories' }) +@Index('idx_uom_categories_tenant', ['tenantId']) +@Index('idx_uom_categories_tenant_name', ['tenantId', 'name'], { unique: true }) +export class UomCategory { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + // Relations + @ManyToOne(() => Tenant) + @JoinColumn({ name: 'tenant_id' }) + tenant: Tenant; + + @OneToMany(() => Uom, (uom) => uom.category) + uoms: Uom[]; + + // Audit fields + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/core/entities/uom.entity.ts b/src/modules/core/entities/uom.entity.ts new file mode 100644 index 0000000..070370a --- /dev/null +++ b/src/modules/core/entities/uom.entity.ts @@ -0,0 +1,89 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { UomCategory } from './uom-category.entity'; +import { Tenant } from './tenant.entity'; + +export enum UomType { + REFERENCE = 'reference', + BIGGER = 'bigger', + SMALLER = 'smaller', +} + +@Entity({ schema: 'core', name: 'uom' }) +@Index('idx_uom_tenant', ['tenantId']) +@Index('idx_uom_category_id', ['categoryId']) +@Index('idx_uom_code', ['code']) +@Index('idx_uom_active', ['active']) +@Index('idx_uom_tenant_category_name', ['tenantId', 'categoryId', 'name'], { unique: true }) +export class Uom { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'uuid', nullable: false, name: 'category_id' }) + categoryId: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + name: string; + + @Column({ type: 'varchar', length: 20, nullable: true }) + code: string | null; + + @Column({ + type: 'enum', + enum: UomType, + nullable: false, + default: UomType.REFERENCE, + name: 'uom_type', + }) + uomType: UomType; + + @Column({ + type: 'decimal', + precision: 12, + scale: 6, + nullable: false, + default: 1.0, + }) + factor: number; + + @Column({ + type: 'decimal', + precision: 12, + scale: 6, + nullable: true, + default: 0.01, + }) + rounding: number; + + @Column({ type: 'boolean', nullable: false, default: true }) + active: boolean; + + // Relations + @ManyToOne(() => Tenant) + @JoinColumn({ name: 'tenant_id' }) + tenant: Tenant; + + @ManyToOne(() => UomCategory, (category) => category.uoms, { + nullable: false, + }) + @JoinColumn({ name: 'category_id' }) + category: UomCategory; + + // Audit fields + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/core/entities/user.entity.ts b/src/modules/core/entities/user.entity.ts new file mode 100644 index 0000000..9ebe843 --- /dev/null +++ b/src/modules/core/entities/user.entity.ts @@ -0,0 +1,78 @@ +/** + * User Entity + * Entidad de usuarios del sistema + * + * @module Core + * @table core.users + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, + Index, +} from 'typeorm'; +import { Tenant } from './tenant.entity'; + +@Entity({ schema: 'auth', name: 'users' }) +@Index(['tenantId', 'email'], { unique: true }) +export class User { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid', nullable: true }) + tenantId: string; + + @Column({ type: 'varchar', length: 255 }) + email: string; + + @Column({ type: 'varchar', length: 100, nullable: true }) + username: string; + + @Column({ name: 'password_hash', type: 'varchar', length: 255, select: false }) + passwordHash: string; + + @Column({ name: 'first_name', type: 'varchar', length: 100, nullable: true }) + firstName: string; + + @Column({ name: 'last_name', type: 'varchar', length: 100, nullable: true }) + lastName: string; + + @Column({ type: 'varchar', array: true, default: ['viewer'] }) + roles: string[]; + + @Column({ name: 'is_active', type: 'boolean', default: true }) + isActive: boolean; + + @Column({ name: 'last_login_at', type: 'timestamptz', nullable: true }) + lastLoginAt: Date; + + @Column({ name: 'default_tenant_id', type: 'uuid', nullable: true }) + defaultTenantId: string; + + @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) + deletedAt: Date; + + // Placeholder para relación de roles (se implementará en ST-004) + userRoles?: { role: { code: string } }[]; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + // Relations + @ManyToOne(() => Tenant, (tenant) => tenant.users) + @JoinColumn({ name: 'tenant_id' }) + tenant: Tenant; + + // Computed property + get fullName(): string { + return [this.firstName, this.lastName].filter(Boolean).join(' ') || this.email; + } +} diff --git a/src/modules/invoices/entities/index.ts b/src/modules/invoices/entities/index.ts new file mode 100644 index 0000000..527fe39 --- /dev/null +++ b/src/modules/invoices/entities/index.ts @@ -0,0 +1,8 @@ +/** + * Invoices Entities - Export + */ + +export { Invoice, InvoiceType, InvoiceStatus, InvoiceContext } from './invoice.entity'; +export { InvoiceItem } from './invoice-item.entity'; +export { Payment } from './payment.entity'; +export { PaymentAllocation } from './payment-allocation.entity'; diff --git a/src/modules/invoices/entities/invoice-item.entity.ts b/src/modules/invoices/entities/invoice-item.entity.ts new file mode 100644 index 0000000..3d46a73 --- /dev/null +++ b/src/modules/invoices/entities/invoice-item.entity.ts @@ -0,0 +1,95 @@ +/** + * InvoiceItem Entity + * Line items for unified invoices + * Compatible with erp-core invoice-item.entity + * + * @module Invoices + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Invoice } from './invoice.entity'; + +@Entity({ name: 'invoice_items', schema: 'billing' }) +export class InvoiceItem { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'invoice_id', type: 'uuid' }) + invoiceId: string; + + @ManyToOne(() => Invoice, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'invoice_id' }) + invoice: Invoice; + + @Index() + @Column({ name: 'product_id', type: 'uuid', nullable: true }) + productId?: string; + + @Column({ name: 'line_number', type: 'int', default: 1 }) + lineNumber: number; + + @Column({ name: 'product_sku', type: 'varchar', length: 50, nullable: true }) + productSku?: string; + + @Column({ name: 'product_name', type: 'varchar', length: 200 }) + productName: string; + + @Column({ type: 'text', nullable: true }) + description?: string; + + // SAT (Mexico) + @Column({ name: 'sat_product_code', type: 'varchar', length: 20, nullable: true }) + satProductCode?: string; + + @Column({ name: 'sat_unit_code', type: 'varchar', length: 10, nullable: true }) + satUnitCode?: string; + + @Column({ type: 'decimal', precision: 15, scale: 4, default: 1 }) + quantity: number; + + @Column({ type: 'varchar', length: 20, default: 'PZA' }) + uom: string; + + @Column({ name: 'unit_price', type: 'decimal', precision: 15, scale: 4, default: 0 }) + unitPrice: number; + + @Column({ name: 'discount_percent', type: 'decimal', precision: 5, scale: 2, default: 0 }) + discountPercent: number; + + @Column({ name: 'discount_amount', type: 'decimal', precision: 15, scale: 2, default: 0 }) + discountAmount: number; + + @Column({ name: 'tax_rate', type: 'decimal', precision: 5, scale: 2, default: 16.00 }) + taxRate: number; + + @Column({ name: 'tax_amount', type: 'decimal', precision: 15, scale: 2, default: 0 }) + taxAmount: number; + + @Column({ name: 'withholding_rate', type: 'decimal', precision: 5, scale: 2, default: 0 }) + withholdingRate: number; + + @Column({ name: 'withholding_amount', type: 'decimal', precision: 15, scale: 2, default: 0 }) + withholdingAmount: number; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0 }) + subtotal: number; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0 }) + total: number; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/invoices/entities/invoice.entity.ts b/src/modules/invoices/entities/invoice.entity.ts new file mode 100644 index 0000000..36ad901 --- /dev/null +++ b/src/modules/invoices/entities/invoice.entity.ts @@ -0,0 +1,187 @@ +/** + * Unified Invoice Entity + * Combines commercial and SaaS billing invoices + * Compatible with erp-core invoice.entity + * + * @module Invoices + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + DeleteDateColumn, + Index, + OneToMany, +} from 'typeorm'; +import { InvoiceItem } from './invoice-item.entity'; + +export type InvoiceType = 'sale' | 'purchase' | 'credit_note' | 'debit_note'; +export type InvoiceStatus = 'draft' | 'validated' | 'sent' | 'partial' | 'paid' | 'overdue' | 'void' | 'refunded' | 'cancelled' | 'voided'; +export type InvoiceContext = 'commercial' | 'saas'; + +@Entity({ name: 'invoices', schema: 'billing' }) +export class Invoice { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Index({ unique: true }) + @Column({ name: 'invoice_number', type: 'varchar', length: 30 }) + invoiceNumber: string; + + @Index() + @Column({ name: 'invoice_type', type: 'varchar', length: 20, default: 'sale' }) + invoiceType: InvoiceType; + + @Index() + @Column({ name: 'invoice_context', type: 'varchar', length: 20, default: 'commercial' }) + invoiceContext: InvoiceContext; + + // Commercial fields + @Column({ name: 'sales_order_id', type: 'uuid', nullable: true }) + salesOrderId: string | null; + + @Column({ name: 'purchase_order_id', type: 'uuid', nullable: true }) + purchaseOrderId: string | null; + + @Index() + @Column({ name: 'partner_id', type: 'uuid', nullable: true }) + partnerId: string | null; + + @Column({ name: 'partner_name', type: 'varchar', length: 200, nullable: true }) + partnerName: string | null; + + @Column({ name: 'partner_tax_id', type: 'varchar', length: 50, nullable: true }) + partnerTaxId: string | null; + + // SaaS billing fields + @Index() + @Column({ name: 'subscription_id', type: 'uuid', nullable: true }) + subscriptionId: string | null; + + @Column({ name: 'period_start', type: 'date', nullable: true }) + periodStart: Date | null; + + @Column({ name: 'period_end', type: 'date', nullable: true }) + periodEnd: Date | null; + + // Billing information + @Column({ name: 'billing_name', type: 'varchar', length: 200, nullable: true }) + billingName: string | null; + + @Column({ name: 'billing_email', type: 'varchar', length: 255, nullable: true }) + billingEmail: string | null; + + @Column({ name: 'billing_address', type: 'jsonb', nullable: true }) + billingAddress: Record | null; + + @Column({ name: 'tax_id', type: 'varchar', length: 50, nullable: true }) + taxId: string | null; + + // Dates + @Index() + @Column({ name: 'invoice_date', type: 'date', default: () => 'CURRENT_DATE' }) + invoiceDate: Date; + + @Column({ name: 'due_date', type: 'date', nullable: true }) + dueDate: Date | null; + + @Column({ name: 'payment_date', type: 'date', nullable: true }) + paymentDate: Date | null; + + @Column({ name: 'paid_at', type: 'timestamptz', nullable: true }) + paidAt: Date | null; + + // Amounts + @Column({ type: 'varchar', length: 3, default: 'MXN' }) + currency: string; + + @Column({ name: 'exchange_rate', type: 'decimal', precision: 10, scale: 6, default: 1 }) + exchangeRate: number; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0 }) + subtotal: number; + + @Column({ name: 'tax_amount', type: 'decimal', precision: 15, scale: 2, default: 0 }) + taxAmount: number; + + @Column({ name: 'withholding_tax', type: 'decimal', precision: 15, scale: 2, default: 0 }) + withholdingTax: number; + + @Column({ name: 'discount_amount', type: 'decimal', precision: 15, scale: 2, default: 0 }) + discountAmount: number; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0 }) + total: number; + + @Column({ name: 'amount_paid', type: 'decimal', precision: 15, scale: 2, default: 0 }) + amountPaid: number; + + @Column({ name: 'amount_due', type: 'decimal', precision: 15, scale: 2, insert: false, update: false, nullable: true }) + amountDue: number | null; + + @Column({ name: 'paid_amount', type: 'decimal', precision: 15, scale: 2, default: 0 }) + paidAmount: number; + + // Payment details + @Column({ name: 'payment_term_days', type: 'int', default: 0 }) + paymentTermDays: number; + + @Column({ name: 'payment_method', type: 'varchar', length: 50, nullable: true }) + paymentMethod: string | null; + + @Column({ name: 'payment_reference', type: 'varchar', length: 100, nullable: true }) + paymentReference: string | null; + + // Status + @Index() + @Column({ type: 'varchar', length: 20, default: 'draft' }) + status: InvoiceStatus; + + // CFDI (Mexico) + @Index() + @Column({ name: 'cfdi_uuid', type: 'varchar', length: 40, nullable: true }) + cfdiUuid: string | null; + + @Column({ name: 'cfdi_status', type: 'varchar', length: 20, nullable: true }) + cfdiStatus: string | null; + + @Column({ name: 'cfdi_xml', type: 'text', nullable: true }) + cfdiXml: string | null; + + @Column({ name: 'cfdi_pdf_url', type: 'varchar', length: 500, nullable: true }) + cfdiPdfUrl: string | null; + + // Notes + @Column({ type: 'text', nullable: true }) + notes: string | null; + + @Column({ name: 'internal_notes', type: 'text', nullable: true }) + internalNotes: string | null; + + // Audit + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ name: 'created_by', type: 'uuid', nullable: true }) + createdBy: string | null; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + @Column({ name: 'updated_by', type: 'uuid', nullable: true }) + updatedBy: string | null; + + @DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true }) + deletedAt: Date | null; + + // Relations + @OneToMany(() => InvoiceItem, (item) => item.invoice, { cascade: true }) + items: InvoiceItem[]; +} diff --git a/src/modules/invoices/entities/payment-allocation.entity.ts b/src/modules/invoices/entities/payment-allocation.entity.ts new file mode 100644 index 0000000..66e9001 --- /dev/null +++ b/src/modules/invoices/entities/payment-allocation.entity.ts @@ -0,0 +1,53 @@ +/** + * PaymentAllocation Entity + * Links payments to invoices + * Compatible with erp-core payment-allocation.entity + * + * @module Invoices + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Payment } from './payment.entity'; +import { Invoice } from './invoice.entity'; + +@Entity({ name: 'payment_allocations', schema: 'billing' }) +export class PaymentAllocation { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'payment_id', type: 'uuid' }) + paymentId: string; + + @ManyToOne(() => Payment, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'payment_id' }) + payment: Payment; + + @Index() + @Column({ name: 'invoice_id', type: 'uuid' }) + invoiceId: string; + + @ManyToOne(() => Invoice, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'invoice_id' }) + invoice: Invoice; + + @Column({ type: 'decimal', precision: 15, scale: 2 }) + amount: number; + + @Column({ name: 'allocation_date', type: 'date', default: () => 'CURRENT_DATE' }) + allocationDate: Date; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ name: 'created_by', type: 'uuid', nullable: true }) + createdBy?: string; +} diff --git a/src/modules/invoices/entities/payment.entity.ts b/src/modules/invoices/entities/payment.entity.ts new file mode 100644 index 0000000..1e6416a --- /dev/null +++ b/src/modules/invoices/entities/payment.entity.ts @@ -0,0 +1,89 @@ +/** + * Payment Entity + * Payment received or made + * Compatible with erp-core payment.entity + * + * @module Invoices + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + DeleteDateColumn, + Index, +} from 'typeorm'; + +@Entity({ name: 'payments', schema: 'billing' }) +export class Payment { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Index() + @Column({ name: 'payment_number', type: 'varchar', length: 30 }) + paymentNumber: string; + + @Index() + @Column({ name: 'payment_type', type: 'varchar', length: 20, default: 'received' }) + paymentType: 'received' | 'made'; + + @Index() + @Column({ name: 'partner_id', type: 'uuid' }) + partnerId: string; + + @Column({ name: 'partner_name', type: 'varchar', length: 200, nullable: true }) + partnerName: string; + + @Column({ type: 'varchar', length: 3, default: 'MXN' }) + currency: string; + + @Column({ type: 'decimal', precision: 15, scale: 2 }) + amount: number; + + @Column({ name: 'exchange_rate', type: 'decimal', precision: 10, scale: 6, default: 1 }) + exchangeRate: number; + + @Column({ name: 'payment_date', type: 'date', default: () => 'CURRENT_DATE' }) + paymentDate: Date; + + @Index() + @Column({ name: 'payment_method', type: 'varchar', length: 50 }) + paymentMethod: string; + + @Column({ type: 'varchar', length: 100, nullable: true }) + reference: string; + + @Column({ name: 'bank_account_id', type: 'uuid', nullable: true }) + bankAccountId: string; + + @Index() + @Column({ type: 'varchar', length: 20, default: 'draft' }) + status: 'draft' | 'confirmed' | 'reconciled' | 'cancelled'; + + @Column({ type: 'text', nullable: true }) + notes: string; + + @Column({ name: 'cfdi_uuid', type: 'varchar', length: 40, nullable: true }) + cfdiUuid: string; + + @Column({ name: 'cfdi_status', type: 'varchar', length: 20, nullable: true }) + cfdiStatus: string; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ name: 'created_by', type: 'uuid', nullable: true }) + createdBy?: string; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + @DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true }) + deletedAt: Date; +} diff --git a/src/modules/notifications/entities/channel.entity.ts b/src/modules/notifications/entities/channel.entity.ts new file mode 100644 index 0000000..4baf2b1 --- /dev/null +++ b/src/modules/notifications/entities/channel.entity.ts @@ -0,0 +1,65 @@ +/** + * Channel Entity + * Notification delivery channel configuration + * Compatible with erp-core channel.entity + * + * @module Notifications + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +export type ChannelType = 'email' | 'sms' | 'push' | 'whatsapp' | 'in_app' | 'webhook'; + +@Entity({ name: 'channels', schema: 'notifications' }) +export class Channel { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 30, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 100 }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string; + + @Column({ name: 'channel_type', type: 'varchar', length: 30 }) + channelType: ChannelType; + + @Column({ type: 'varchar', length: 50, nullable: true }) + provider: string; + + @Column({ name: 'provider_config', type: 'jsonb', default: {} }) + providerConfig: Record; + + @Column({ name: 'rate_limit_per_minute', type: 'int', nullable: true }) + rateLimitPerMinute: number; + + @Column({ name: 'rate_limit_per_hour', type: 'int', nullable: true }) + rateLimitPerHour: number; + + @Column({ name: 'rate_limit_per_day', type: 'int', nullable: true }) + rateLimitPerDay: number; + + @Column({ name: 'is_active', type: 'boolean', default: true }) + isActive: boolean; + + @Column({ name: 'is_default', type: 'boolean', default: false }) + isDefault: boolean; + + @Column({ type: 'jsonb', default: {} }) + metadata: Record; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/notifications/entities/in-app-notification.entity.ts b/src/modules/notifications/entities/in-app-notification.entity.ts new file mode 100644 index 0000000..77703fe --- /dev/null +++ b/src/modules/notifications/entities/in-app-notification.entity.ts @@ -0,0 +1,85 @@ +/** + * InAppNotification Entity + * In-app notification with read/archive tracking + * Compatible with erp-core in-app-notification.entity + * + * @module Notifications + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; + +export type InAppCategory = 'info' | 'success' | 'warning' | 'error' | 'task'; +export type InAppPriority = 'low' | 'normal' | 'high' | 'urgent'; +export type InAppActionType = 'link' | 'modal' | 'function'; + +@Entity({ name: 'in_app_notifications', schema: 'notifications' }) +export class InAppNotification { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Index() + @Column({ name: 'user_id', type: 'uuid' }) + userId: string; + + @Column({ type: 'varchar', length: 200 }) + title: string; + + @Column({ type: 'text' }) + message: string; + + @Column({ type: 'varchar', length: 50, nullable: true }) + icon: string; + + @Column({ type: 'varchar', length: 20, nullable: true }) + color: string; + + @Column({ name: 'action_type', type: 'varchar', length: 20, nullable: true }) + actionType: InAppActionType; + + @Column({ name: 'action_url', type: 'text', nullable: true }) + actionUrl: string; + + @Column({ name: 'action_data', type: 'jsonb', nullable: true }) + actionData: Record; + + @Column({ type: 'varchar', length: 30, default: 'info' }) + category: InAppCategory; + + @Column({ name: 'context_type', type: 'varchar', length: 100, nullable: true }) + contextType: string; + + @Column({ name: 'context_id', type: 'uuid', nullable: true }) + contextId: string; + + @Column({ name: 'is_read', type: 'boolean', default: false }) + isRead: boolean; + + @Column({ name: 'read_at', type: 'timestamptz', nullable: true }) + readAt: Date; + + @Column({ name: 'is_archived', type: 'boolean', default: false }) + isArchived: boolean; + + @Column({ name: 'archived_at', type: 'timestamptz', nullable: true }) + archivedAt: Date; + + @Column({ type: 'varchar', length: 20, default: 'normal' }) + priority: InAppPriority; + + @Column({ name: 'expires_at', type: 'timestamptz', nullable: true }) + expiresAt: Date; + + @Index() + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; +} diff --git a/src/modules/notifications/entities/index.ts b/src/modules/notifications/entities/index.ts new file mode 100644 index 0000000..e4621a7 --- /dev/null +++ b/src/modules/notifications/entities/index.ts @@ -0,0 +1,10 @@ +/** + * Notifications Entities - Export + */ + +export { Channel, ChannelType } from './channel.entity'; +export { NotificationTemplate, TemplateTranslation, TemplateCategory } from './template.entity'; +export { NotificationPreference, DigestFrequency } from './preference.entity'; +export { Notification, NotificationStatus, NotificationPriority } from './notification.entity'; +export { NotificationBatch, BatchStatus, AudienceType } from './notification-batch.entity'; +export { InAppNotification, InAppCategory, InAppPriority, InAppActionType } from './in-app-notification.entity'; diff --git a/src/modules/notifications/entities/notification-batch.entity.ts b/src/modules/notifications/entities/notification-batch.entity.ts new file mode 100644 index 0000000..12afda4 --- /dev/null +++ b/src/modules/notifications/entities/notification-batch.entity.ts @@ -0,0 +1,96 @@ +/** + * NotificationBatch Entity + * Batch notification campaigns with audience targeting + * Compatible with erp-core notification-batch.entity + * + * @module Notifications + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { NotificationTemplate } from './template.entity'; +import { ChannelType } from './channel.entity'; + +export type BatchStatus = 'draft' | 'scheduled' | 'processing' | 'completed' | 'failed' | 'cancelled'; +export type AudienceType = 'all_users' | 'segment' | 'custom'; + +@Entity({ name: 'notification_batches', schema: 'notifications' }) +export class NotificationBatch { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Column({ type: 'varchar', length: 200 }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string; + + @Column({ name: 'template_id', type: 'uuid', nullable: true }) + templateId: string; + + @Column({ name: 'channel_type', type: 'varchar', length: 30 }) + channelType: ChannelType; + + @Column({ name: 'audience_type', type: 'varchar', length: 30 }) + audienceType: AudienceType; + + @Column({ name: 'audience_filter', type: 'jsonb', default: {} }) + audienceFilter: Record; + + @Column({ type: 'jsonb', default: {} }) + variables: Record; + + @Index() + @Column({ name: 'scheduled_at', type: 'timestamptz', nullable: true }) + scheduledAt: Date; + + @Index() + @Column({ type: 'varchar', length: 20, default: 'draft' }) + status: BatchStatus; + + @Column({ name: 'total_recipients', type: 'int', default: 0 }) + totalRecipients: number; + + @Column({ name: 'sent_count', type: 'int', default: 0 }) + sentCount: number; + + @Column({ name: 'delivered_count', type: 'int', default: 0 }) + deliveredCount: number; + + @Column({ name: 'failed_count', type: 'int', default: 0 }) + failedCount: number; + + @Column({ name: 'read_count', type: 'int', default: 0 }) + readCount: number; + + @Column({ name: 'started_at', type: 'timestamptz', nullable: true }) + startedAt: Date; + + @Column({ name: 'completed_at', type: 'timestamptz', nullable: true }) + completedAt: Date; + + @ManyToOne(() => NotificationTemplate, { onDelete: 'SET NULL', nullable: true }) + @JoinColumn({ name: 'template_id' }) + template: NotificationTemplate | null; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + @Column({ name: 'created_by', type: 'uuid', nullable: true }) + createdBy: string | null; +} diff --git a/src/modules/notifications/entities/notification.entity.ts b/src/modules/notifications/entities/notification.entity.ts new file mode 100644 index 0000000..63a086e --- /dev/null +++ b/src/modules/notifications/entities/notification.entity.ts @@ -0,0 +1,138 @@ +/** + * Notification Entity + * Individual notification with delivery tracking + * Compatible with erp-core notification.entity + * + * @module Notifications + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { NotificationTemplate } from './template.entity'; +import { Channel, ChannelType } from './channel.entity'; + +export type NotificationStatus = 'pending' | 'queued' | 'sending' | 'sent' | 'delivered' | 'read' | 'failed' | 'cancelled'; +export type NotificationPriority = 'low' | 'normal' | 'high' | 'urgent'; + +@Entity({ name: 'notifications', schema: 'notifications' }) +export class Notification { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Index() + @Column({ name: 'user_id', type: 'uuid', nullable: true }) + userId: string; + + @Column({ name: 'recipient_email', type: 'varchar', length: 255, nullable: true }) + recipientEmail: string; + + @Column({ name: 'recipient_phone', type: 'varchar', length: 20, nullable: true }) + recipientPhone: string; + + @Column({ name: 'recipient_device_id', type: 'varchar', length: 255, nullable: true }) + recipientDeviceId: string; + + @Index() + @Column({ name: 'template_id', type: 'uuid', nullable: true }) + templateId: string; + + @Column({ name: 'template_code', type: 'varchar', length: 100, nullable: true }) + templateCode: string; + + @Column({ name: 'channel_type', type: 'varchar', length: 30 }) + channelType: ChannelType; + + @Index() + @Column({ name: 'channel_id', type: 'uuid', nullable: true }) + channelId: string; + + @Column({ type: 'varchar', length: 500, nullable: true }) + subject: string; + + @Column({ type: 'text', nullable: true }) + body: string; + + @Column({ name: 'body_html', type: 'text', nullable: true }) + bodyHtml: string; + + @Column({ type: 'jsonb', default: {} }) + variables: Record; + + @Column({ name: 'context_type', type: 'varchar', length: 100, nullable: true }) + contextType: string; + + @Column({ name: 'context_id', type: 'uuid', nullable: true }) + contextId: string; + + @Column({ type: 'varchar', length: 20, default: 'normal' }) + priority: NotificationPriority; + + @Index() + @Column({ type: 'varchar', length: 20, default: 'pending' }) + status: NotificationStatus; + + @Column({ name: 'queued_at', type: 'timestamptz', nullable: true }) + queuedAt: Date; + + @Column({ name: 'sent_at', type: 'timestamptz', nullable: true }) + sentAt: Date; + + @Column({ name: 'delivered_at', type: 'timestamptz', nullable: true }) + deliveredAt: Date; + + @Column({ name: 'read_at', type: 'timestamptz', nullable: true }) + readAt: Date; + + @Column({ name: 'failed_at', type: 'timestamptz', nullable: true }) + failedAt: Date; + + @Column({ name: 'error_message', type: 'text', nullable: true }) + errorMessage: string; + + @Column({ name: 'retry_count', type: 'int', default: 0 }) + retryCount: number; + + @Column({ name: 'max_retries', type: 'int', default: 3 }) + maxRetries: number; + + @Column({ name: 'next_retry_at', type: 'timestamptz', nullable: true }) + nextRetryAt: Date; + + @Column({ name: 'provider_message_id', type: 'varchar', length: 255, nullable: true }) + providerMessageId: string; + + @Column({ name: 'provider_response', type: 'jsonb', nullable: true }) + providerResponse: Record; + + @Column({ type: 'jsonb', default: {} }) + metadata: Record; + + @Column({ name: 'expires_at', type: 'timestamptz', nullable: true }) + expiresAt: Date; + + @ManyToOne(() => NotificationTemplate, { onDelete: 'SET NULL', nullable: true }) + @JoinColumn({ name: 'template_id' }) + template: NotificationTemplate | null; + + @ManyToOne(() => Channel, { onDelete: 'SET NULL', nullable: true }) + @JoinColumn({ name: 'channel_id' }) + channel: Channel | null; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/notifications/entities/preference.entity.ts b/src/modules/notifications/entities/preference.entity.ts new file mode 100644 index 0000000..fa129bb --- /dev/null +++ b/src/modules/notifications/entities/preference.entity.ts @@ -0,0 +1,82 @@ +/** + * NotificationPreference Entity + * User notification preferences per tenant + * Compatible with erp-core preference.entity + * + * @module Notifications + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + Unique, +} from 'typeorm'; + +export type DigestFrequency = 'instant' | 'hourly' | 'daily' | 'weekly'; + +@Entity({ name: 'notification_preferences', schema: 'notifications' }) +@Unique(['userId', 'tenantId']) +export class NotificationPreference { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'user_id', type: 'uuid' }) + userId: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Column({ name: 'global_enabled', type: 'boolean', default: true }) + globalEnabled: boolean; + + @Column({ name: 'quiet_hours_start', type: 'time', nullable: true }) + quietHoursStart: string; + + @Column({ name: 'quiet_hours_end', type: 'time', nullable: true }) + quietHoursEnd: string; + + @Column({ type: 'varchar', length: 50, default: 'America/Mexico_City' }) + timezone: string; + + @Column({ name: 'email_enabled', type: 'boolean', default: true }) + emailEnabled: boolean; + + @Column({ name: 'sms_enabled', type: 'boolean', default: false }) + smsEnabled: boolean; + + @Column({ name: 'push_enabled', type: 'boolean', default: true }) + pushEnabled: boolean; + + @Column({ name: 'whatsapp_enabled', type: 'boolean', default: false }) + whatsappEnabled: boolean; + + @Column({ name: 'in_app_enabled', type: 'boolean', default: true }) + inAppEnabled: boolean; + + @Column({ name: 'category_preferences', type: 'jsonb', default: {} }) + categoryPreferences: Record; + + @Column({ name: 'digest_frequency', type: 'varchar', length: 20, default: 'instant' }) + digestFrequency: DigestFrequency; + + @Column({ name: 'digest_day', type: 'int', nullable: true }) + digestDay: number; + + @Column({ name: 'digest_hour', type: 'int', nullable: true }) + digestHour: number; + + @Column({ type: 'jsonb', default: {} }) + metadata: Record; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/notifications/entities/template.entity.ts b/src/modules/notifications/entities/template.entity.ts new file mode 100644 index 0000000..3638f84 --- /dev/null +++ b/src/modules/notifications/entities/template.entity.ts @@ -0,0 +1,126 @@ +/** + * NotificationTemplate + TemplateTranslation Entities + * Template system with i18n support + * Compatible with erp-core template.entity + * + * @module Notifications + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + Unique, + OneToMany, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { ChannelType } from './channel.entity'; + +export type TemplateCategory = 'system' | 'marketing' | 'transactional' | 'alert'; + +@Entity({ name: 'notification_templates', schema: 'notifications' }) +@Unique(['tenantId', 'code', 'channelType']) +export class NotificationTemplate { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'tenant_id', type: 'uuid', nullable: true }) + tenantId: string | null; + + @Index() + @Column({ type: 'varchar', length: 100 }) + code: string; + + @Column({ type: 'varchar', length: 200 }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string; + + @Column({ type: 'varchar', length: 30, nullable: true }) + category: TemplateCategory; + + @Index() + @Column({ name: 'channel_type', type: 'varchar', length: 30 }) + channelType: ChannelType; + + @Column({ type: 'varchar', length: 500, nullable: true }) + subject: string; + + @Column({ name: 'body_template', type: 'text', nullable: true }) + bodyTemplate: string; + + @Column({ name: 'body_html', type: 'text', nullable: true }) + bodyHtml: string; + + @Column({ name: 'available_variables', type: 'jsonb', default: [] }) + availableVariables: string[]; + + @Column({ name: 'default_locale', type: 'varchar', length: 10, default: 'es-MX' }) + defaultLocale: string; + + @Column({ name: 'is_active', type: 'boolean', default: true }) + isActive: boolean; + + @Column({ name: 'is_system', type: 'boolean', default: false }) + isSystem: boolean; + + @Column({ type: 'int', default: 1 }) + version: number; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + @Column({ name: 'created_by', type: 'uuid', nullable: true }) + createdBy: string | null; + + @Column({ name: 'updated_by', type: 'uuid', nullable: true }) + updatedBy: string | null; + + @OneToMany(() => TemplateTranslation, (t) => t.template) + translations: TemplateTranslation[]; +} + +@Entity({ name: 'template_translations', schema: 'notifications' }) +@Unique(['templateId', 'locale']) +export class TemplateTranslation { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ name: 'template_id', type: 'uuid' }) + templateId: string; + + @Column({ type: 'varchar', length: 10 }) + locale: string; + + @Column({ type: 'varchar', length: 500, nullable: true }) + subject: string; + + @Column({ name: 'body_template', type: 'text', nullable: true }) + bodyTemplate: string; + + @Column({ name: 'body_html', type: 'text', nullable: true }) + bodyHtml: string; + + @Column({ name: 'is_active', type: 'boolean', default: true }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + @ManyToOne(() => NotificationTemplate, (t) => t.translations, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'template_id' }) + template: NotificationTemplate; +}