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 <noreply@anthropic.com>
This commit is contained in:
parent
a0ca2bd77a
commit
8c010221fb
116
src/modules/audit/entities/audit-log.entity.ts
Normal file
116
src/modules/audit/entities/audit-log.entity.ts
Normal file
@ -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<string, any>;
|
||||
|
||||
@Column({ name: 'new_values', type: 'jsonb', nullable: true })
|
||||
newValues: Record<string, any>;
|
||||
|
||||
@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<string, any>;
|
||||
|
||||
@Column({ name: 'location', type: 'jsonb', default: {} })
|
||||
location: Record<string, any>;
|
||||
|
||||
@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<string, any>;
|
||||
|
||||
@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<string, any>;
|
||||
|
||||
@Column({ name: 'tags', type: 'text', array: true, default: [] })
|
||||
tags: string[];
|
||||
|
||||
@Index()
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
}
|
||||
55
src/modules/audit/entities/config-change.entity.ts
Normal file
55
src/modules/audit/entities/config-change.entity.ts
Normal file
@ -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<string, any>;
|
||||
|
||||
@Column({ name: 'new_value', type: 'jsonb', nullable: true })
|
||||
newValue: Record<string, any>;
|
||||
|
||||
@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;
|
||||
}
|
||||
88
src/modules/audit/entities/data-export.entity.ts
Normal file
88
src/modules/audit/entities/data-export.entity.ts
Normal file
@ -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<string, any>;
|
||||
|
||||
@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;
|
||||
}
|
||||
63
src/modules/audit/entities/entity-change.entity.ts
Normal file
63
src/modules/audit/entities/entity-change.entity.ts
Normal file
@ -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<string, any>;
|
||||
|
||||
@Column({ name: 'changes', type: 'jsonb', default: [] })
|
||||
changes: Record<string, any>[];
|
||||
|
||||
@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;
|
||||
}
|
||||
11
src/modules/audit/entities/index.ts
Normal file
11
src/modules/audit/entities/index.ts
Normal file
@ -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';
|
||||
114
src/modules/audit/entities/login-history.entity.ts
Normal file
114
src/modules/audit/entities/login-history.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
71
src/modules/audit/entities/permission-change.entity.ts
Normal file
71
src/modules/audit/entities/permission-change.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
70
src/modules/audit/entities/sensitive-data-access.entity.ts
Normal file
70
src/modules/audit/entities/sensitive-data-access.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
72
src/modules/billing-usage/entities/billing-alert.entity.ts
Normal file
72
src/modules/billing-usage/entities/billing-alert.entity.ts
Normal file
@ -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<string, any>;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
72
src/modules/billing-usage/entities/coupon.entity.ts
Normal file
72
src/modules/billing-usage/entities/coupon.entity.ts
Normal file
@ -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[];
|
||||
}
|
||||
13
src/modules/billing-usage/entities/index.ts
Normal file
13
src/modules/billing-usage/entities/index.ts
Normal file
@ -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';
|
||||
65
src/modules/billing-usage/entities/invoice-item.entity.ts
Normal file
65
src/modules/billing-usage/entities/invoice-item.entity.ts
Normal file
@ -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<string, any>;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
// Relaciones
|
||||
@ManyToOne(() => Invoice, (invoice) => invoice.items, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'invoice_id' })
|
||||
invoice: Invoice;
|
||||
}
|
||||
17
src/modules/billing-usage/entities/invoice.entity.ts
Normal file
17
src/modules/billing-usage/entities/invoice.entity.ts
Normal file
@ -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';
|
||||
85
src/modules/billing-usage/entities/payment-method.entity.ts
Normal file
85
src/modules/billing-usage/entities/payment-method.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
61
src/modules/billing-usage/entities/plan-feature.entity.ts
Normal file
61
src/modules/billing-usage/entities/plan-feature.entity.ts
Normal file
@ -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<string, any>;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string | null;
|
||||
|
||||
@Column({ type: 'jsonb', default: {} })
|
||||
metadata: Record<string, any>;
|
||||
|
||||
// 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;
|
||||
}
|
||||
52
src/modules/billing-usage/entities/plan-limit.entity.ts
Normal file
52
src/modules/billing-usage/entities/plan-limit.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
43
src/modules/billing-usage/entities/stripe-event.entity.ts
Normal file
43
src/modules/billing-usage/entities/stripe-event.entity.ts
Normal file
@ -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<string, any>;
|
||||
|
||||
@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;
|
||||
}
|
||||
@ -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<string, boolean>;
|
||||
|
||||
// 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;
|
||||
}
|
||||
132
src/modules/billing-usage/entities/tenant-subscription.entity.ts
Normal file
132
src/modules/billing-usage/entities/tenant-subscription.entity.ts
Normal file
@ -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<string, any>;
|
||||
|
||||
@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;
|
||||
}
|
||||
73
src/modules/billing-usage/entities/usage-event.entity.ts
Normal file
73
src/modules/billing-usage/entities/usage-event.entity.ts
Normal file
@ -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<string, any>;
|
||||
|
||||
@Index()
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
}
|
||||
91
src/modules/billing-usage/entities/usage-tracking.entity.ts
Normal file
91
src/modules/billing-usage/entities/usage-tracking.entity.ts
Normal file
@ -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<string, number>; // {"ADM": 2, "VNT": 5, "ALM": 3}
|
||||
|
||||
// Por plataforma
|
||||
@Column({ name: 'users_by_platform', type: 'jsonb', default: {} })
|
||||
usersByPlatform: Record<string, number>; // {"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;
|
||||
}
|
||||
35
src/modules/core/entities/country.entity.ts
Normal file
35
src/modules/core/entities/country.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
55
src/modules/core/entities/currency-rate.entity.ts
Normal file
55
src/modules/core/entities/currency-rate.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
43
src/modules/core/entities/currency.entity.ts
Normal file
43
src/modules/core/entities/currency.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
163
src/modules/core/entities/discount-rule.entity.ts
Normal file
163
src/modules/core/entities/discount-rule.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
19
src/modules/core/entities/index.ts
Normal file
19
src/modules/core/entities/index.ts
Normal file
@ -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';
|
||||
144
src/modules/core/entities/payment-term.entity.ts
Normal file
144
src/modules/core/entities/payment-term.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
79
src/modules/core/entities/product-category.entity.ts
Normal file
79
src/modules/core/entities/product-category.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
83
src/modules/core/entities/sequence.entity.ts
Normal file
83
src/modules/core/entities/sequence.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
45
src/modules/core/entities/state.entity.ts
Normal file
45
src/modules/core/entities/state.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
50
src/modules/core/entities/tenant.entity.ts
Normal file
50
src/modules/core/entities/tenant.entity.ts
Normal file
@ -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<string, unknown>;
|
||||
|
||||
@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[];
|
||||
}
|
||||
45
src/modules/core/entities/uom-category.entity.ts
Normal file
45
src/modules/core/entities/uom-category.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
89
src/modules/core/entities/uom.entity.ts
Normal file
89
src/modules/core/entities/uom.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
78
src/modules/core/entities/user.entity.ts
Normal file
78
src/modules/core/entities/user.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
8
src/modules/invoices/entities/index.ts
Normal file
8
src/modules/invoices/entities/index.ts
Normal file
@ -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';
|
||||
95
src/modules/invoices/entities/invoice-item.entity.ts
Normal file
95
src/modules/invoices/entities/invoice-item.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
187
src/modules/invoices/entities/invoice.entity.ts
Normal file
187
src/modules/invoices/entities/invoice.entity.ts
Normal file
@ -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<string, any> | 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[];
|
||||
}
|
||||
53
src/modules/invoices/entities/payment-allocation.entity.ts
Normal file
53
src/modules/invoices/entities/payment-allocation.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
89
src/modules/invoices/entities/payment.entity.ts
Normal file
89
src/modules/invoices/entities/payment.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
65
src/modules/notifications/entities/channel.entity.ts
Normal file
65
src/modules/notifications/entities/channel.entity.ts
Normal file
@ -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<string, any>;
|
||||
|
||||
@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<string, any>;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@ -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<string, any>;
|
||||
|
||||
@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;
|
||||
}
|
||||
10
src/modules/notifications/entities/index.ts
Normal file
10
src/modules/notifications/entities/index.ts
Normal file
@ -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';
|
||||
@ -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<string, any>;
|
||||
|
||||
@Column({ type: 'jsonb', default: {} })
|
||||
variables: Record<string, any>;
|
||||
|
||||
@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;
|
||||
}
|
||||
138
src/modules/notifications/entities/notification.entity.ts
Normal file
138
src/modules/notifications/entities/notification.entity.ts
Normal file
@ -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<string, any>;
|
||||
|
||||
@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<string, any>;
|
||||
|
||||
@Column({ type: 'jsonb', default: {} })
|
||||
metadata: Record<string, any>;
|
||||
|
||||
@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;
|
||||
}
|
||||
82
src/modules/notifications/entities/preference.entity.ts
Normal file
82
src/modules/notifications/entities/preference.entity.ts
Normal file
@ -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<string, any>;
|
||||
|
||||
@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<string, any>;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
126
src/modules/notifications/entities/template.entity.ts
Normal file
126
src/modules/notifications/entities/template.entity.ts
Normal file
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user