feat: Propagate entities from erp-construccion

Modules added (entities only):
- biometrics (4 entities)
- feature-flags (3 entities)
- hr (8 entities)
- invoices (4 entities)
- products (7 entities)
- profiles (5 entities)
- projects (1 entity)
- purchase (11 entities)
- reports (13 entities)
- sales (4 entities)
- settings (4 entities)
- storage (7 entities)
- warehouses (3 entities)
- webhooks (6 entities)
- whatsapp (10 entities)

Total: 90 new entity files
Source: erp-construccion (validated, build clean)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-27 09:06:31 -06:00
parent 06d79e1c52
commit ec59053bbe
104 changed files with 8387 additions and 0 deletions

View File

@ -0,0 +1,81 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
ManyToOne,
JoinColumn,
Unique,
} from 'typeorm';
import { Device, BiometricType } from './device.entity';
@Entity({ name: 'biometric_credentials', schema: 'auth' })
@Unique(['deviceId', 'credentialId'])
export class BiometricCredential {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'device_id', type: 'uuid' })
deviceId: string;
@Index()
@Column({ name: 'user_id', type: 'uuid' })
userId: string;
// Tipo de biometrico
@Index()
@Column({ name: 'biometric_type', type: 'varchar', length: 50 })
biometricType: BiometricType;
// Credencial (public key para WebAuthn/FIDO2)
@Column({ name: 'credential_id', type: 'text' })
credentialId: string;
@Column({ name: 'public_key', type: 'text' })
publicKey: string;
@Column({ type: 'varchar', length: 20, default: 'ES256' })
algorithm: string;
// Metadata
@Column({ name: 'credential_name', type: 'varchar', length: 100, nullable: true })
credentialName: string; // "Huella indice derecho", "Face ID iPhone"
@Column({ name: 'is_primary', type: 'boolean', default: false })
isPrimary: boolean;
// Estado
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'last_used_at', type: 'timestamptz', nullable: true })
lastUsedAt: Date;
@Column({ name: 'use_count', type: 'integer', default: 0 })
useCount: number;
// Seguridad
@Column({ name: 'failed_attempts', type: 'integer', default: 0 })
failedAttempts: number;
@Column({ name: 'locked_until', type: 'timestamptz', nullable: true })
lockedUntil: Date;
@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;
// Relaciones
@ManyToOne(() => Device, (device) => device.biometricCredentials, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'device_id' })
device: Device;
}

View File

@ -0,0 +1,50 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
} from 'typeorm';
export type ActivityType = 'login' | 'logout' | 'biometric_auth' | 'location_update' | 'app_open' | 'app_close';
export type ActivityStatus = 'success' | 'failed' | 'blocked';
@Entity({ name: 'device_activity_log', schema: 'auth' })
export class DeviceActivityLog {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'device_id', type: 'uuid' })
deviceId: string;
@Index()
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string;
// Actividad
@Index()
@Column({ name: 'activity_type', type: 'varchar', length: 50 })
activityType: ActivityType;
@Column({ name: 'activity_status', type: 'varchar', length: 20 })
activityStatus: ActivityStatus;
// Detalles
@Column({ type: 'jsonb', default: {} })
details: Record<string, any>;
// Ubicacion
@Column({ name: 'ip_address', type: 'inet', nullable: true })
ipAddress: string;
@Column({ type: 'decimal', precision: 10, scale: 8, nullable: true })
latitude: number;
@Column({ type: 'decimal', precision: 11, scale: 8, nullable: true })
longitude: number;
@Index()
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
}

View File

@ -0,0 +1,84 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Device } from './device.entity';
export type AuthMethod = 'password' | 'biometric' | 'oauth' | 'mfa';
@Entity({ name: 'device_sessions', schema: 'auth' })
export class DeviceSession {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'device_id', type: 'uuid' })
deviceId: string;
@Index()
@Column({ name: 'user_id', type: 'uuid' })
userId: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid', nullable: true })
tenantId: string;
// Tokens
@Index()
@Column({ name: 'access_token_hash', type: 'varchar', length: 255 })
accessTokenHash: string;
@Column({ name: 'refresh_token_hash', type: 'varchar', length: 255, nullable: true })
refreshTokenHash: string;
// Metodo de autenticacion
@Column({ name: 'auth_method', type: 'varchar', length: 50 })
authMethod: AuthMethod;
// Validez
@Column({ name: 'issued_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
issuedAt: Date;
@Index()
@Column({ name: 'expires_at', type: 'timestamptz' })
expiresAt: Date;
@Column({ name: 'refresh_expires_at', type: 'timestamptz', nullable: true })
refreshExpiresAt: Date;
// Estado
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'revoked_at', type: 'timestamptz', nullable: true })
revokedAt: Date;
@Column({ name: 'revoked_reason', type: 'varchar', length: 100, nullable: true })
revokedReason: string;
// Ubicacion
@Column({ name: 'ip_address', type: 'inet', nullable: true })
ipAddress: string;
@Column({ name: 'user_agent', type: 'text', nullable: true })
userAgent: string;
@Column({ type: 'decimal', precision: 10, scale: 8, nullable: true })
latitude: number;
@Column({ type: 'decimal', precision: 11, scale: 8, nullable: true })
longitude: number;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
// Relaciones
@ManyToOne(() => Device, (device) => device.sessions, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'device_id' })
device: Device;
}

View File

@ -0,0 +1,121 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
OneToMany,
Unique,
} from 'typeorm';
import { BiometricCredential } from './biometric-credential.entity';
import { DeviceSession } from './device-session.entity';
export type DevicePlatform = 'ios' | 'android' | 'web' | 'desktop';
export type BiometricType = 'fingerprint' | 'face_id' | 'face_recognition' | 'iris';
@Entity({ name: 'devices', schema: 'auth' })
@Unique(['userId', 'deviceUuid'])
export class Device {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'user_id', type: 'uuid' })
userId: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid', nullable: true })
tenantId: string;
// Identificacion del dispositivo
@Index()
@Column({ name: 'device_uuid', type: 'varchar', length: 100 })
deviceUuid: string;
@Column({ name: 'device_name', type: 'varchar', length: 100, nullable: true })
deviceName: string;
@Column({ name: 'device_model', type: 'varchar', length: 100, nullable: true })
deviceModel: string;
@Column({ name: 'device_brand', type: 'varchar', length: 50, nullable: true })
deviceBrand: string;
// Plataforma
@Index()
@Column({ type: 'varchar', length: 20 })
platform: DevicePlatform;
@Column({ name: 'platform_version', type: 'varchar', length: 20, nullable: true })
platformVersion: string;
@Column({ name: 'app_version', type: 'varchar', length: 20, nullable: true })
appVersion: string;
// Estado
@Index()
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'is_trusted', type: 'boolean', default: false })
isTrusted: boolean;
@Column({ name: 'trust_level', type: 'integer', default: 0 })
trustLevel: number; // 0=none, 1=low, 2=medium, 3=high
// Biometricos habilitados
@Column({ name: 'biometric_enabled', type: 'boolean', default: false })
biometricEnabled: boolean;
@Column({ name: 'biometric_type', type: 'varchar', length: 50, nullable: true })
biometricType: BiometricType;
// Push notifications
@Column({ name: 'push_token', type: 'text', nullable: true })
pushToken: string;
@Column({ name: 'push_token_updated_at', type: 'timestamptz', nullable: true })
pushTokenUpdatedAt: Date;
// Ubicacion ultima conocida
@Column({ name: 'last_latitude', type: 'decimal', precision: 10, scale: 8, nullable: true })
lastLatitude: number;
@Column({ name: 'last_longitude', type: 'decimal', precision: 11, scale: 8, nullable: true })
lastLongitude: number;
@Column({ name: 'last_location_at', type: 'timestamptz', nullable: true })
lastLocationAt: Date;
// Seguridad
@Column({ name: 'last_ip_address', type: 'inet', nullable: true })
lastIpAddress: string;
@Column({ name: 'last_user_agent', type: 'text', nullable: true })
lastUserAgent: string;
// Registro
@Column({ name: 'first_seen_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
firstSeenAt: Date;
@Column({ name: 'last_seen_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
lastSeenAt: Date;
@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;
// Relaciones
@OneToMany(() => BiometricCredential, (credential) => credential.device)
biometricCredentials: BiometricCredential[];
@OneToMany(() => DeviceSession, (session) => session.device)
sessions: DeviceSession[];
}

View File

@ -0,0 +1,4 @@
export { Device, DevicePlatform, BiometricType } from './device.entity';
export { BiometricCredential } from './biometric-credential.entity';
export { DeviceSession, AuthMethod } from './device-session.entity';
export { DeviceActivityLog, ActivityType, ActivityStatus } from './device-activity-log.entity';

View File

@ -0,0 +1,54 @@
/**
* FlagEvaluation Entity
* Feature flag evaluation history for analytics
* Compatible with erp-core flag-evaluation.entity
*
* @module FeatureFlags
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Flag } from './flag.entity';
@Entity({ schema: 'feature_flags', name: 'flag_evaluations' })
@Index('idx_flag_evaluations_flag', ['flagId'])
@Index('idx_flag_evaluations_tenant', ['tenantId'])
@Index('idx_flag_evaluations_date', ['evaluatedAt'])
export class FlagEvaluation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', nullable: false, name: 'flag_id' })
flagId: string;
@Column({ type: 'uuid', nullable: false, name: 'tenant_id' })
tenantId: string;
@Column({ type: 'uuid', nullable: true, name: 'user_id' })
userId: string | null;
@Column({ type: 'boolean', nullable: false })
result: boolean;
@Column({ type: 'varchar', length: 100, nullable: true })
variant: string | null;
@Column({ type: 'jsonb', default: {}, name: 'evaluation_context' })
evaluationContext: Record<string, any>;
@Column({ type: 'varchar', length: 100, nullable: true, name: 'evaluation_reason' })
evaluationReason: string | null;
@Column({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP', name: 'evaluated_at' })
evaluatedAt: Date;
@ManyToOne(() => Flag, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'flag_id' })
flag: Flag;
}

View File

@ -0,0 +1,65 @@
/**
* Flag Entity
* Feature flag definition with rollout control
* Compatible with erp-core flag.entity
*
* @module FeatureFlags
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
Unique,
OneToMany,
} from 'typeorm';
import { TenantOverride } from './tenant-override.entity';
@Entity({ name: 'flags', schema: 'feature_flags' })
@Unique(['code'])
export class Flag {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'code', type: 'varchar', length: 50 })
code: string;
@Column({ name: 'name', type: 'varchar', length: 100 })
name: string;
@Column({ name: 'description', type: 'text', nullable: true })
description: string;
@Index()
@Column({ name: 'enabled', type: 'boolean', default: false })
enabled: boolean;
@Column({ name: 'rollout_percentage', type: 'int', default: 100 })
rolloutPercentage: number;
@Column({ name: 'tags', type: 'text', array: true, nullable: true })
tags: string[];
@Index()
@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;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdBy: string;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedBy: string;
@OneToMany(() => TenantOverride, (override) => override.flag)
overrides: TenantOverride[];
}

View File

@ -0,0 +1,7 @@
/**
* Feature Flags Entities - Export
*/
export { Flag } from './flag.entity';
export { TenantOverride } from './tenant-override.entity';
export { FlagEvaluation } from './flag-evaluation.entity';

View File

@ -0,0 +1,58 @@
/**
* TenantOverride Entity
* Per-tenant feature flag override
* Compatible with erp-core tenant-override.entity
*
* @module FeatureFlags
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
Unique,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Flag } from './flag.entity';
@Entity({ name: 'tenant_overrides', schema: 'feature_flags' })
@Unique(['flagId', 'tenantId'])
export class TenantOverride {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'flag_id', type: 'uuid' })
flagId: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'enabled', type: 'boolean' })
enabled: boolean;
@Column({ name: 'reason', type: 'text', nullable: true })
reason: string;
@Index()
@Column({ name: 'expires_at', type: 'timestamptz', nullable: true })
expiresAt: Date;
@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;
@ManyToOne(() => Flag, (flag) => flag.overrides, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'flag_id' })
flag: Flag;
}

View File

@ -0,0 +1,161 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Department } from './department.entity';
/**
* Contract Type Enum
*/
export type ContractType = 'permanent' | 'temporary' | 'contractor' | 'internship' | 'part_time';
/**
* Contract Status Enum
*/
export type ContractStatus = 'draft' | 'active' | 'expired' | 'terminated' | 'cancelled';
/**
* Wage Type for payment frequency
*/
export type WageType = 'hourly' | 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'annual';
/**
* Contract Entity (schema: hr.contracts)
*
* Employment contracts with details about compensation, duration,
* and terms of employment. Tracks contract lifecycle from draft to termination.
*
* RLS Policy: tenant_id = current_setting('app.current_tenant_id')
*/
@Entity({ name: 'contracts', schema: 'hr' })
export class Contract {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'company_id', type: 'uuid' })
companyId: string;
@Index()
@Column({ name: 'employee_id', type: 'uuid' })
employeeId: string;
// Note: ManyToOne to Employee removed - construction Employee
// has a different structure and does not have contracts back-reference.
@Column({ name: 'name', type: 'varchar', length: 255 })
name: string;
@Column({ name: 'reference', type: 'varchar', length: 50, nullable: true })
reference: string | null;
@Index()
@Column({
name: 'contract_type',
type: 'enum',
enum: ['permanent', 'temporary', 'contractor', 'internship', 'part_time'],
enumName: 'hr_contract_type',
default: 'permanent',
})
contractType: ContractType;
@Index()
@Column({
name: 'status',
type: 'enum',
enum: ['draft', 'active', 'expired', 'terminated', 'cancelled'],
enumName: 'hr_contract_status',
default: 'draft',
})
status: ContractStatus;
@Index()
@Column({ name: 'date_start', type: 'date' })
dateStart: Date;
@Column({ name: 'date_end', type: 'date', nullable: true })
dateEnd: Date | null;
@Column({ name: 'job_position_id', type: 'uuid', nullable: true })
jobPositionId: string | null;
// Note: ManyToOne to JobPosition removed - construction uses Puesto entity instead.
@Column({ name: 'department_id', type: 'uuid', nullable: true })
departmentId: string | null;
@ManyToOne(() => Department, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'department_id' })
department: Department | null;
// Compensation
@Column({ name: 'wage', type: 'decimal', precision: 15, scale: 2 })
wage: number;
@Column({
name: 'wage_type',
type: 'varchar',
length: 20,
default: 'monthly',
})
wageType: WageType;
@Column({ name: 'currency', type: 'varchar', length: 3, default: 'MXN' })
currency: string;
// Schedule
@Column({
name: 'hours_per_week',
type: 'decimal',
precision: 5,
scale: 2,
nullable: true,
default: 48,
})
hoursPerWeek: number | null;
@Column({ name: 'schedule_type', type: 'varchar', length: 50, nullable: true })
scheduleType: string | null;
// Trial period
@Column({ name: 'trial_period_months', type: 'integer', nullable: true, default: 0 })
trialPeriodMonths: number | null;
@Column({ name: 'trial_date_end', type: 'date', nullable: true })
trialDateEnd: Date | null;
// Metadata
@Column({ name: 'notes', type: 'text', nullable: true })
notes: string | null;
@Column({ name: 'document_url', type: 'text', nullable: true })
documentUrl: string | null;
// Audit
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'activated_at', type: 'timestamptz', nullable: true })
activatedAt: Date | null;
@Column({ name: 'terminated_at', type: 'timestamptz', nullable: true })
terminatedAt: Date | null;
@Column({ name: 'terminated_by', type: 'uuid', nullable: true })
terminatedBy: string | null;
@Column({ name: 'termination_reason', type: 'text', nullable: true })
terminationReason: string | null;
}

View File

@ -0,0 +1,75 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm';
/**
* Department Entity (schema: hr.departments)
*
* Organizational departments with self-referential hierarchy.
* Supports parent/child relationships for department structure.
*
* RLS Policy: tenant_id = current_setting('app.current_tenant_id')
*/
@Entity({ name: 'departments', schema: 'hr' })
@Index(['tenantId', 'companyId', 'code'], { unique: true })
export class Department {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'company_id', type: 'uuid' })
companyId: string;
@Column({ name: 'name', type: 'varchar', length: 255 })
name: string;
@Column({ name: 'code', type: 'varchar', length: 50, nullable: true })
code: string | null;
@Index()
@Column({ name: 'parent_id', type: 'uuid', nullable: true })
parentId: string | null;
@ManyToOne(() => Department, (department) => department.children, {
onDelete: 'SET NULL',
})
@JoinColumn({ name: 'parent_id' })
parent: Department | null;
@OneToMany(() => Department, (department) => department.parent)
children: Department[];
@Column({ name: 'manager_id', type: 'uuid', nullable: true })
managerId: string | null;
// Note: manager ManyToOne to Employee removed - construction Employee
// has a different structure and does not map this back-reference.
@Index()
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'description', type: 'text', nullable: true })
description: string | null;
@Column({ name: 'color', type: 'varchar', length: 7, nullable: true })
color: string | null;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,65 @@
/**
* EmployeeFraccionamiento Entity
* Asignación de empleados a obras/fraccionamientos
*
* @module HR
* @table hr.employee_fraccionamientos
* @ddl schemas/02-hr-schema-ddl.sql
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
JoinColumn,
Index,
} from 'typeorm';
import { Tenant } from '../../core/entities/tenant.entity';
import { Employee } from './employee.entity';
import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity';
@Entity({ schema: 'hr', name: 'employee_fraccionamientos' })
@Index(['employeeId', 'fraccionamientoId', 'fechaInicio'], { unique: true })
export class EmployeeFraccionamiento {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'employee_id', type: 'uuid' })
employeeId: string;
@Column({ name: 'fraccionamiento_id', type: 'uuid' })
fraccionamientoId: string;
@Column({ name: 'fecha_inicio', type: 'date' })
fechaInicio: Date;
@Column({ name: 'fecha_fin', type: 'date', nullable: true })
fechaFin: Date;
@Column({ type: 'varchar', length: 50, nullable: true })
rol: string;
@Column({ type: 'boolean', default: true })
activo: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@ManyToOne(() => Employee, (e) => e.asignaciones)
@JoinColumn({ name: 'employee_id' })
employee: Employee;
@ManyToOne(() => Fraccionamiento)
@JoinColumn({ name: 'fraccionamiento_id' })
fraccionamiento: Fraccionamiento;
}

View File

@ -0,0 +1,136 @@
/**
* Employee Entity
* Empleados de la empresa
*
* @module HR
* @table hr.employees
* @ddl schemas/02-hr-schema-ddl.sql
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany,
JoinColumn,
Index,
} from 'typeorm';
import { Tenant } from '../../core/entities/tenant.entity';
import { User } from '../../core/entities/user.entity';
import { Puesto } from './puesto.entity';
import { EmployeeFraccionamiento } from './employee-fraccionamiento.entity';
export type EstadoEmpleado = 'activo' | 'inactivo' | 'baja';
export type Genero = 'M' | 'F';
@Entity({ schema: 'hr', name: 'employees' })
@Index(['tenantId', 'codigo'], { unique: true })
@Index(['tenantId', 'curp'], { unique: true })
export class Employee {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ type: 'varchar', length: 20 })
codigo: string;
@Column({ type: 'varchar', length: 100 })
nombre: string;
@Column({ name: 'apellido_paterno', type: 'varchar', length: 100 })
apellidoPaterno: string;
@Column({ name: 'apellido_materno', type: 'varchar', length: 100, nullable: true })
apellidoMaterno: string;
@Column({ type: 'varchar', length: 18, nullable: true })
curp: string;
@Column({ type: 'varchar', length: 13, nullable: true })
rfc: string;
@Column({ type: 'varchar', length: 11, nullable: true })
nss: string;
@Column({ name: 'fecha_nacimiento', type: 'date', nullable: true })
fechaNacimiento: Date;
@Column({ type: 'varchar', length: 1, nullable: true })
genero: Genero;
@Column({ type: 'varchar', length: 255, nullable: true })
email: string;
@Column({ type: 'varchar', length: 20, nullable: true })
telefono: string;
@Column({ type: 'text', nullable: true })
direccion: string;
@Column({ name: 'fecha_ingreso', type: 'date' })
fechaIngreso: Date;
@Column({ name: 'fecha_baja', type: 'date', nullable: true })
fechaBaja: Date;
@Column({ name: 'puesto_id', type: 'uuid', nullable: true })
puestoId: string;
@Column({ type: 'varchar', length: 100, nullable: true })
departamento: string;
@Column({ name: 'tipo_contrato', type: 'varchar', length: 50, nullable: true })
tipoContrato: string;
@Column({
name: 'salario_diario',
type: 'decimal',
precision: 10,
scale: 2,
nullable: true
})
salarioDiario: number;
@Column({ type: 'varchar', length: 20, default: 'activo' })
estado: EstadoEmpleado;
@Column({ name: 'foto_url', type: 'varchar', length: 500, nullable: true })
fotoUrl: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdById: string;
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@ManyToOne(() => Puesto, (p) => p.empleados)
@JoinColumn({ name: 'puesto_id' })
puesto: Puesto;
@ManyToOne(() => User)
@JoinColumn({ name: 'created_by' })
createdBy: User;
@OneToMany(() => EmployeeFraccionamiento, (ef) => ef.employee)
asignaciones: EmployeeFraccionamiento[];
// Computed property
get nombreCompleto(): string {
return [this.nombre, this.apellidoPaterno, this.apellidoMaterno]
.filter(Boolean)
.join(' ');
}
}

View File

@ -0,0 +1,16 @@
/**
* HR Entities Index
* @module HR
*/
// Existing construction-specific entities
export * from './puesto.entity';
export * from './employee.entity';
export * from './employee-fraccionamiento.entity';
// Entities propagated from erp-core
export { Department } from './department.entity';
export { Contract, ContractType, ContractStatus, WageType } from './contract.entity';
export { LeaveType, LeaveTypeCategory, AllocationType } from './leave-type.entity';
export { LeaveAllocation } from './leave-allocation.entity';
export { Leave, LeaveStatus, HalfDayType } from './leave.entity';

View File

@ -0,0 +1,85 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { LeaveType } from './leave-type.entity';
/**
* Leave Allocation Entity (schema: hr.leave_allocations)
*
* Tracks allocated leave days per employee and leave type.
* Supports period-based allocations with used/remaining tracking.
*
* Note: days_remaining is a computed column in PostgreSQL
*
* RLS Policy: tenant_id = current_setting('app.current_tenant_id')
*/
@Entity({ name: 'leave_allocations', schema: 'hr' })
export class LeaveAllocation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'employee_id', type: 'uuid' })
employeeId: string;
// Note: ManyToOne to Employee removed - construction Employee
// has a different structure and does not have leaveAllocations back-reference.
@Index()
@Column({ name: 'leave_type_id', type: 'uuid' })
leaveTypeId: string;
@ManyToOne(() => LeaveType, (leaveType) => leaveType.allocations, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'leave_type_id' })
leaveType: LeaveType;
@Column({ name: 'days_allocated', type: 'decimal', precision: 5, scale: 2 })
daysAllocated: number;
@Column({ name: 'days_used', type: 'decimal', precision: 5, scale: 2, default: 0 })
daysUsed: number;
/**
* Generated column in PostgreSQL: days_allocated - days_used
* Mark as insert: false, update: false since it's computed by DB
*/
@Column({
name: 'days_remaining',
type: 'decimal',
precision: 5,
scale: 2,
insert: false,
update: false,
nullable: true,
})
daysRemaining: number;
@Index()
@Column({ name: 'date_from', type: 'date' })
dateFrom: Date;
@Column({ name: 'date_to', type: 'date' })
dateTo: Date;
@Column({ name: 'notes', type: 'text', nullable: true })
notes: string | null;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,131 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
OneToMany,
} from 'typeorm';
import { Leave } from './leave.entity';
import { LeaveAllocation } from './leave-allocation.entity';
/**
* Leave Type Category Enum
*/
export type LeaveTypeCategory =
| 'vacation'
| 'sick'
| 'personal'
| 'maternity'
| 'paternity'
| 'bereavement'
| 'unpaid'
| 'other';
/**
* Allocation Type Enum
*/
export type AllocationType = 'fixed' | 'accrual' | 'unlimited';
/**
* Leave Type Entity (schema: hr.leave_types)
*
* Configurable leave/absence types for the organization.
* Defines rules for approval, allocation, and payment.
*
* Examples: Vacation, Sick Leave, Maternity, etc.
*
* RLS Policy: tenant_id = current_setting('app.current_tenant_id')
*/
@Entity({ name: 'leave_types', schema: 'hr' })
@Index(['tenantId', 'companyId', 'code'], { unique: true })
export class LeaveType {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'company_id', type: 'uuid' })
companyId: string;
@Column({ name: 'name', type: 'varchar', length: 255 })
name: string;
@Column({ name: 'code', type: 'varchar', length: 50 })
code: string;
@Column({ name: 'description', type: 'text', nullable: true })
description: string | null;
@Column({ name: 'color', type: 'varchar', length: 7, default: '#3B82F6' })
color: string;
@Index()
@Column({
name: 'leave_category',
type: 'enum',
enum: ['vacation', 'sick', 'personal', 'maternity', 'paternity', 'bereavement', 'unpaid', 'other'],
enumName: 'hr_leave_type_category',
default: 'other',
})
leaveCategory: LeaveTypeCategory;
@Column({
name: 'allocation_type',
type: 'enum',
enum: ['fixed', 'accrual', 'unlimited'],
enumName: 'hr_allocation_type',
default: 'fixed',
})
allocationType: AllocationType;
@Column({ name: 'requires_approval', type: 'boolean', default: true })
requiresApproval: boolean;
@Column({ name: 'requires_document', type: 'boolean', default: false })
requiresDocument: boolean;
// Limits
@Column({ name: 'max_days_per_request', type: 'integer', nullable: true })
maxDaysPerRequest: number | null;
@Column({ name: 'max_days_per_year', type: 'integer', nullable: true })
maxDaysPerYear: number | null;
@Column({ name: 'min_days_notice', type: 'integer', default: 0 })
minDaysNotice: number;
// Payment
@Column({ name: 'is_paid', type: 'boolean', default: true })
isPaid: boolean;
@Column({
name: 'pay_percentage',
type: 'decimal',
precision: 5,
scale: 2,
default: 100,
})
payPercentage: number;
@Index()
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
// Relations
@OneToMany(() => Leave, (leave) => leave.leaveType)
leaves: Leave[];
@OneToMany(() => LeaveAllocation, (allocation) => allocation.leaveType)
allocations: LeaveAllocation[];
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,138 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { LeaveType } from './leave-type.entity';
import { LeaveAllocation } from './leave-allocation.entity';
/**
* Leave Status Enum
*/
export type LeaveStatus = 'draft' | 'submitted' | 'approved' | 'rejected' | 'cancelled';
/**
* Half Day Type
*/
export type HalfDayType = 'morning' | 'afternoon';
/**
* Leave Entity (schema: hr.leaves)
*
* Leave/absence requests from employees.
* Tracks the full lifecycle from draft to approval/rejection.
*
* RLS Policy: tenant_id = current_setting('app.current_tenant_id')
*/
@Entity({ name: 'leaves', schema: 'hr' })
export class Leave {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'company_id', type: 'uuid' })
companyId: string;
@Index()
@Column({ name: 'employee_id', type: 'uuid' })
employeeId: string;
// Note: ManyToOne to Employee removed - construction Employee
// has a different structure and does not have leaves back-reference.
@Index()
@Column({ name: 'leave_type_id', type: 'uuid' })
leaveTypeId: string;
@ManyToOne(() => LeaveType, (leaveType) => leaveType.leaves, {
onDelete: 'RESTRICT',
})
@JoinColumn({ name: 'leave_type_id' })
leaveType: LeaveType;
@Column({ name: 'allocation_id', type: 'uuid', nullable: true })
allocationId: string | null;
@ManyToOne(() => LeaveAllocation, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'allocation_id' })
allocation: LeaveAllocation | null;
// Period
@Index()
@Column({ name: 'date_from', type: 'date' })
dateFrom: Date;
@Column({ name: 'date_to', type: 'date' })
dateTo: Date;
@Column({ name: 'days_requested', type: 'decimal', precision: 5, scale: 2 })
daysRequested: number;
// Half day support
@Column({ name: 'is_half_day', type: 'boolean', default: false })
isHalfDay: boolean;
@Column({
name: 'half_day_type',
type: 'varchar',
length: 20,
nullable: true,
})
halfDayType: HalfDayType | null;
@Index()
@Column({
name: 'status',
type: 'enum',
enum: ['draft', 'submitted', 'approved', 'rejected', 'cancelled'],
enumName: 'hr_leave_status',
default: 'draft',
})
status: LeaveStatus;
// Approval
@Index()
@Column({ name: 'approver_id', type: 'uuid', nullable: true })
approverId: string | null;
@Column({ name: 'approved_at', type: 'timestamptz', nullable: true })
approvedAt: Date | null;
@Column({ name: 'rejection_reason', type: 'text', nullable: true })
rejectionReason: string | null;
// Metadata
@Column({ name: 'request_reason', type: 'text', nullable: true })
requestReason: string | null;
@Column({ name: 'document_url', type: 'text', nullable: true })
documentUrl: string | null;
@Column({ name: 'notes', type: 'text', nullable: true })
notes: string | null;
// Audit
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'submitted_at', type: 'timestamptz', nullable: true })
submittedAt: Date | null;
@Column({ name: 'cancelled_at', type: 'timestamptz', nullable: true })
cancelledAt: Date | null;
@Column({ name: 'cancelled_by', type: 'uuid', nullable: true })
cancelledBy: string | null;
}

View File

@ -0,0 +1,68 @@
/**
* Puesto Entity
* Catálogo de puestos de trabajo
*
* @module HR
* @table hr.puestos
* @ddl schemas/02-hr-schema-ddl.sql
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany,
JoinColumn,
Index,
} from 'typeorm';
import { Tenant } from '../../core/entities/tenant.entity';
import { Employee } from './employee.entity';
@Entity({ schema: 'hr', name: 'puestos' })
@Index(['tenantId', 'codigo'], { unique: true })
export class Puesto {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ type: 'varchar', length: 20 })
codigo: string;
@Column({ type: 'varchar', length: 100 })
nombre: string;
@Column({ type: 'text', nullable: true })
descripcion: string;
@Column({ name: 'nivel_riesgo', type: 'varchar', length: 20, nullable: true })
nivelRiesgo: string;
@Column({
name: 'requiere_capacitacion_especial',
type: 'boolean',
default: false
})
requiereCapacitacionEspecial: boolean;
@Column({ type: 'boolean', default: true })
activo: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@OneToMany(() => Employee, (e) => e.puesto)
empleados: Employee[];
}

View 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';

View 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;
}

View 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[];
}

View 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;
}

View 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;
}

View File

@ -0,0 +1,7 @@
export { ProductCategory } from './product-category.entity';
export { Product } from './product.entity';
export { ProductPrice } from './product-price.entity';
export { ProductSupplier } from './product-supplier.entity';
export { ProductAttribute } from './product-attribute.entity';
export { ProductAttributeValue } from './product-attribute-value.entity';
export { ProductVariant } from './product-variant.entity';

View File

@ -0,0 +1,55 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { ProductAttribute } from './product-attribute.entity';
/**
* Product Attribute Value Entity (schema: products.product_attribute_values)
*
* Represents specific values for product attributes.
* Example: For attribute "Color", values could be "Red", "Blue", "Green".
*/
@Entity({ name: 'product_attribute_values', schema: 'products' })
export class ProductAttributeValue {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'attribute_id', type: 'uuid' })
attributeId: string;
@ManyToOne(() => ProductAttribute, (attribute) => attribute.values, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'attribute_id' })
attribute: ProductAttribute;
@Column({ type: 'varchar', length: 50, nullable: true })
code: string;
@Column({ type: 'varchar', length: 100 })
name: string;
@Column({ name: 'html_color', type: 'varchar', length: 20, nullable: true })
htmlColor: string;
@Column({ name: 'image_url', type: 'varchar', length: 500, nullable: true })
imageUrl: string;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'sort_order', type: 'int', default: 0 })
sortOrder: number;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,60 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
OneToMany,
} from 'typeorm';
import { ProductAttributeValue } from './product-attribute-value.entity';
/**
* Product Attribute Entity (schema: products.product_attributes)
*
* Represents configurable attributes for products like color, size, material.
* Each attribute can have multiple values (e.g., Color: Red, Blue, Green).
*/
@Entity({ name: 'product_attributes', schema: 'products' })
export class ProductAttribute {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ type: 'varchar', length: 50 })
code: string;
@Column({ type: 'varchar', length: 100 })
name: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({ name: 'display_type', type: 'varchar', length: 20, default: 'radio' })
displayType: 'radio' | 'select' | 'color' | 'pills';
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'sort_order', type: 'int', default: 0 })
sortOrder: number;
@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;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedBy: string;
@OneToMany(() => ProductAttributeValue, (value) => value.attribute)
values: ProductAttributeValue[];
}

View File

@ -0,0 +1,69 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
@Entity({ name: 'product_categories', schema: 'products' })
export class ProductCategory {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'parent_id', type: 'uuid', nullable: true })
parentId: string;
@ManyToOne(() => ProductCategory, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'parent_id' })
parent: ProductCategory;
// Identificacion
@Index()
@Column({ type: 'varchar', length: 20 })
code: string;
@Column({ type: 'varchar', length: 100 })
name: string;
@Column({ type: 'text', nullable: true })
description: string;
// Jerarquia
@Column({ name: 'hierarchy_path', type: 'text', nullable: true })
hierarchyPath: string;
@Column({ name: 'hierarchy_level', type: 'int', default: 0 })
hierarchyLevel: number;
// Imagen
@Column({ name: 'image_url', type: 'varchar', length: 500, nullable: true })
imageUrl: string;
// Orden
@Column({ name: 'sort_order', type: 'int', default: 0 })
sortOrder: number;
// Estado
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
// Metadata
@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;
}

View File

@ -0,0 +1,48 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
import { Product } from './product.entity';
@Entity({ name: 'product_prices', schema: 'products' })
export class ProductPrice {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'product_id', type: 'uuid' })
productId: string;
@ManyToOne(() => Product, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'product_id' })
product: Product;
@Index()
@Column({ name: 'price_type', type: 'varchar', length: 30, default: 'standard' })
priceType: 'standard' | 'wholesale' | 'retail' | 'promo';
@Column({ name: 'price_list_name', type: 'varchar', length: 100, nullable: true })
priceListName?: string;
@Column({ type: 'decimal', precision: 15, scale: 4 })
price: number;
@Column({ type: 'varchar', length: 3, default: 'MXN' })
currency: string;
@Column({ name: 'min_quantity', type: 'decimal', precision: 15, scale: 4, default: 1 })
minQuantity: number;
@Column({ name: 'valid_from', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
validFrom: Date;
@Column({ name: 'valid_to', type: 'timestamptz', nullable: true })
validTo?: Date;
@Index()
@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;
}

View File

@ -0,0 +1,51 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
import { Product } from './product.entity';
@Entity({ name: 'product_suppliers', schema: 'products' })
export class ProductSupplier {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'product_id', type: 'uuid' })
productId: string;
@ManyToOne(() => Product, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'product_id' })
product: Product;
@Index()
@Column({ name: 'supplier_id', type: 'uuid' })
supplierId: string;
@Column({ name: 'supplier_sku', type: 'varchar', length: 50, nullable: true })
supplierSku?: string;
@Column({ name: 'supplier_name', type: 'varchar', length: 200, nullable: true })
supplierName?: string;
@Column({ name: 'purchase_price', type: 'decimal', precision: 15, scale: 4, nullable: true })
purchasePrice?: number;
@Column({ type: 'varchar', length: 3, default: 'MXN' })
currency: string;
@Column({ name: 'min_order_qty', type: 'decimal', precision: 15, scale: 4, default: 1 })
minOrderQty: number;
@Column({ name: 'lead_time_days', type: 'int', default: 0 })
leadTimeDays: number;
@Index()
@Column({ name: 'is_preferred', type: 'boolean', default: false })
isPreferred: boolean;
@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;
}

View File

@ -0,0 +1,72 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Product } from './product.entity';
/**
* Product Variant Entity (schema: products.product_variants)
*
* Represents product variants generated from attribute combinations.
* Example: "Blue T-Shirt - Size M" is a variant of product "T-Shirt".
*/
@Entity({ name: 'product_variants', schema: 'products' })
export class ProductVariant {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'product_id', type: 'uuid' })
productId: string;
@ManyToOne(() => Product, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'product_id' })
product: Product;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ type: 'varchar', length: 50 })
sku: string;
@Column({ type: 'varchar', length: 50, nullable: true })
barcode: string;
@Column({ type: 'varchar', length: 200 })
name: string;
@Column({ name: 'price_extra', type: 'decimal', precision: 15, scale: 4, default: 0 })
priceExtra: number;
@Column({ name: 'cost_extra', type: 'decimal', precision: 15, scale: 4, default: 0 })
costExtra: number;
@Column({ name: 'stock_qty', type: 'decimal', precision: 15, scale: 4, default: 0 })
stockQty: number;
@Column({ name: 'image_url', type: 'varchar', length: 500, nullable: true })
imageUrl: string;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@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;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedBy: string;
}

View File

@ -0,0 +1,206 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { ProductCategory } from './product-category.entity';
/**
* Commerce Product Entity (schema: products.products)
*
* NOTE: This is NOT a duplicate of inventory/entities/product.entity.ts
*
* Key differences:
* - This entity: products.products - Commerce/retail focused
* - Has: SAT codes, tax rates, detailed dimensions, min/max stock, reorder points
* - Used by: Sales, purchases, invoicing, POS
*
* - Inventory Product: inventory.products - Warehouse/stock management focused (Odoo-style)
* - Has: valuationMethod, tracking (lot/serial), isStorable, StockQuant/Lot relations
* - Used by: Inventory module for stock tracking, valuation, picking operations
*
* These are intentionally separate by domain. This commerce product entity handles
* pricing, tax compliance (SAT/CFDI), and business rules. For physical stock tracking,
* use the inventory module's product entity.
*/
@Entity({ name: 'products', schema: 'products' })
export class Product {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'category_id', type: 'uuid', nullable: true })
categoryId: string;
@ManyToOne(() => ProductCategory, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'category_id' })
category: ProductCategory;
/**
* Optional link to inventory.products for unified stock management.
* This allows the commerce product to be linked to its inventory counterpart
* for stock tracking, valuation (FIFO/AVERAGE), and warehouse operations.
*
* The inventory product handles: stock levels, lot/serial tracking, valuation layers
* This commerce product handles: pricing, taxes, SAT compliance, commercial data
*/
@Index()
@Column({ name: 'inventory_product_id', type: 'uuid', nullable: true })
inventoryProductId: string | null;
// Identificacion
@Index()
@Column({ type: 'varchar', length: 50 })
sku: string;
@Index()
@Column({ type: 'varchar', length: 50, nullable: true })
barcode: string;
@Column({ type: 'varchar', length: 200 })
name: string;
@Column({ name: 'short_name', type: 'varchar', length: 50, nullable: true })
shortName: string;
@Column({ type: 'text', nullable: true })
description: string;
// Tipo
@Index()
@Column({ name: 'product_type', type: 'varchar', length: 20, default: 'product' })
productType: 'product' | 'service' | 'consumable' | 'kit';
// Precios
@Column({ name: 'sale_price', type: 'decimal', precision: 15, scale: 4, default: 0 })
salePrice: number;
@Column({ name: 'cost_price', type: 'decimal', precision: 15, scale: 4, default: 0 })
costPrice: number;
@Column({ name: 'min_sale_price', type: 'decimal', precision: 15, scale: 4, nullable: true })
minSalePrice: number;
@Column({ type: 'varchar', length: 3, default: 'MXN' })
currency: string;
// Impuestos
@Column({ name: 'tax_rate', type: 'decimal', precision: 5, scale: 2, default: 16 })
taxRate: number;
@Column({ name: 'tax_included', type: 'boolean', default: false })
taxIncluded: boolean;
// 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;
// Unidad de medida
@Column({ type: 'varchar', length: 20, default: 'PZA' })
uom: string;
@Column({ name: 'uom_purchase', type: 'varchar', length: 20, nullable: true })
uomPurchase: string;
@Column({ name: 'conversion_factor', type: 'decimal', precision: 10, scale: 4, default: 1 })
conversionFactor: number;
// Inventario
@Column({ name: 'track_inventory', type: 'boolean', default: true })
trackInventory: boolean;
@Column({ name: 'min_stock', type: 'decimal', precision: 15, scale: 4, default: 0 })
minStock: number;
@Column({ name: 'max_stock', type: 'decimal', precision: 15, scale: 4, nullable: true })
maxStock: number;
@Column({ name: 'reorder_point', type: 'decimal', precision: 15, scale: 4, nullable: true })
reorderPoint: number;
@Column({ name: 'reorder_quantity', type: 'decimal', precision: 15, scale: 4, nullable: true })
reorderQuantity: number;
// Lotes y series
@Column({ name: 'track_lots', type: 'boolean', default: false })
trackLots: boolean;
@Column({ name: 'track_serials', type: 'boolean', default: false })
trackSerials: boolean;
@Column({ name: 'track_expiry', type: 'boolean', default: false })
trackExpiry: boolean;
// Dimensiones
@Column({ type: 'decimal', precision: 10, scale: 4, nullable: true })
weight: number;
@Column({ name: 'weight_unit', type: 'varchar', length: 10, default: 'kg' })
weightUnit: string;
@Column({ type: 'decimal', precision: 10, scale: 4, nullable: true })
length: number;
@Column({ type: 'decimal', precision: 10, scale: 4, nullable: true })
width: number;
@Column({ type: 'decimal', precision: 10, scale: 4, nullable: true })
height: number;
@Column({ name: 'dimension_unit', type: 'varchar', length: 10, default: 'cm' })
dimensionUnit: string;
// Imagenes
@Column({ name: 'image_url', type: 'varchar', length: 500, nullable: true })
imageUrl: string;
@Column({ type: 'text', array: true, default: '{}' })
images: string[];
// Tags
@Column({ type: 'text', array: true, default: '{}' })
tags: string[];
// Notas
@Column({ type: 'text', nullable: true })
notes: string;
// Estado
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'is_sellable', type: 'boolean', default: true })
isSellable: boolean;
@Column({ name: 'is_purchasable', type: 'boolean', default: true })
isPurchasable: boolean;
// Metadata
@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;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedBy: string;
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date;
}

View File

@ -0,0 +1,9 @@
/**
* Profiles Entities - Export
*/
export { Person } from './person.entity';
export { UserProfile } from './user-profile.entity';
export { ProfileTool } from './profile-tool.entity';
export { ProfileModule } from './profile-module.entity';
export { UserProfileAssignment } from './user-profile-assignment.entity';

View File

@ -0,0 +1,78 @@
/**
* Person Entity
* Contact/person information with identity verification
* Compatible with erp-core person.entity
*
* @module Profiles
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
} from 'typeorm';
@Entity({ name: 'persons', schema: 'auth' })
export class Person {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'full_name', type: 'varchar', length: 200 })
fullName: 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({ name: 'maternal_name', type: 'varchar', length: 100, nullable: true })
maternalName: string;
@Index()
@Column({ type: 'varchar', length: 255 })
email: string;
@Column({ type: 'varchar', length: 20, nullable: true })
phone: string;
@Column({ name: 'mobile_phone', type: 'varchar', length: 20, nullable: true })
mobilePhone: string;
@Column({ name: 'identification_type', type: 'varchar', length: 50, nullable: true })
identificationType: string;
@Column({ name: 'identification_number', type: 'varchar', length: 50, nullable: true })
identificationNumber: string;
@Column({ name: 'identification_expiry', type: 'date', nullable: true })
identificationExpiry: Date;
@Column({ type: 'jsonb', default: {} })
address: Record<string, any>;
@Column({ name: 'is_verified', type: 'boolean', default: false })
isVerified: boolean;
@Column({ name: 'verified_at', type: 'timestamptz', nullable: true })
verifiedAt: Date;
@Column({ name: 'verified_by', type: 'uuid', nullable: true })
verifiedBy: string;
@Column({ name: 'is_responsible_for_tenant', type: 'boolean', default: false })
isResponsibleForTenant: 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;
}

View File

@ -0,0 +1,45 @@
/**
* ProfileModule Entity
* Module-level access control per profile
* Compatible with erp-core profile-module.entity
*
* @module Profiles
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
Unique,
Index,
} from 'typeorm';
import { UserProfile } from './user-profile.entity';
@Entity({ name: 'profile_modules', schema: 'auth' })
@Unique(['profileId', 'moduleCode'])
export class ProfileModule {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'profile_id', type: 'uuid' })
profileId: string;
@Column({ name: 'module_code', type: 'varchar', length: 50 })
moduleCode: string;
@Column({ name: 'access_level', type: 'varchar', length: 20, default: 'read' })
accessLevel: 'read' | 'write' | 'admin';
@Column({ name: 'can_export', type: 'boolean', default: false })
canExport: boolean;
@Column({ name: 'can_print', type: 'boolean', default: true })
canPrint: boolean;
@ManyToOne(() => UserProfile, (profile) => profile.modules, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'profile_id' })
profile: UserProfile;
}

View File

@ -0,0 +1,68 @@
/**
* ProfileTool Entity
* Tool/permission assignments per profile
* Compatible with erp-core profile-tool.entity
*
* @module Profiles
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
Unique,
} from 'typeorm';
import { UserProfile } from './user-profile.entity';
@Entity({ name: 'profile_tools', schema: 'auth' })
@Unique(['profileId', 'toolCode'])
export class ProfileTool {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'profile_id', type: 'uuid' })
profileId: string;
@Index()
@Column({ name: 'tool_code', type: 'varchar', length: 50 })
toolCode: string;
@Column({ name: 'tool_name', type: 'varchar', length: 100 })
toolName: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({ type: 'varchar', length: 50, nullable: true })
category: string;
@Column({ name: 'is_mobile_only', type: 'boolean', default: false })
isMobileOnly: boolean;
@Column({ name: 'is_web_only', type: 'boolean', default: false })
isWebOnly: boolean;
@Column({ type: 'varchar', length: 50, nullable: true })
icon: string;
@Column({ type: 'jsonb', default: {} })
configuration: Record<string, any>;
@Column({ name: 'sort_order', type: 'integer', default: 0 })
sortOrder: number;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@ManyToOne(() => UserProfile, (profile) => profile.tools, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'profile_id' })
profile: UserProfile;
}

View File

@ -0,0 +1,50 @@
/**
* UserProfileAssignment Entity
* Links users to profiles with expiration support
* Compatible with erp-core user-profile-assignment.entity
*
* @module Profiles
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
Unique,
} from 'typeorm';
import { UserProfile } from './user-profile.entity';
@Entity({ name: 'user_profile_assignments', schema: 'auth' })
@Unique(['userId', 'profileId'])
export class UserProfileAssignment {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'user_id', type: 'uuid' })
userId: string;
@Index()
@Column({ name: 'profile_id', type: 'uuid' })
profileId: string;
@Column({ name: 'is_primary', type: 'boolean', default: false })
isPrimary: boolean;
@CreateDateColumn({ name: 'assigned_at', type: 'timestamptz' })
assignedAt: Date;
@Column({ name: 'assigned_by', type: 'uuid', nullable: true })
assignedBy: string;
@Column({ name: 'expires_at', type: 'timestamptz', nullable: true })
expiresAt: Date;
@ManyToOne(() => UserProfile, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'profile_id' })
profile: UserProfile;
}

View File

@ -0,0 +1,90 @@
/**
* UserProfile Entity
* Role-based profile with module access, tools and pricing
* Compatible with erp-core user-profile.entity
*
* @module Profiles
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
OneToMany,
Unique,
} from 'typeorm';
import { ProfileTool } from './profile-tool.entity';
import { ProfileModule } from './profile-module.entity';
@Entity({ name: 'user_profiles', schema: 'auth' })
@Unique(['tenantId', 'code'])
export class UserProfile {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid', nullable: true })
tenantId: string;
@Index()
@Column({ type: 'varchar', length: 10 })
code: string;
@Column({ type: 'varchar', length: 100 })
name: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({ name: 'is_system', type: 'boolean', default: false })
isSystem: boolean;
@Column({ type: 'varchar', length: 20, nullable: true })
color: string;
@Column({ type: 'varchar', length: 50, nullable: true })
icon: string;
@Column({ name: 'base_permissions', type: 'jsonb', default: [] })
basePermissions: string[];
@Column({ name: 'available_modules', type: 'text', array: true, default: [] })
availableModules: string[];
@Column({ name: 'monthly_price', type: 'decimal', precision: 10, scale: 2, default: 0 })
monthlyPrice: number;
@Column({ name: 'included_platforms', type: 'text', array: true, default: ['web'] })
includedPlatforms: string[];
@Column({ name: 'default_tools', type: 'text', array: true, default: [] })
defaultTools: string[];
@Column({ name: 'feature_flags', type: 'jsonb', default: {} })
featureFlags: Record<string, boolean>;
@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;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedBy: string;
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date;
@OneToMany(() => ProfileTool, (tool) => tool.profile, { cascade: true })
tools: ProfileTool[];
@OneToMany(() => ProfileModule, (module) => module.profile, { cascade: true })
modules: ProfileModule[];
}

View File

@ -0,0 +1 @@
export * from './timesheet.entity';

View File

@ -0,0 +1,93 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
} from 'typeorm';
export enum TimesheetStatus {
DRAFT = 'draft',
SUBMITTED = 'submitted',
APPROVED = 'approved',
REJECTED = 'rejected',
}
@Entity({ schema: 'projects', name: 'timesheets' })
@Index('idx_timesheets_tenant', ['tenantId'])
@Index('idx_timesheets_company', ['companyId'])
@Index('idx_timesheets_project', ['projectId'])
@Index('idx_timesheets_task', ['taskId'])
@Index('idx_timesheets_user', ['userId'])
@Index('idx_timesheets_user_date', ['userId', 'date'])
@Index('idx_timesheets_date', ['date'])
@Index('idx_timesheets_status', ['status'])
export class TimesheetEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', nullable: false, name: 'tenant_id' })
tenantId: string;
@Column({ type: 'uuid', nullable: false, name: 'company_id' })
companyId: string;
@Column({ type: 'uuid', nullable: false, name: 'project_id' })
projectId: string;
@Column({ type: 'uuid', nullable: true, name: 'task_id' })
taskId: string | null;
@Column({ type: 'uuid', nullable: false, name: 'user_id' })
userId: string;
@Column({ type: 'date', nullable: false })
date: Date;
@Column({ type: 'decimal', precision: 5, scale: 2, nullable: false })
hours: number;
@Column({ type: 'text', nullable: true })
description: string | null;
@Column({ type: 'boolean', default: true, nullable: false })
billable: boolean;
@Column({ type: 'boolean', default: false, nullable: false })
invoiced: boolean;
@Column({ type: 'uuid', nullable: true, name: 'invoice_id' })
invoiceId: string | null;
@Column({
type: 'enum',
enum: TimesheetStatus,
default: TimesheetStatus.DRAFT,
nullable: false,
})
status: TimesheetStatus;
@Column({ type: 'uuid', nullable: true, name: 'approved_by' })
approvedBy: string | null;
@Column({ type: 'timestamp', nullable: true, name: 'approved_at' })
approvedAt: Date | null;
// 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;
}

View File

@ -0,0 +1,101 @@
/**
* ComparativoCotizaciones Entity
* Cuadro comparativo de cotizaciones
*
* @module Purchase
* @table purchase.comparativo_cotizaciones
* @ddl schemas/07-purchase-ext-schema-ddl.sql
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany,
JoinColumn,
Index,
} from 'typeorm';
import { Tenant } from '../../core/entities/tenant.entity';
import { User } from '../../core/entities/user.entity';
import { RequisicionObra } from '../../inventory/entities/requisicion-obra.entity';
import { ComparativoProveedor } from './comparativo-proveedor.entity';
export type ComparativoStatus = 'draft' | 'in_evaluation' | 'approved' | 'cancelled';
@Entity({ schema: 'purchase', name: 'comparativo_cotizaciones' })
@Index(['tenantId', 'code'], { unique: true })
export class ComparativoCotizaciones {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'requisicion_id', type: 'uuid', nullable: true })
requisicionId: string;
@Column({ type: 'varchar', length: 30 })
code: string;
@Column({ type: 'varchar', length: 255 })
name: string;
@Column({ name: 'comparison_date', type: 'date' })
comparisonDate: Date;
@Column({ type: 'varchar', length: 20, default: 'draft' })
status: ComparativoStatus;
@Column({ name: 'winner_supplier_id', type: 'uuid', nullable: true })
winnerSupplierId: string;
@Column({ name: 'approved_by', type: 'uuid', nullable: true })
approvedById: string;
@Column({ name: 'approved_at', type: 'timestamptz', nullable: true })
approvedAt: Date;
@Column({ type: 'text', nullable: true })
notes: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdById: string;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedById: string;
@Column({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date;
@Column({ name: 'deleted_by', type: 'uuid', nullable: true })
deletedById: string;
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@ManyToOne(() => RequisicionObra)
@JoinColumn({ name: 'requisicion_id' })
requisicion: RequisicionObra;
@ManyToOne(() => User)
@JoinColumn({ name: 'approved_by' })
approvedBy: User;
@ManyToOne(() => User)
@JoinColumn({ name: 'created_by' })
createdBy: User;
@OneToMany(() => ComparativoProveedor, (cp) => cp.comparativo)
proveedores: ComparativoProveedor[];
}

View File

@ -0,0 +1,75 @@
/**
* ComparativoProducto Entity
* Productos cotizados por proveedor en comparativo
*
* @module Purchase
* @table purchase.comparativo_productos
* @ddl schemas/07-purchase-ext-schema-ddl.sql
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Tenant } from '../../core/entities/tenant.entity';
import { User } from '../../core/entities/user.entity';
import { ComparativoProveedor } from './comparativo-proveedor.entity';
@Entity({ schema: 'purchase', name: 'comparativo_productos' })
export class ComparativoProducto {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'comparativo_proveedor_id', type: 'uuid' })
comparativoProveedorId: string;
@Column({ name: 'product_id', type: 'uuid' })
productId: string;
@Column({ type: 'decimal', precision: 12, scale: 4 })
quantity: number;
@Column({ name: 'unit_price', type: 'decimal', precision: 12, scale: 4 })
unitPrice: number;
@Column({ type: 'text', nullable: true })
notes: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdById: string;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedById: string;
// Computed property (in DB is GENERATED ALWAYS AS)
get totalPrice(): number {
return this.quantity * this.unitPrice;
}
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@ManyToOne(() => ComparativoProveedor, (cp) => cp.productos, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'comparativo_proveedor_id' })
comparativoProveedor: ComparativoProveedor;
@ManyToOne(() => User)
@JoinColumn({ name: 'created_by' })
createdBy: User;
}

View File

@ -0,0 +1,87 @@
/**
* ComparativoProveedor Entity
* Proveedores participantes en comparativo
*
* @module Purchase
* @table purchase.comparativo_proveedores
* @ddl schemas/07-purchase-ext-schema-ddl.sql
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm';
import { Tenant } from '../../core/entities/tenant.entity';
import { User } from '../../core/entities/user.entity';
import { ComparativoCotizaciones } from './comparativo-cotizaciones.entity';
import { ComparativoProducto } from './comparativo-producto.entity';
@Entity({ schema: 'purchase', name: 'comparativo_proveedores' })
export class ComparativoProveedor {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'comparativo_id', type: 'uuid' })
comparativoId: string;
@Column({ name: 'supplier_id', type: 'uuid' })
supplierId: string;
@Column({ name: 'quotation_number', type: 'varchar', length: 50, nullable: true })
quotationNumber: string;
@Column({ name: 'quotation_date', type: 'date', nullable: true })
quotationDate: Date;
@Column({ name: 'delivery_days', type: 'integer', nullable: true })
deliveryDays: number;
@Column({ name: 'payment_conditions', type: 'varchar', length: 100, nullable: true })
paymentConditions: string;
@Column({ name: 'total_amount', type: 'decimal', precision: 16, scale: 2, nullable: true })
totalAmount: number;
@Column({ name: 'is_selected', type: 'boolean', default: false })
isSelected: boolean;
@Column({ name: 'evaluation_notes', type: 'text', nullable: true })
evaluationNotes: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdById: string;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedById: string;
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@ManyToOne(() => ComparativoCotizaciones, (c) => c.proveedores, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'comparativo_id' })
comparativo: ComparativoCotizaciones;
@ManyToOne(() => User)
@JoinColumn({ name: 'created_by' })
createdBy: User;
@OneToMany(() => ComparativoProducto, (cp) => cp.comparativoProveedor)
productos: ComparativoProducto[];
}

View File

@ -0,0 +1,20 @@
/**
* Purchase Entities Index
* @module Purchase
*
* Extensiones de compras para construccion (MAI-004)
*/
// Construction-specific entities
export * from './purchase-order-construction.entity';
export * from './supplier-construction.entity';
export * from './comparativo-cotizaciones.entity';
export * from './comparativo-proveedor.entity';
export * from './comparativo-producto.entity';
// Core purchase entities (from erp-core)
export * from './purchase-receipt.entity';
export * from './purchase-receipt-item.entity';
export * from './purchase-order-matching.entity';
export * from './purchase-matching-line.entity';
export * from './matching-exception.entity';

View File

@ -0,0 +1,78 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { PurchaseOrderMatching } from './purchase-order-matching.entity';
import { PurchaseMatchingLine } from './purchase-matching-line.entity';
export type ExceptionType =
| 'over_receipt'
| 'short_receipt'
| 'over_invoice'
| 'short_invoice'
| 'price_variance';
export type ExceptionStatus = 'pending' | 'approved' | 'rejected';
@Entity({ name: 'matching_exceptions', schema: 'purchases' })
export class MatchingException {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'matching_id', type: 'uuid', nullable: true })
matchingId?: string;
@ManyToOne(() => PurchaseOrderMatching, (matching) => matching.exceptions, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'matching_id' })
matching?: PurchaseOrderMatching;
@Index()
@Column({ name: 'matching_line_id', type: 'uuid', nullable: true })
matchingLineId?: string;
@ManyToOne(() => PurchaseMatchingLine, (line) => line.exceptions, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'matching_line_id' })
matchingLine?: PurchaseMatchingLine;
@Index()
@Column({ name: 'exception_type', type: 'varchar', length: 50 })
exceptionType: ExceptionType;
@Column({ name: 'expected_value', type: 'decimal', precision: 15, scale: 4, nullable: true })
expectedValue?: number;
@Column({ name: 'actual_value', type: 'decimal', precision: 15, scale: 4, nullable: true })
actualValue?: number;
@Column({ name: 'variance_value', type: 'decimal', precision: 15, scale: 4, nullable: true })
varianceValue?: number;
@Column({ name: 'variance_percent', type: 'decimal', precision: 5, scale: 2, nullable: true })
variancePercent?: number;
@Index()
@Column({ type: 'varchar', length: 20, default: 'pending' })
status: ExceptionStatus;
@Column({ name: 'resolved_at', type: 'timestamptz', nullable: true })
resolvedAt?: Date;
@Column({ name: 'resolved_by', type: 'uuid', nullable: true })
resolvedBy?: string;
@Column({ name: 'resolution_notes', type: 'text', nullable: true })
resolutionNotes?: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
}

View File

@ -0,0 +1,98 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm';
import { PurchaseOrderMatching } from './purchase-order-matching.entity';
import { MatchingException } from './matching-exception.entity';
export type MatchingLineStatus = 'pending' | 'partial' | 'matched' | 'mismatch';
@Entity({ name: 'purchase_matching_lines', schema: 'purchases' })
export class PurchaseMatchingLine {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'matching_id', type: 'uuid' })
matchingId: string;
@ManyToOne(() => PurchaseOrderMatching, (matching) => matching.lines, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'matching_id' })
matching: PurchaseOrderMatching;
@Index()
@Column({ name: 'order_item_id', type: 'uuid' })
orderItemId: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
// Quantities
@Column({ name: 'qty_ordered', type: 'decimal', precision: 15, scale: 4 })
qtyOrdered: number;
@Column({ name: 'qty_received', type: 'decimal', precision: 15, scale: 4, default: 0 })
qtyReceived: number;
@Column({ name: 'qty_invoiced', type: 'decimal', precision: 15, scale: 4, default: 0 })
qtyInvoiced: number;
// Prices
@Column({ name: 'price_ordered', type: 'decimal', precision: 15, scale: 2 })
priceOrdered: number;
@Column({ name: 'price_invoiced', type: 'decimal', precision: 15, scale: 2, default: 0 })
priceInvoiced: number;
// Generated columns (read-only in TypeORM)
@Column({
name: 'qty_variance',
type: 'decimal',
precision: 15,
scale: 4,
insert: false,
update: false,
nullable: true,
})
qtyVariance: number;
@Column({
name: 'invoice_qty_variance',
type: 'decimal',
precision: 15,
scale: 4,
insert: false,
update: false,
nullable: true,
})
invoiceQtyVariance: number;
@Column({
name: 'price_variance',
type: 'decimal',
precision: 15,
scale: 2,
insert: false,
update: false,
nullable: true,
})
priceVariance: number;
@Index()
@Column({ type: 'varchar', length: 20, default: 'pending' })
status: MatchingLineStatus;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
// Relations
@OneToMany(() => MatchingException, (exception) => exception.matchingLine)
exceptions: MatchingException[];
}

View File

@ -0,0 +1,114 @@
/**
* PurchaseOrderConstruction Entity
* Extensión de órdenes de compra para construcción
*
* @module Purchase (MAI-004)
* @table purchase.purchase_order_construction
* @ddl schemas/07-purchase-ext-schema-ddl.sql
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Index,
} from 'typeorm';
import { Tenant } from '../../core/entities/tenant.entity';
import { User } from '../../core/entities/user.entity';
import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity';
import { RequisicionObra } from '../../inventory/entities/requisicion-obra.entity';
@Entity({ schema: 'purchase', name: 'purchase_order_construction' })
@Index(['tenantId'])
@Index(['purchaseOrderId'], { unique: true })
@Index(['fraccionamientoId'])
@Index(['requisicionId'])
export class PurchaseOrderConstruction {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
// FK a purchase.purchase_orders (ERP Core)
@Column({ name: 'purchase_order_id', type: 'uuid' })
purchaseOrderId: string;
@Column({ name: 'fraccionamiento_id', type: 'uuid', nullable: true })
fraccionamientoId: string;
@Column({ name: 'requisicion_id', type: 'uuid', nullable: true })
requisicionId: string;
// Delivery information
@Column({ name: 'delivery_location', type: 'varchar', length: 255, nullable: true })
deliveryLocation: string;
@Column({ name: 'delivery_contact', type: 'varchar', length: 100, nullable: true })
deliveryContact: string;
@Column({ name: 'delivery_phone', type: 'varchar', length: 20, nullable: true })
deliveryPhone: string;
// Reception
@Column({ name: 'received_by', type: 'uuid', nullable: true })
receivedById: string;
@Column({ name: 'received_at', type: 'timestamptz', nullable: true })
receivedAt: Date;
// Quality check
@Column({ name: 'quality_approved', type: 'boolean', nullable: true })
qualityApproved: boolean;
@Column({ name: 'quality_notes', type: 'text', nullable: true })
qualityNotes: string;
// Audit
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdById: string;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedById: string;
@Column({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date;
@Column({ name: 'deleted_by', type: 'uuid', nullable: true })
deletedById: string;
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@ManyToOne(() => Fraccionamiento, { nullable: true })
@JoinColumn({ name: 'fraccionamiento_id' })
fraccionamiento: Fraccionamiento;
@ManyToOne(() => RequisicionObra, { nullable: true })
@JoinColumn({ name: 'requisicion_id' })
requisicion: RequisicionObra;
@ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: 'received_by' })
receivedBy: User;
@ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: 'created_by' })
createdBy: User;
@ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: 'updated_by' })
updatedBy: User;
}

View File

@ -0,0 +1,102 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm';
import { PurchaseReceipt } from './purchase-receipt.entity';
import { PurchaseMatchingLine } from './purchase-matching-line.entity';
import { MatchingException } from './matching-exception.entity';
export type MatchingStatus =
| 'pending'
| 'partial_receipt'
| 'received'
| 'partial_invoice'
| 'matched'
| 'mismatch';
@Entity({ name: 'purchase_order_matching', schema: 'purchases' })
export class PurchaseOrderMatching {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'purchase_order_id', type: 'uuid' })
purchaseOrderId: string;
@Index()
@Column({ type: 'varchar', length: 20, default: 'pending' })
status: MatchingStatus;
@Column({ name: 'total_ordered', type: 'decimal', precision: 15, scale: 2 })
totalOrdered: number;
@Column({ name: 'total_received', type: 'decimal', precision: 15, scale: 2, default: 0 })
totalReceived: number;
@Column({ name: 'total_invoiced', type: 'decimal', precision: 15, scale: 2, default: 0 })
totalInvoiced: number;
// Generated columns (read-only in TypeORM)
@Column({
name: 'receipt_variance',
type: 'decimal',
precision: 15,
scale: 2,
insert: false,
update: false,
nullable: true,
})
receiptVariance: number;
@Column({
name: 'invoice_variance',
type: 'decimal',
precision: 15,
scale: 2,
insert: false,
update: false,
nullable: true,
})
invoiceVariance: number;
@Index()
@Column({ name: 'last_receipt_id', type: 'uuid', nullable: true })
lastReceiptId?: string;
@ManyToOne(() => PurchaseReceipt, { nullable: true })
@JoinColumn({ name: 'last_receipt_id' })
lastReceipt?: PurchaseReceipt;
@Column({ name: 'last_invoice_id', type: 'uuid', nullable: true })
lastInvoiceId?: string;
@Column({ name: 'matched_at', type: 'timestamptz', nullable: true })
matchedAt?: Date;
@Column({ name: 'matched_by', type: 'uuid', nullable: true })
matchedBy?: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
// Relations
@OneToMany(() => PurchaseMatchingLine, (line) => line.matching)
lines: PurchaseMatchingLine[];
@OneToMany(() => MatchingException, (exception) => exception.matching)
exceptions: MatchingException[];
}

View File

@ -0,0 +1,57 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
import { PurchaseReceipt } from './purchase-receipt.entity';
@Entity({ name: 'purchase_receipt_items', schema: 'purchases' })
export class PurchaseReceiptItem {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'receipt_id', type: 'uuid' })
receiptId: string;
@ManyToOne(() => PurchaseReceipt, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'receipt_id' })
receipt: PurchaseReceipt;
@Column({ name: 'order_item_id', type: 'uuid', nullable: true })
orderItemId?: string;
@Index()
@Column({ name: 'product_id', type: 'uuid', nullable: true })
productId?: string;
@Column({ name: 'quantity_expected', type: 'decimal', precision: 15, scale: 4, nullable: true })
quantityExpected?: number;
@Column({ name: 'quantity_received', type: 'decimal', precision: 15, scale: 4 })
quantityReceived: number;
@Column({ name: 'quantity_rejected', type: 'decimal', precision: 15, scale: 4, default: 0 })
quantityRejected: number;
@Index()
@Column({ name: 'lot_number', type: 'varchar', length: 50, nullable: true })
lotNumber?: string;
@Column({ name: 'serial_number', type: 'varchar', length: 50, nullable: true })
serialNumber?: string;
@Column({ name: 'expiry_date', type: 'date', nullable: true })
expiryDate?: Date;
@Column({ name: 'location_id', type: 'uuid', nullable: true })
locationId?: string;
@Column({ name: 'quality_status', type: 'varchar', length: 20, default: 'pending' })
qualityStatus: 'pending' | 'approved' | 'rejected' | 'quarantine';
@Column({ name: 'quality_notes', type: 'text', nullable: true })
qualityNotes?: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,52 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
@Entity({ name: 'purchase_receipts', schema: 'purchases' })
export class PurchaseReceipt {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'order_id', type: 'uuid' })
orderId: string;
@Column({ name: 'receipt_number', type: 'varchar', length: 30 })
receiptNumber: string;
@Column({ name: 'receipt_date', type: 'date', default: () => 'CURRENT_DATE' })
receiptDate: Date;
@Column({ name: 'received_by', type: 'uuid', nullable: true })
receivedBy?: string;
@Column({ name: 'warehouse_id', type: 'uuid', nullable: true })
warehouseId?: string;
@Column({ name: 'location_id', type: 'uuid', nullable: true })
locationId?: string;
@Column({ name: 'supplier_delivery_note', type: 'varchar', length: 100, nullable: true })
supplierDeliveryNote?: string;
@Column({ name: 'supplier_invoice_number', type: 'varchar', length: 100, nullable: true })
supplierInvoiceNumber?: string;
@Index()
@Column({ type: 'varchar', length: 20, default: 'draft' })
status: 'draft' | 'confirmed' | 'cancelled';
@Column({ type: 'text', nullable: true })
notes?: 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;
}

View File

@ -0,0 +1,130 @@
/**
* SupplierConstruction Entity
* Extensión de proveedores para construcción
*
* @module Purchase (MAI-004)
* @table purchase.supplier_construction
* @ddl schemas/07-purchase-ext-schema-ddl.sql
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Index,
} from 'typeorm';
import { Tenant } from '../../core/entities/tenant.entity';
import { User } from '../../core/entities/user.entity';
@Entity({ schema: 'purchase', name: 'supplier_construction' })
@Index(['tenantId'])
@Index(['supplierId'], { unique: true })
@Index(['overallRating'])
export class SupplierConstruction {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
// FK a purchase.suppliers (ERP Core)
@Column({ name: 'supplier_id', type: 'uuid' })
supplierId: string;
// Supplier type flags
@Column({ name: 'is_materials_supplier', type: 'boolean', default: false })
isMaterialsSupplier: boolean;
@Column({ name: 'is_services_supplier', type: 'boolean', default: false })
isServicesSupplier: boolean;
@Column({ name: 'is_equipment_supplier', type: 'boolean', default: false })
isEquipmentSupplier: boolean;
@Column({ type: 'text', array: true, nullable: true })
specialties: string[];
// Ratings (1.00 - 5.00)
@Column({ name: 'quality_rating', type: 'decimal', precision: 3, scale: 2, nullable: true })
qualityRating: number;
@Column({ name: 'delivery_rating', type: 'decimal', precision: 3, scale: 2, nullable: true })
deliveryRating: number;
@Column({ name: 'price_rating', type: 'decimal', precision: 3, scale: 2, nullable: true })
priceRating: number;
// Overall rating (computed in DB, but we can calculate in code too)
@Column({
name: 'overall_rating',
type: 'decimal',
precision: 3,
scale: 2,
nullable: true,
insert: false,
update: false,
})
overallRating: number;
@Column({ name: 'last_evaluation_date', type: 'date', nullable: true })
lastEvaluationDate: Date;
// Credit terms
@Column({ name: 'credit_limit', type: 'decimal', precision: 14, scale: 2, nullable: true })
creditLimit: number;
@Column({ name: 'payment_days', type: 'int', default: 30 })
paymentDays: number;
// Documents status
@Column({ name: 'has_valid_documents', type: 'boolean', default: false })
hasValidDocuments: boolean;
@Column({ name: 'documents_expiry_date', type: 'date', nullable: true })
documentsExpiryDate: Date;
// Audit
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdById: string;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedById: string;
@Column({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date;
@Column({ name: 'deleted_by', type: 'uuid', nullable: true })
deletedById: string;
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: 'created_by' })
createdBy: User;
@ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: 'updated_by' })
updatedBy: User;
// Computed property for overall rating
calculateOverallRating(): number {
const ratings = [this.qualityRating, this.deliveryRating, this.priceRating].filter(
(r) => r !== null && r !== undefined
);
if (ratings.length === 0) return 0;
return ratings.reduce((sum, r) => sum + Number(r), 0) / ratings.length;
}
}

View File

@ -0,0 +1,67 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Report } from './report.entity';
/**
* Custom Report Entity (schema: reports.custom_reports)
*
* User-personalized reports based on existing definitions.
* Stores custom columns, filters, grouping, and sorting preferences.
*/
@Entity({ name: 'custom_reports', schema: 'reports' })
export class CustomReport {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'owner_id', type: 'uuid' })
ownerId: string;
@Column({ name: 'base_definition_id', type: 'uuid', nullable: true })
baseDefinitionId: string | null;
@Column({ name: 'name', type: 'varchar', length: 255 })
name: string;
@Column({ name: 'description', type: 'text', nullable: true })
description: string | null;
@Column({ name: 'custom_columns', type: 'jsonb', default: '[]' })
customColumns: Record<string, any>[];
@Column({ name: 'custom_filters', type: 'jsonb', default: '[]' })
customFilters: Record<string, any>[];
@Column({ name: 'custom_grouping', type: 'jsonb', default: '[]' })
customGrouping: Record<string, any>[];
@Column({ name: 'custom_sorting', type: 'jsonb', default: '[]' })
customSorting: Record<string, any>[];
@Index()
@Column({ name: 'is_favorite', type: 'boolean', default: false })
isFavorite: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
// Relations
@ManyToOne(() => Report, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'base_definition_id' })
baseDefinition: Report | null;
}

View File

@ -0,0 +1,222 @@
/**
* DashboardWidget Entity
* Configuración de widgets de dashboard
*
* @module Reports
* @table reports.dashboard_widgets
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Index,
} from 'typeorm';
import { Tenant } from '../../core/entities/tenant.entity';
import { Dashboard } from './dashboard.entity';
export type WidgetType =
| 'kpi_card'
| 'line_chart'
| 'bar_chart'
| 'pie_chart'
| 'donut_chart'
| 'area_chart'
| 'gauge'
| 'table'
| 'heatmap'
| 'map'
| 'timeline'
| 'progress'
| 'list'
| 'text'
| 'image'
| 'custom';
export type DataSourceType = 'query' | 'api' | 'static' | 'kpi' | 'report';
@Entity({ schema: 'reports', name: 'dashboard_widgets' })
@Index(['tenantId'])
@Index(['dashboardId'])
@Index(['widgetType'])
@Index(['isActive'])
export class DashboardWidget {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'dashboard_id', type: 'uuid' })
dashboardId: string;
@Column({ type: 'varchar', length: 200 })
title: string;
@Column({ type: 'text', nullable: true })
subtitle: string | null;
@Column({
name: 'widget_type',
type: 'varchar',
length: 30,
})
widgetType: WidgetType;
@Column({
name: 'data_source_type',
type: 'varchar',
length: 20,
default: 'query',
})
dataSourceType: DataSourceType;
@Column({
name: 'data_source',
type: 'jsonb',
nullable: true,
comment: 'Query, API endpoint, or KPI code',
})
dataSource: Record<string, any> | null;
@Column({
type: 'jsonb',
nullable: true,
comment: 'Widget-specific configuration',
})
config: Record<string, any> | null;
@Column({
type: 'jsonb',
nullable: true,
comment: 'Chart options (colors, legend, etc)',
})
chartOptions: Record<string, any> | null;
@Column({
type: 'jsonb',
nullable: true,
comment: 'Threshold/alert configuration',
})
thresholds: Record<string, any> | null;
@Column({
name: 'grid_x',
type: 'integer',
default: 0,
comment: 'Grid position X',
})
gridX: number;
@Column({
name: 'grid_y',
type: 'integer',
default: 0,
comment: 'Grid position Y',
})
gridY: number;
@Column({
name: 'grid_width',
type: 'integer',
default: 4,
comment: 'Width in grid units',
})
gridWidth: number;
@Column({
name: 'grid_height',
type: 'integer',
default: 2,
comment: 'Height in grid units',
})
gridHeight: number;
@Column({
name: 'min_width',
type: 'integer',
default: 2,
})
minWidth: number;
@Column({
name: 'min_height',
type: 'integer',
default: 1,
})
minHeight: number;
@Column({
name: 'refresh_interval',
type: 'integer',
nullable: true,
comment: 'Override dashboard refresh (seconds)',
})
refreshInterval: number | null;
@Column({
name: 'cache_duration',
type: 'integer',
default: 60,
comment: 'Cache duration in seconds',
})
cacheDuration: number;
@Column({
name: 'drill_down_config',
type: 'jsonb',
nullable: true,
comment: 'Drill-down navigation config',
})
drillDownConfig: Record<string, any> | null;
@Column({
name: 'click_action',
type: 'jsonb',
nullable: true,
comment: 'Action on click (navigate, filter, etc)',
})
clickAction: Record<string, any> | null;
@Column({
name: 'is_active',
type: 'boolean',
default: true,
})
isActive: boolean;
@Column({
name: 'sort_order',
type: 'integer',
default: 0,
})
sortOrder: number;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdById: string | null;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true })
updatedAt: Date | null;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedById: string | null;
@Column({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date | null;
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@ManyToOne(() => Dashboard)
@JoinColumn({ name: 'dashboard_id' })
dashboard: Dashboard;
}

View File

@ -0,0 +1,205 @@
/**
* Dashboard Entity
* Configuración de dashboards personalizables
*
* @module Reports
* @table reports.dashboards
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany,
JoinColumn,
Index,
} from 'typeorm';
import { Tenant } from '../../core/entities/tenant.entity';
import { User } from '../../core/entities/user.entity';
import { DashboardWidget } from './dashboard-widget.entity';
export type DashboardType = 'corporate' | 'project' | 'department' | 'personal' | 'custom';
export type DashboardVisibility = 'private' | 'team' | 'department' | 'company';
@Entity({ schema: 'reports', name: 'dashboards' })
@Index(['tenantId', 'code'], { unique: true })
@Index(['tenantId'])
@Index(['dashboardType'])
@Index(['ownerId'])
@Index(['isActive'])
export class Dashboard {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ type: 'varchar', length: 50 })
code: string;
@Column({ type: 'varchar', length: 200 })
name: string;
@Column({ type: 'text', nullable: true })
description: string | null;
@Column({
name: 'dashboard_type',
type: 'varchar',
length: 30,
default: 'custom',
})
dashboardType: DashboardType;
@Column({
type: 'varchar',
length: 20,
default: 'private',
})
visibility: DashboardVisibility;
@Column({
name: 'owner_id',
type: 'uuid',
nullable: true,
comment: 'User who owns this dashboard',
})
ownerId: string | null;
@Column({
name: 'fraccionamiento_id',
type: 'uuid',
nullable: true,
comment: 'Project-specific dashboard',
})
fraccionamientoId: string | null;
@Column({
type: 'jsonb',
nullable: true,
comment: 'Layout configuration (grid positions)',
})
layout: Record<string, any> | null;
@Column({
type: 'jsonb',
nullable: true,
comment: 'Theme and styling configuration',
})
theme: Record<string, any> | null;
@Column({
name: 'refresh_interval',
type: 'integer',
default: 300,
comment: 'Auto-refresh interval in seconds',
})
refreshInterval: number;
@Column({
name: 'default_date_range',
type: 'varchar',
length: 30,
default: 'last_30_days',
comment: 'Default date range filter',
})
defaultDateRange: string;
@Column({
name: 'default_filters',
type: 'jsonb',
nullable: true,
})
defaultFilters: Record<string, any> | null;
@Column({
name: 'allowed_roles',
type: 'varchar',
array: true,
nullable: true,
comment: 'Roles that can view this dashboard',
})
allowedRoles: string[] | null;
@Column({
name: 'is_default',
type: 'boolean',
default: false,
comment: 'Default dashboard for type',
})
isDefault: boolean;
@Column({
name: 'is_active',
type: 'boolean',
default: true,
})
isActive: boolean;
@Column({
name: 'is_system',
type: 'boolean',
default: false,
comment: 'System dashboard cannot be deleted',
})
isSystem: boolean;
@Column({
name: 'sort_order',
type: 'integer',
default: 0,
})
sortOrder: number;
@Column({
name: 'view_count',
type: 'integer',
default: 0,
})
viewCount: number;
@Column({
name: 'last_viewed_at',
type: 'timestamptz',
nullable: true,
})
lastViewedAt: Date | null;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdById: string | null;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true })
updatedAt: Date | null;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedById: string | null;
@Column({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date | null;
@Column({ name: 'deleted_by', type: 'uuid', nullable: true })
deletedById: string | null;
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@ManyToOne(() => User)
@JoinColumn({ name: 'owner_id' })
owner: User | null;
@ManyToOne(() => User)
@JoinColumn({ name: 'created_by' })
createdBy: User | null;
@OneToMany(() => DashboardWidget, (w) => w.dashboard)
widgets: DashboardWidget[];
}

View File

@ -0,0 +1,69 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
OneToMany,
} from 'typeorm';
import { DataModelField } from './data-model-field.entity';
import { DataModelRelationship } from './data-model-relationship.entity';
/**
* Data Model Entity (schema: reports.data_model_entities)
*
* Represents database tables/entities available for report building.
* Used by the report builder UI to construct dynamic queries.
*/
@Entity({ name: 'data_model_entities', schema: 'reports' })
@Index(['name'], { unique: true })
export class DataModelEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'name', type: 'varchar', length: 100 })
name: string;
@Column({ name: 'display_name', type: 'varchar', length: 255 })
displayName: string;
@Column({ name: 'description', type: 'text', nullable: true })
description: string | null;
@Index()
@Column({ name: 'schema_name', type: 'varchar', length: 100 })
schemaName: string;
@Column({ name: 'table_name', type: 'varchar', length: 100 })
tableName: string;
@Column({ name: 'primary_key_column', type: 'varchar', length: 100, default: 'id' })
primaryKeyColumn: string;
@Column({ name: 'tenant_column', type: 'varchar', length: 100, nullable: true, default: 'tenant_id' })
tenantColumn: string | null;
@Column({ name: 'is_multi_tenant', type: 'boolean', default: true })
isMultiTenant: boolean;
@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;
// Relations
@OneToMany(() => DataModelField, (field) => field.entity)
fields: DataModelField[];
@OneToMany(() => DataModelRelationship, (rel) => rel.sourceEntity)
sourceRelationships: DataModelRelationship[];
@OneToMany(() => DataModelRelationship, (rel) => rel.targetEntity)
targetRelationships: DataModelRelationship[];
}

View File

@ -0,0 +1,77 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { DataModelEntity } from './data-model-entity.entity';
/**
* Data Model Field Entity (schema: reports.data_model_fields)
*
* Represents columns/fields within a data model entity.
* Includes metadata for filtering, sorting, grouping, and formatting.
*/
@Entity({ name: 'data_model_fields', schema: 'reports' })
@Index(['entityId', 'name'], { unique: true })
export class DataModelField {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'entity_id', type: 'uuid' })
entityId: string;
@Column({ name: 'name', type: 'varchar', length: 100 })
name: string;
@Column({ name: 'display_name', type: 'varchar', length: 255 })
displayName: string;
@Column({ name: 'description', type: 'text', nullable: true })
description: string | null;
@Column({ name: 'data_type', type: 'varchar', length: 50 })
dataType: string;
@Column({ name: 'is_nullable', type: 'boolean', default: true })
isNullable: boolean;
@Column({ name: 'is_filterable', type: 'boolean', default: true })
isFilterable: boolean;
@Column({ name: 'is_sortable', type: 'boolean', default: true })
isSortable: boolean;
@Column({ name: 'is_groupable', type: 'boolean', default: false })
isGroupable: boolean;
@Column({ name: 'is_aggregatable', type: 'boolean', default: false })
isAggregatable: boolean;
@Column({ name: 'aggregation_functions', type: 'text', array: true, default: '{}' })
aggregationFunctions: string[];
@Column({ name: 'format_pattern', type: 'varchar', length: 100, nullable: true })
formatPattern: string | null;
@Column({ name: 'display_format', type: 'varchar', length: 50, nullable: true })
displayFormat: string | null;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'sort_order', type: 'int', default: 0 })
sortOrder: number;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
// Relations
@ManyToOne(() => DataModelEntity, (entity) => entity.fields, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'entity_id' })
entity: DataModelEntity;
}

View File

@ -0,0 +1,75 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { DataModelEntity } from './data-model-entity.entity';
/**
* Relationship type enum
*/
export enum RelationshipType {
ONE_TO_ONE = 'one_to_one',
ONE_TO_MANY = 'one_to_many',
MANY_TO_ONE = 'many_to_one',
MANY_TO_MANY = 'many_to_many',
}
/**
* Data Model Relationship Entity (schema: reports.data_model_relationships)
*
* Defines relationships between data model entities for join operations
* in the report builder.
*/
@Entity({ name: 'data_model_relationships', schema: 'reports' })
@Index(['sourceEntityId', 'targetEntityId', 'name'], { unique: true })
export class DataModelRelationship {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'source_entity_id', type: 'uuid' })
sourceEntityId: string;
@Index()
@Column({ name: 'target_entity_id', type: 'uuid' })
targetEntityId: string;
@Column({ name: 'name', type: 'varchar', length: 100 })
name: string;
@Column({
name: 'relationship_type',
type: 'varchar',
length: 20,
})
relationshipType: RelationshipType;
@Column({ name: 'source_column', type: 'varchar', length: 100 })
sourceColumn: string;
@Column({ name: 'target_column', type: 'varchar', length: 100 })
targetColumn: string;
@Column({ name: 'join_condition', type: 'text', nullable: true })
joinCondition: string | null;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
// Relations
@ManyToOne(() => DataModelEntity, (entity) => entity.sourceRelationships, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'source_entity_id' })
sourceEntity: DataModelEntity;
@ManyToOne(() => DataModelEntity, (entity) => entity.targetRelationships, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'target_entity_id' })
targetEntity: DataModelEntity;
}

View File

@ -0,0 +1,21 @@
/**
* Reports Module - Entity Exports
* MAI-006: Reportes y Analytics
*/
// Existing construction entities
export * from './report.entity';
export * from './report-execution.entity';
export * from './dashboard.entity';
export * from './dashboard-widget.entity';
export * from './kpi-snapshot.entity';
// Core report entities (from erp-core)
export * from './report-schedule.entity';
export * from './report-recipient.entity';
export * from './schedule-execution.entity';
export * from './widget-query.entity';
export * from './custom-report.entity';
export * from './data-model-entity.entity';
export * from './data-model-field.entity';
export * from './data-model-relationship.entity';

View File

@ -0,0 +1,220 @@
/**
* KpiSnapshot Entity
* Snapshots históricos de KPIs para análisis
*
* @module Reports
* @table reports.kpi_snapshots
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
JoinColumn,
Index,
} from 'typeorm';
import { Tenant } from '../../core/entities/tenant.entity';
export type KpiCategory =
| 'financial'
| 'progress'
| 'quality'
| 'hse'
| 'hr'
| 'inventory'
| 'sales'
| 'operational';
export type KpiPeriodType = 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly';
export type TrendDirection = 'up' | 'down' | 'stable';
@Entity({ schema: 'reports', name: 'kpi_snapshots' })
@Index(['tenantId', 'kpiCode', 'snapshotDate'])
@Index(['tenantId'])
@Index(['kpiCode'])
@Index(['category'])
@Index(['snapshotDate'])
@Index(['fraccionamientoId'])
export class KpiSnapshot {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({
name: 'kpi_code',
type: 'varchar',
length: 50,
comment: 'Unique KPI identifier',
})
kpiCode: string;
@Column({
name: 'kpi_name',
type: 'varchar',
length: 200,
})
kpiName: string;
@Column({
type: 'varchar',
length: 30,
})
category: KpiCategory;
@Column({
name: 'snapshot_date',
type: 'date',
})
snapshotDate: Date;
@Column({
name: 'period_type',
type: 'varchar',
length: 20,
default: 'daily',
})
periodType: KpiPeriodType;
@Column({
name: 'period_start',
type: 'date',
nullable: true,
})
periodStart: Date | null;
@Column({
name: 'period_end',
type: 'date',
nullable: true,
})
periodEnd: Date | null;
@Column({
name: 'fraccionamiento_id',
type: 'uuid',
nullable: true,
comment: 'Project-specific KPI, null for global',
})
fraccionamientoId: string | null;
@Column({
type: 'decimal',
precision: 18,
scale: 4,
})
value: number;
@Column({
name: 'previous_value',
type: 'decimal',
precision: 18,
scale: 4,
nullable: true,
})
previousValue: number | null;
@Column({
name: 'target_value',
type: 'decimal',
precision: 18,
scale: 4,
nullable: true,
})
targetValue: number | null;
@Column({
name: 'min_value',
type: 'decimal',
precision: 18,
scale: 4,
nullable: true,
comment: 'Minimum acceptable value',
})
minValue: number | null;
@Column({
name: 'max_value',
type: 'decimal',
precision: 18,
scale: 4,
nullable: true,
comment: 'Maximum acceptable value',
})
maxValue: number | null;
@Column({
type: 'varchar',
length: 20,
nullable: true,
comment: 'Unit of measurement',
})
unit: string | null;
@Column({
name: 'change_percentage',
type: 'decimal',
precision: 8,
scale: 2,
nullable: true,
comment: 'Percentage change from previous',
})
changePercentage: number | null;
@Column({
name: 'trend_direction',
type: 'varchar',
length: 10,
nullable: true,
})
trendDirection: TrendDirection | null;
@Column({
name: 'is_on_target',
type: 'boolean',
nullable: true,
})
isOnTarget: boolean | null;
@Column({
name: 'status_color',
type: 'varchar',
length: 20,
nullable: true,
comment: 'green, yellow, red based on thresholds',
})
statusColor: string | null;
@Column({
type: 'jsonb',
nullable: true,
comment: 'Additional breakdown data',
})
breakdown: Record<string, any> | null;
@Column({
type: 'jsonb',
nullable: true,
comment: 'Source data references',
})
metadata: Record<string, any> | null;
@Column({
name: 'calculated_at',
type: 'timestamptz',
default: () => 'CURRENT_TIMESTAMP',
})
calculatedAt: Date;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
}

View File

@ -0,0 +1,191 @@
/**
* ReportExecution Entity
* Historial de ejecuciones de reportes
*
* @module Reports
* @table reports.report_executions
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
JoinColumn,
Index,
} from 'typeorm';
import { Tenant } from '../../core/entities/tenant.entity';
import { User } from '../../core/entities/user.entity';
import { Report, ReportFormat } from './report.entity';
export type ExecutionStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
@Entity({ schema: 'reports', name: 'report_executions' })
@Index(['tenantId'])
@Index(['reportId'])
@Index(['status'])
@Index(['executedAt'])
@Index(['executedById'])
export class ReportExecution {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'report_id', type: 'uuid' })
reportId: string;
@Column({
type: 'varchar',
length: 20,
default: 'pending',
})
status: ExecutionStatus;
@Column({
type: 'varchar',
length: 20,
})
format: ReportFormat;
@Column({
type: 'jsonb',
nullable: true,
comment: 'Parameters used for this execution',
})
parameters: Record<string, any> | null;
@Column({
type: 'jsonb',
nullable: true,
comment: 'Filters applied',
})
filters: Record<string, any> | null;
@Column({
name: 'started_at',
type: 'timestamptz',
nullable: true,
})
startedAt: Date | null;
@Column({
name: 'completed_at',
type: 'timestamptz',
nullable: true,
})
completedAt: Date | null;
@Column({
name: 'duration_ms',
type: 'integer',
nullable: true,
comment: 'Execution duration in milliseconds',
})
durationMs: number | null;
@Column({
name: 'row_count',
type: 'integer',
nullable: true,
})
rowCount: number | null;
@Column({
name: 'file_path',
type: 'varchar',
length: 500,
nullable: true,
})
filePath: string | null;
@Column({
name: 'file_size',
type: 'integer',
nullable: true,
comment: 'File size in bytes',
})
fileSize: number | null;
@Column({
name: 'file_url',
type: 'varchar',
length: 1000,
nullable: true,
comment: 'Presigned URL or download URL',
})
fileUrl: string | null;
@Column({
name: 'url_expires_at',
type: 'timestamptz',
nullable: true,
})
urlExpiresAt: Date | null;
@Column({
name: 'error_message',
type: 'text',
nullable: true,
})
errorMessage: string | null;
@Column({
name: 'error_stack',
type: 'text',
nullable: true,
})
errorStack: string | null;
@Column({
name: 'is_scheduled',
type: 'boolean',
default: false,
comment: 'Was this a scheduled execution?',
})
isScheduled: boolean;
@Column({
name: 'distributed_to',
type: 'varchar',
array: true,
nullable: true,
comment: 'Email addresses report was sent to',
})
distributedTo: string[] | null;
@Column({
name: 'distributed_at',
type: 'timestamptz',
nullable: true,
})
distributedAt: Date | null;
@Column({
name: 'executed_at',
type: 'timestamptz',
default: () => 'CURRENT_TIMESTAMP',
})
executedAt: Date;
@Column({ name: 'executed_by', type: 'uuid', nullable: true })
executedById: string | null;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@ManyToOne(() => Report)
@JoinColumn({ name: 'report_id' })
report: Report;
@ManyToOne(() => User)
@JoinColumn({ name: 'executed_by' })
executedBy: User | null;
}

View File

@ -0,0 +1,47 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { ReportSchedule } from './report-schedule.entity';
/**
* Report Recipient Entity (schema: reports.report_recipients)
*
* Stores recipients for scheduled reports. Can reference internal users
* or external email addresses.
*/
@Entity({ name: 'report_recipients', schema: 'reports' })
export class ReportRecipient {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'schedule_id', type: 'uuid' })
scheduleId: string;
@Index()
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string | null;
@Column({ name: 'email', type: 'varchar', length: 255, nullable: true })
email: string | null;
@Column({ name: 'name', type: 'varchar', length: 255, nullable: true })
name: string | null;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
// Relations
@ManyToOne(() => ReportSchedule, (schedule) => schedule.recipients, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'schedule_id' })
schedule: ReportSchedule;
}

View File

@ -0,0 +1,142 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
OneToMany,
} from 'typeorm';
import { Report } from './report.entity';
import { ReportRecipient } from './report-recipient.entity';
import { ScheduleExecution } from './schedule-execution.entity';
/**
* Delivery method enum
*/
export enum DeliveryMethod {
NONE = 'none',
EMAIL = 'email',
STORAGE = 'storage',
WEBHOOK = 'webhook',
}
/**
* Schedule execution status enum
*/
export enum ScheduleExecutionStatus {
PENDING = 'pending',
RUNNING = 'running',
COMPLETED = 'completed',
FAILED = 'failed',
CANCELLED = 'cancelled',
}
/**
* Schedule export format enum
*/
export enum ScheduleExportFormat {
PDF = 'pdf',
EXCEL = 'excel',
CSV = 'csv',
JSON = 'json',
HTML = 'html',
}
/**
* Report Schedule Entity (schema: reports.report_schedules)
*
* Configures scheduled report execution with cron expressions,
* delivery methods, and default parameters.
*/
@Entity({ name: 'report_schedules', schema: 'reports' })
export class ReportSchedule {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'report_definition_id', type: 'uuid' })
reportDefinitionId: string;
@Column({ name: 'name', type: 'varchar', length: 255 })
name: string;
@Column({ name: 'cron_expression', type: 'varchar', length: 100 })
cronExpression: string;
@Column({ name: 'timezone', type: 'varchar', length: 100, default: 'America/Mexico_City' })
timezone: string;
@Column({ name: 'parameters', type: 'jsonb', default: '{}' })
parameters: Record<string, any>;
@Column({
name: 'delivery_method',
type: 'enum',
enum: DeliveryMethod,
enumName: 'delivery_method',
default: DeliveryMethod.EMAIL,
})
deliveryMethod: DeliveryMethod;
@Column({ name: 'delivery_config', type: 'jsonb', default: '{}' })
deliveryConfig: Record<string, any>;
@Column({
name: 'export_format',
type: 'enum',
enum: ScheduleExportFormat,
enumName: 'schedule_export_format',
default: ScheduleExportFormat.PDF,
})
exportFormat: ScheduleExportFormat;
@Index()
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'last_run_at', type: 'timestamptz', nullable: true })
lastRunAt: Date | null;
@Column({
name: 'last_run_status',
type: 'enum',
enum: ScheduleExecutionStatus,
enumName: 'schedule_execution_status',
nullable: true,
})
lastRunStatus: ScheduleExecutionStatus | null;
@Index()
@Column({ name: 'next_run_at', type: 'timestamptz', nullable: true })
nextRunAt: Date | null;
@Column({ name: 'run_count', type: 'int', default: 0 })
runCount: 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;
// Relations
@ManyToOne(() => Report, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'report_definition_id' })
reportDefinition: Report;
@OneToMany(() => ReportRecipient, (recipient) => recipient.schedule)
recipients: ReportRecipient[];
@OneToMany(() => ScheduleExecution, (scheduleExec) => scheduleExec.schedule)
scheduleExecutions: ScheduleExecution[];
}

View File

@ -0,0 +1,222 @@
/**
* Report Entity
* Definición de reportes configurables
*
* @module Reports
* @table reports.report_definitions
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany,
JoinColumn,
Index,
} from 'typeorm';
import { Tenant } from '../../core/entities/tenant.entity';
import { User } from '../../core/entities/user.entity';
import { ReportExecution } from './report-execution.entity';
export type ReportType =
| 'financial'
| 'progress'
| 'quality'
| 'hse'
| 'hr'
| 'inventory'
| 'contracts'
| 'executive'
| 'custom';
export type ReportFormat = 'pdf' | 'excel' | 'csv' | 'html' | 'json';
export type ReportFrequency = 'once' | 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly';
@Entity({ schema: 'reports', name: 'report_definitions' })
@Index(['tenantId', 'code'], { unique: true })
@Index(['tenantId'])
@Index(['reportType'])
@Index(['isActive'])
export class Report {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ type: 'varchar', length: 50 })
code: string;
@Column({ type: 'varchar', length: 200 })
name: string;
@Column({ type: 'text', nullable: true })
description: string | null;
@Column({
name: 'report_type',
type: 'varchar',
length: 30,
})
reportType: ReportType;
@Column({
name: 'default_format',
type: 'varchar',
length: 20,
default: 'pdf',
})
defaultFormat: ReportFormat;
@Column({
name: 'available_formats',
type: 'varchar',
array: true,
default: ['pdf', 'excel'],
})
availableFormats: ReportFormat[];
@Column({
type: 'jsonb',
nullable: true,
comment: 'SQL query or data source configuration',
})
query: Record<string, any> | null;
@Column({
type: 'jsonb',
nullable: true,
comment: 'Report parameters definition',
})
parameters: Record<string, any> | null;
@Column({
type: 'jsonb',
nullable: true,
comment: 'Column/field definitions',
})
columns: Record<string, any>[] | null;
@Column({
type: 'jsonb',
nullable: true,
comment: 'Grouping and aggregation config',
})
grouping: Record<string, any> | null;
@Column({
type: 'jsonb',
nullable: true,
comment: 'Sorting configuration',
})
sorting: Record<string, any>[] | null;
@Column({
type: 'jsonb',
nullable: true,
comment: 'Default filters',
})
filters: Record<string, any> | null;
@Column({
name: 'template_path',
type: 'varchar',
length: 500,
nullable: true,
})
templatePath: string | null;
@Column({
name: 'is_scheduled',
type: 'boolean',
default: false,
})
isScheduled: boolean;
@Column({
type: 'varchar',
length: 20,
nullable: true,
})
frequency: ReportFrequency | null;
@Column({
name: 'schedule_config',
type: 'jsonb',
nullable: true,
comment: 'Cron or schedule configuration',
})
scheduleConfig: Record<string, any> | null;
@Column({
name: 'distribution_list',
type: 'varchar',
array: true,
nullable: true,
comment: 'Email addresses for distribution',
})
distributionList: string[] | null;
@Column({
name: 'is_active',
type: 'boolean',
default: true,
})
isActive: boolean;
@Column({
name: 'is_system',
type: 'boolean',
default: false,
comment: 'System report cannot be deleted',
})
isSystem: boolean;
@Column({
name: 'execution_count',
type: 'integer',
default: 0,
})
executionCount: number;
@Column({
name: 'last_executed_at',
type: 'timestamptz',
nullable: true,
})
lastExecutedAt: Date | null;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdById: string | null;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true })
updatedAt: Date | null;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedById: string | null;
@Column({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date | null;
@Column({ name: 'deleted_by', type: 'uuid', nullable: true })
deletedById: string | null;
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@ManyToOne(() => User)
@JoinColumn({ name: 'created_by' })
createdBy: User | null;
@OneToMany(() => ReportExecution, (e) => e.report)
executions: ReportExecution[];
}

View File

@ -0,0 +1,59 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { ReportSchedule, ScheduleExecutionStatus } from './report-schedule.entity';
import { ReportExecution } from './report-execution.entity';
/**
* Schedule Execution Entity (schema: reports.schedule_executions)
*
* Links scheduled reports to their actual executions,
* tracking delivery status and recipient notifications.
*/
@Entity({ name: 'schedule_executions', schema: 'reports' })
export class ScheduleExecution {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'schedule_id', type: 'uuid' })
scheduleId: string;
@Column({ name: 'execution_id', type: 'uuid', nullable: true })
executionId: string | null;
@Column({
name: 'status',
type: 'enum',
enum: ScheduleExecutionStatus,
enumName: 'schedule_execution_status',
})
status: ScheduleExecutionStatus;
@Column({ name: 'recipients_notified', type: 'int', default: 0 })
recipientsNotified: number;
@Column({ name: 'delivery_status', type: 'jsonb', default: '{}' })
deliveryStatus: Record<string, any>;
@Column({ name: 'error_message', type: 'text', nullable: true })
errorMessage: string | null;
@Index()
@Column({ name: 'executed_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
executedAt: Date;
// Relations
@ManyToOne(() => ReportSchedule, (schedule) => schedule.scheduleExecutions, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'schedule_id' })
schedule: ReportSchedule;
@ManyToOne(() => ReportExecution, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'execution_id' })
execution: ReportExecution | null;
}

View File

@ -0,0 +1,59 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { DashboardWidget } from './dashboard-widget.entity';
/**
* Widget Query Entity (schema: reports.widget_queries)
*
* Data source queries for dashboard widgets.
* Supports both raw SQL and function-based queries with caching.
*/
@Entity({ name: 'widget_queries', schema: 'reports' })
export class WidgetQuery {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'widget_id', type: 'uuid' })
widgetId: string;
@Column({ name: 'name', type: 'varchar', length: 100 })
name: string;
@Column({ name: 'query_text', type: 'text', nullable: true })
queryText: string | null;
@Column({ name: 'query_function', type: 'varchar', length: 255, nullable: true })
queryFunction: string | null;
@Column({ name: 'parameters', type: 'jsonb', default: '{}' })
parameters: Record<string, any>;
@Column({ name: 'result_mapping', type: 'jsonb', default: '{}' })
resultMapping: Record<string, any>;
@Column({ name: 'cache_ttl_seconds', type: 'int', nullable: true, default: 300 })
cacheTtlSeconds: number | null;
@Column({ name: 'last_cached_at', type: 'timestamptz', nullable: true })
lastCachedAt: Date | null;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
// Relations
@ManyToOne(() => DashboardWidget, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'widget_id' })
widget: DashboardWidget;
}

View File

@ -0,0 +1,4 @@
export { Quotation } from './quotation.entity';
export { QuotationItem } from './quotation-item.entity';
export { SalesOrder } from './sales-order.entity';
export { SalesOrderItem } from './sales-order-item.entity';

View File

@ -0,0 +1,65 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
import { Quotation } from './quotation.entity';
@Entity({ name: 'quotation_items', schema: 'sales' })
export class QuotationItem {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'quotation_id', type: 'uuid' })
quotationId: string;
@ManyToOne(() => Quotation, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'quotation_id' })
quotation: Quotation;
@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;
@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({ 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;
}

View File

@ -0,0 +1,101 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, DeleteDateColumn, Index } from 'typeorm';
@Entity({ name: 'quotations', schema: 'sales' })
export class Quotation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'quotation_number', type: 'varchar', length: 30 })
quotationNumber: string;
@Index()
@Column({ name: 'partner_id', type: 'uuid' })
partnerId: string;
@Column({ name: 'partner_name', type: 'varchar', length: 200, nullable: true })
partnerName: string;
@Column({ name: 'partner_email', type: 'varchar', length: 255, nullable: true })
partnerEmail: string;
@Column({ name: 'billing_address', type: 'jsonb', nullable: true })
billingAddress: object;
@Column({ name: 'shipping_address', type: 'jsonb', nullable: true })
shippingAddress: object;
@Column({ name: 'quotation_date', type: 'date', default: () => 'CURRENT_DATE' })
quotationDate: Date;
@Column({ name: 'valid_until', type: 'date', nullable: true })
validUntil: Date;
@Column({ name: 'expected_close_date', type: 'date', nullable: true })
expectedCloseDate: Date;
@Column({ name: 'sales_rep_id', type: 'uuid', nullable: true })
salesRepId: string;
@Column({ type: 'varchar', length: 3, default: 'MXN' })
currency: string;
@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: '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: 'payment_term_days', type: 'int', default: 0 })
paymentTermDays: number;
@Column({ name: 'payment_method', type: 'varchar', length: 50, nullable: true })
paymentMethod: string;
@Index()
@Column({ type: 'varchar', length: 20, default: 'draft' })
status: 'draft' | 'sent' | 'accepted' | 'rejected' | 'expired' | 'converted';
@Column({ name: 'converted_to_order', type: 'boolean', default: false })
convertedToOrder: boolean;
@Column({ name: 'order_id', type: 'uuid', nullable: true })
orderId: string;
@Column({ name: 'converted_at', type: 'timestamptz', nullable: true })
convertedAt: Date;
@Column({ type: 'text', nullable: true })
notes: string;
@Column({ name: 'internal_notes', type: 'text', nullable: true })
internalNotes: string;
@Column({ name: 'terms_and_conditions', type: 'text', nullable: true })
termsAndConditions: 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;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedBy?: string;
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date;
}

View File

@ -0,0 +1,90 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
import { SalesOrder } from './sales-order.entity';
@Entity({ name: 'sales_order_items', schema: 'sales' })
export class SalesOrderItem {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'order_id', type: 'uuid' })
orderId: string;
@ManyToOne(() => SalesOrder, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'order_id' })
order: SalesOrder;
@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;
@Column({ type: 'decimal', precision: 15, scale: 4, default: 1 })
quantity: number;
@Column({ name: 'quantity_reserved', type: 'decimal', precision: 15, scale: 4, default: 0 })
quantityReserved: number;
@Column({ name: 'quantity_shipped', type: 'decimal', precision: 15, scale: 4, default: 0 })
quantityShipped: number;
@Column({ name: 'quantity_delivered', type: 'decimal', precision: 15, scale: 4, default: 0 })
quantityDelivered: number;
@Column({ name: 'quantity_returned', type: 'decimal', precision: 15, scale: 4, default: 0 })
quantityReturned: 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: 'unit_cost', type: 'decimal', precision: 15, scale: 4, default: 0 })
unitCost: 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({ type: 'decimal', precision: 15, scale: 2, default: 0 })
subtotal: number;
@Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
total: number;
@Column({ name: 'lot_number', type: 'varchar', length: 50, nullable: true })
lotNumber?: string;
@Column({ name: 'serial_number', type: 'varchar', length: 50, nullable: true })
serialNumber?: string;
@Index()
@Column({ type: 'varchar', length: 20, default: 'pending' })
status: 'pending' | 'reserved' | 'shipped' | 'delivered' | 'cancelled';
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,138 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, DeleteDateColumn, Index } from 'typeorm';
/**
* Sales Order Entity
*
* Aligned with SQL schema used by orders.service.ts
* Supports full Order-to-Cash flow with:
* - PaymentTerms integration
* - Automatic picking creation
* - Stock reservation
* - Invoice and delivery status tracking
*/
@Entity({ name: 'sales_orders', schema: 'sales' })
export class SalesOrder {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'company_id', type: 'uuid' })
companyId: string;
// Order identification
@Index()
@Column({ type: 'varchar', length: 30 })
name: string; // Order number (e.g., SO-000001)
@Column({ name: 'client_order_ref', type: 'varchar', length: 100, nullable: true })
clientOrderRef: string | null; // Customer's reference number
@Column({ name: 'quotation_id', type: 'uuid', nullable: true })
quotationId: string | null;
// Partner/Customer
@Index()
@Column({ name: 'partner_id', type: 'uuid' })
partnerId: string;
// Dates
@Column({ name: 'order_date', type: 'date', default: () => 'CURRENT_DATE' })
orderDate: Date;
@Column({ name: 'validity_date', type: 'date', nullable: true })
validityDate: Date | null;
@Column({ name: 'commitment_date', type: 'date', nullable: true })
commitmentDate: Date | null; // Promised delivery date
// Currency and pricing
@Index()
@Column({ name: 'currency_id', type: 'uuid' })
currencyId: string;
@Column({ name: 'pricelist_id', type: 'uuid', nullable: true })
pricelistId: string | null;
// Payment terms integration (TASK-003-01)
@Index()
@Column({ name: 'payment_term_id', type: 'uuid', nullable: true })
paymentTermId: string | null;
// Sales team
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string | null; // Sales representative
@Column({ name: 'sales_team_id', type: 'uuid', nullable: true })
salesTeamId: string | null;
// Amounts
@Column({ name: 'amount_untaxed', type: 'decimal', precision: 15, scale: 2, default: 0 })
amountUntaxed: number;
@Column({ name: 'amount_tax', type: 'decimal', precision: 15, scale: 2, default: 0 })
amountTax: number;
@Column({ name: 'amount_total', type: 'decimal', precision: 15, scale: 2, default: 0 })
amountTotal: number;
// Status fields (Order-to-Cash tracking)
@Index()
@Column({ type: 'varchar', length: 20, default: 'draft' })
status: 'draft' | 'sent' | 'sale' | 'done' | 'cancelled';
@Index()
@Column({ name: 'invoice_status', type: 'varchar', length: 20, default: 'pending' })
invoiceStatus: 'pending' | 'partial' | 'invoiced';
@Index()
@Column({ name: 'delivery_status', type: 'varchar', length: 20, default: 'pending' })
deliveryStatus: 'pending' | 'partial' | 'delivered';
@Column({ name: 'invoice_policy', type: 'varchar', length: 20, default: 'order' })
invoicePolicy: 'order' | 'delivery';
// Delivery/Picking integration (TASK-003-03)
@Column({ name: 'picking_id', type: 'uuid', nullable: true })
pickingId: string | null;
// Notes
@Column({ type: 'text', nullable: true })
notes: string | null;
@Column({ name: 'terms_conditions', type: 'text', nullable: true })
termsConditions: string | null;
// Confirmation tracking
@Column({ name: 'confirmed_at', type: 'timestamptz', nullable: true })
confirmedAt: Date | null;
@Column({ name: 'confirmed_by', type: 'uuid', nullable: true })
confirmedBy: string | null;
// Cancellation tracking
@Column({ name: 'cancelled_at', type: 'timestamptz', nullable: true })
cancelledAt: Date | null;
@Column({ name: 'cancelled_by', type: 'uuid', nullable: true })
cancelledBy: string | null;
// Audit fields
@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;
}

View File

@ -0,0 +1,8 @@
/**
* Settings Entities - Export
*/
export { SystemSetting } from './system-setting.entity';
export { PlanSetting } from './plan-setting.entity';
export { TenantSetting } from './tenant-setting.entity';
export { UserPreference } from './user-preference.entity';

View File

@ -0,0 +1,42 @@
/**
* Plan Setting Entity
* Default configuration per subscription plan
* Hierarchy: system_settings < plan_settings < tenant_settings
* Compatible with erp-core plan-setting.entity
*
* @module Settings
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
Unique,
} from 'typeorm';
@Entity({ name: 'plan_settings', schema: 'core_settings' })
@Unique(['planId', 'key'])
export class PlanSetting {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'plan_id', type: 'uuid' })
planId: string;
@Index()
@Column({ name: 'key', type: 'varchar', length: 100 })
key: string;
@Column({ name: 'value', type: 'jsonb' })
value: any;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,67 @@
/**
* System Setting Entity
* Global system configuration settings across all tenants
* Compatible with erp-core system-setting.entity
*
* @module Settings
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
Unique,
} from 'typeorm';
@Entity({ name: 'system_settings', schema: 'core_settings' })
@Unique(['key'])
export class SystemSetting {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'key', type: 'varchar', length: 100 })
key: string;
@Column({ name: 'value', type: 'jsonb' })
value: any;
@Column({
name: 'data_type',
type: 'varchar',
length: 20,
default: 'string',
})
dataType: 'string' | 'number' | 'boolean' | 'json' | 'array' | 'secret';
@Index()
@Column({ name: 'category', type: 'varchar', length: 50 })
category: string;
@Column({ name: 'description', type: 'text', nullable: true })
description: string | null;
@Column({ name: 'is_public', type: 'boolean', default: false })
isPublic: boolean;
@Column({ name: 'is_editable', type: 'boolean', default: true })
isEditable: boolean;
@Column({ name: 'default_value', type: 'jsonb', nullable: true })
defaultValue: any;
@Column({ name: 'validation_rules', type: 'jsonb', default: '{}' })
validationRules: Record<string, any>;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedBy: string | null;
}

View File

@ -0,0 +1,53 @@
/**
* Tenant Setting Entity
* Custom configuration per tenant, overrides system and plan defaults
* Hierarchy: system_settings < plan_settings < tenant_settings
* Compatible with erp-core tenant-setting.entity
*
* @module Settings
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
Unique,
} from 'typeorm';
@Entity({ name: 'tenant_settings', schema: 'core_settings' })
@Unique(['tenantId', 'key'])
export class TenantSetting {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'key', type: 'varchar', length: 100 })
key: string;
@Column({ name: 'value', type: 'jsonb' })
value: any;
@Column({
name: 'inherited_from',
type: 'varchar',
length: 20,
default: 'custom',
})
inheritedFrom: 'system' | 'plan' | 'custom';
@Column({ name: 'is_overridden', type: 'boolean', default: true })
isOverridden: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,44 @@
/**
* User Preference Entity
* Personal preferences per user (theme, language, notifications, etc.)
* Compatible with erp-core user-preference.entity
*
* @module Settings
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
Unique,
} from 'typeorm';
@Entity({ name: 'user_preferences', schema: 'core_settings' })
@Unique(['userId', 'key'])
export class UserPreference {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'user_id', type: 'uuid' })
userId: string;
@Index()
@Column({ name: 'key', type: 'varchar', length: 100 })
key: string;
@Column({ name: 'value', type: 'jsonb' })
value: any;
@Column({ name: 'synced_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
syncedAt: Date;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,74 @@
/**
* StorageBucket Entity
* Storage bucket configuration with provider and quota settings
* Compatible with erp-core bucket.entity
*
* @module Storage
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
} from 'typeorm';
export type BucketType = 'public' | 'private' | 'protected';
export type StorageProvider = 'local' | 's3' | 'gcs' | 'azure';
@Entity({ name: 'buckets', schema: 'storage' })
export class StorageBucket {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index({ unique: true })
@Column({ name: 'name', type: 'varchar', length: 100 })
name: string;
@Column({ name: 'description', type: 'text', nullable: true })
description: string;
@Column({ name: 'bucket_type', type: 'varchar', length: 30, default: 'private' })
bucketType: BucketType;
@Column({ name: 'max_file_size_mb', type: 'int', default: 50 })
maxFileSizeMb: number;
@Column({ name: 'allowed_mime_types', type: 'text', array: true, default: [] })
allowedMimeTypes: string[];
@Column({ name: 'allowed_extensions', type: 'text', array: true, default: [] })
allowedExtensions: string[];
@Column({ name: 'auto_delete_days', type: 'int', nullable: true })
autoDeleteDays: number;
@Column({ name: 'versioning_enabled', type: 'boolean', default: false })
versioningEnabled: boolean;
@Column({ name: 'max_versions', type: 'int', default: 5 })
maxVersions: number;
@Column({ name: 'storage_provider', type: 'varchar', length: 30, default: 'local' })
storageProvider: StorageProvider;
@Column({ name: 'storage_config', type: 'jsonb', default: {} })
storageConfig: Record<string, any>;
@Column({ name: 'quota_per_tenant_gb', type: 'int', nullable: true })
quotaPerTenantGb: number;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'is_system', type: 'boolean', default: false })
isSystem: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,71 @@
/**
* FileAccessToken Entity
* Temporary access tokens for file downloads
* Compatible with erp-core file-access-token.entity
*
* @module Storage
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { StorageFile } from './file.entity';
@Entity({ name: 'file_access_tokens', schema: 'storage' })
export class FileAccessToken {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'file_id', type: 'uuid' })
fileId: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'token', type: 'varchar', length: 255, unique: true })
token: string;
@Column({ name: 'permissions', type: 'text', array: true, default: ['read'] })
permissions: string[];
@Column({ name: 'allowed_ips', type: 'inet', array: true, nullable: true })
allowedIps: string[];
@Column({ name: 'max_downloads', type: 'int', nullable: true })
maxDownloads: number;
@Column({ name: 'download_count', type: 'int', default: 0 })
downloadCount: number;
@Index()
@Column({ name: 'expires_at', type: 'timestamptz' })
expiresAt: Date;
@Column({ name: 'revoked_at', type: 'timestamptz', nullable: true })
revokedAt: Date;
@Column({ name: 'created_for', type: 'varchar', length: 255, nullable: true })
createdFor: string;
@Column({ name: 'purpose', type: 'text', nullable: true })
purpose: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdBy: string;
@ManyToOne(() => StorageFile, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'file_id' })
file: StorageFile;
}

View File

@ -0,0 +1,96 @@
/**
* FileShare Entity
* File sharing with granular permissions
* Compatible with erp-core file-share.entity
*
* @module Storage
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { StorageFile } from './file.entity';
@Entity({ name: 'file_shares', schema: 'storage' })
export class FileShare {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'file_id', type: 'uuid' })
fileId: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'shared_with_user_id', type: 'uuid', nullable: true })
sharedWithUserId: string;
@Column({ name: 'shared_with_email', type: 'varchar', length: 255, nullable: true })
sharedWithEmail: string;
@Column({ name: 'shared_with_role', type: 'varchar', length: 50, nullable: true })
sharedWithRole: string;
@Column({ name: 'can_view', type: 'boolean', default: true })
canView: boolean;
@Column({ name: 'can_download', type: 'boolean', default: true })
canDownload: boolean;
@Column({ name: 'can_edit', type: 'boolean', default: false })
canEdit: boolean;
@Column({ name: 'can_delete', type: 'boolean', default: false })
canDelete: boolean;
@Column({ name: 'can_share', type: 'boolean', default: false })
canShare: boolean;
@Index()
@Column({ name: 'public_link', type: 'varchar', length: 255, unique: true, nullable: true })
publicLink: string;
@Column({ name: 'public_link_password', type: 'varchar', length: 255, nullable: true })
publicLinkPassword: string;
@Column({ name: 'expires_at', type: 'timestamptz', nullable: true })
expiresAt: Date;
@Column({ name: 'revoked_at', type: 'timestamptz', nullable: true })
revokedAt: Date;
@Column({ name: 'view_count', type: 'int', default: 0 })
viewCount: number;
@Column({ name: 'download_count', type: 'int', default: 0 })
downloadCount: number;
@Column({ name: 'last_accessed_at', type: 'timestamptz', nullable: true })
lastAccessedAt: Date;
@Column({ name: 'notify_on_access', type: 'boolean', default: false })
notifyOnAccess: boolean;
@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;
@ManyToOne(() => StorageFile, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'file_id' })
file: StorageFile;
}

View File

@ -0,0 +1,162 @@
/**
* StorageFile Entity
* File metadata with versioning, checksums, and processing status
* Compatible with erp-core file.entity
*
* @module Storage
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
Unique,
} from 'typeorm';
import { StorageBucket } from './bucket.entity';
import { StorageFolder } from './folder.entity';
export type FileCategory = 'image' | 'document' | 'video' | 'audio' | 'archive' | 'other';
export type FileStatus = 'active' | 'processing' | 'archived' | 'deleted';
export type ProcessingStatus = 'pending' | 'processing' | 'completed' | 'failed';
@Entity({ name: 'files', schema: 'storage' })
@Unique(['tenantId', 'bucketId', 'path', 'version'])
export class StorageFile {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'bucket_id', type: 'uuid' })
bucketId: string;
@Index()
@Column({ name: 'folder_id', type: 'uuid', nullable: true })
folderId: string;
@Column({ name: 'name', type: 'varchar', length: 255 })
name: string;
@Column({ name: 'original_name', type: 'varchar', length: 255 })
originalName: string;
@Column({ name: 'path', type: 'text' })
path: string;
@Index()
@Column({ name: 'mime_type', type: 'varchar', length: 100 })
mimeType: string;
@Column({ name: 'extension', type: 'varchar', length: 20, nullable: true })
extension: string;
@Index()
@Column({ name: 'category', type: 'varchar', length: 30, nullable: true })
category: FileCategory;
@Column({ name: 'size_bytes', type: 'bigint' })
sizeBytes: number;
@Column({ name: 'checksum_md5', type: 'varchar', length: 32, nullable: true })
checksumMd5: string;
@Index()
@Column({ name: 'checksum_sha256', type: 'varchar', length: 64, nullable: true })
checksumSha256: string;
@Column({ name: 'storage_key', type: 'text' })
storageKey: string;
@Column({ name: 'storage_url', type: 'text', nullable: true })
storageUrl: string;
@Column({ name: 'cdn_url', type: 'text', nullable: true })
cdnUrl: string;
@Column({ name: 'width', type: 'int', nullable: true })
width: number;
@Column({ name: 'height', type: 'int', nullable: true })
height: number;
@Column({ name: 'thumbnail_url', type: 'text', nullable: true })
thumbnailUrl: string;
@Column({ name: 'thumbnails', type: 'jsonb', default: {} })
thumbnails: Record<string, string>;
@Column({ name: 'metadata', type: 'jsonb', default: {} })
metadata: Record<string, any>;
@Column({ name: 'tags', type: 'text', array: true, default: [] })
tags: string[];
@Column({ name: 'alt_text', type: 'text', nullable: true })
altText: string;
@Column({ name: 'version', type: 'int', default: 1 })
version: number;
@Column({ name: 'parent_version_id', type: 'uuid', nullable: true })
parentVersionId: string;
@Column({ name: 'is_latest', type: 'boolean', default: true })
isLatest: boolean;
@Index()
@Column({ name: 'entity_type', type: 'varchar', length: 100, nullable: true })
entityType: string;
@Column({ name: 'entity_id', type: 'uuid', nullable: true })
entityId: string;
@Column({ name: 'is_public', type: 'boolean', default: false })
isPublic: boolean;
@Column({ name: 'access_count', type: 'int', default: 0 })
accessCount: number;
@Column({ name: 'last_accessed_at', type: 'timestamptz', nullable: true })
lastAccessedAt: Date;
@Index()
@Column({ name: 'status', type: 'varchar', length: 20, default: 'active' })
status: FileStatus;
@Column({ name: 'archived_at', type: 'timestamptz', nullable: true })
archivedAt: Date;
@Column({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date;
@Column({ name: 'processing_status', type: 'varchar', length: 20, nullable: true })
processingStatus: ProcessingStatus;
@Column({ name: 'processing_error', type: 'text', nullable: true })
processingError: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'uploaded_by', type: 'uuid', nullable: true })
uploadedBy: string;
@ManyToOne(() => StorageBucket, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'bucket_id' })
bucket: StorageBucket;
@ManyToOne(() => StorageFolder, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'folder_id' })
folder: StorageFolder;
}

View File

@ -0,0 +1,91 @@
/**
* StorageFolder Entity
* Hierarchical folder structure within buckets
* Compatible with erp-core folder.entity
*
* @module Storage
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
OneToMany,
Unique,
} from 'typeorm';
import { StorageBucket } from './bucket.entity';
@Entity({ name: 'folders', schema: 'storage' })
@Unique(['tenantId', 'bucketId', 'path'])
export class StorageFolder {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'bucket_id', type: 'uuid' })
bucketId: string;
@Index()
@Column({ name: 'parent_id', type: 'uuid', nullable: true })
parentId: string;
@Index()
@Column({ name: 'path', type: 'text' })
path: string;
@Column({ name: 'name', type: 'varchar', length: 255 })
name: string;
@Column({ name: 'depth', type: 'int', default: 0 })
depth: number;
@Column({ name: 'description', type: 'text', nullable: true })
description: string;
@Column({ name: 'color', type: 'varchar', length: 7, nullable: true })
color: string;
@Column({ name: 'icon', type: 'varchar', length: 50, nullable: true })
icon: string;
@Column({ name: 'is_private', type: 'boolean', default: false })
isPrivate: boolean;
@Column({ name: 'owner_id', type: 'uuid', nullable: true })
ownerId: string;
@Column({ name: 'file_count', type: 'int', default: 0 })
fileCount: number;
@Column({ name: 'total_size_bytes', type: 'bigint', default: 0 })
totalSizeBytes: 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;
@ManyToOne(() => StorageBucket, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'bucket_id' })
bucket: StorageBucket;
@ManyToOne(() => StorageFolder, { nullable: true, onDelete: 'CASCADE' })
@JoinColumn({ name: 'parent_id' })
parent: StorageFolder;
@OneToMany(() => StorageFolder, (folder) => folder.parent)
children: StorageFolder[];
}

View File

@ -0,0 +1,11 @@
/**
* Storage Entities - Export
*/
export { StorageBucket, BucketType, StorageProvider } from './bucket.entity';
export { StorageFolder } from './folder.entity';
export { StorageFile, FileCategory, FileStatus, ProcessingStatus } from './file.entity';
export { FileAccessToken } from './file-access-token.entity';
export { StorageUpload, UploadStatus } from './upload.entity';
export { FileShare } from './file-share.entity';
export { TenantUsage } from './tenant-usage.entity';

View File

@ -0,0 +1,65 @@
/**
* TenantUsage Entity
* Per-tenant storage usage and quota tracking
* Compatible with erp-core tenant-usage.entity
*
* @module Storage
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
Unique,
} from 'typeorm';
import { StorageBucket } from './bucket.entity';
@Entity({ name: 'tenant_usage', schema: 'storage' })
@Unique(['tenantId', 'bucketId', 'monthYear'])
export class TenantUsage {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'bucket_id', type: 'uuid' })
bucketId: string;
@Column({ name: 'file_count', type: 'int', default: 0 })
fileCount: number;
@Column({ name: 'total_size_bytes', type: 'bigint', default: 0 })
totalSizeBytes: number;
@Column({ name: 'quota_bytes', type: 'bigint', nullable: true })
quotaBytes: number;
@Column({ name: 'quota_file_count', type: 'int', nullable: true })
quotaFileCount: number;
@Column({ name: 'usage_by_category', type: 'jsonb', default: {} })
usageByCategory: Record<string, any>;
@Column({ name: 'monthly_upload_bytes', type: 'bigint', default: 0 })
monthlyUploadBytes: number;
@Column({ name: 'monthly_download_bytes', type: 'bigint', default: 0 })
monthlyDownloadBytes: number;
@Column({ name: 'month_year', type: 'varchar', length: 7 })
monthYear: string;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@ManyToOne(() => StorageBucket, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'bucket_id' })
bucket: StorageBucket;
}

View File

@ -0,0 +1,110 @@
/**
* StorageUpload Entity
* Chunked upload tracking
* Compatible with erp-core upload.entity
*
* @module Storage
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { StorageBucket } from './bucket.entity';
import { StorageFolder } from './folder.entity';
import { StorageFile } from './file.entity';
export type UploadStatus = 'pending' | 'uploading' | 'processing' | 'completed' | 'failed' | 'cancelled';
@Entity({ name: 'uploads', schema: 'storage' })
export class StorageUpload {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'bucket_id', type: 'uuid' })
bucketId: string;
@Column({ name: 'folder_id', type: 'uuid', nullable: true })
folderId: string;
@Column({ name: 'file_name', type: 'varchar', length: 255 })
fileName: string;
@Column({ name: 'mime_type', type: 'varchar', length: 100, nullable: true })
mimeType: string;
@Column({ name: 'total_size_bytes', type: 'bigint', nullable: true })
totalSizeBytes: number;
@Index()
@Column({ name: 'status', type: 'varchar', length: 20, default: 'pending' })
status: UploadStatus;
@Column({ name: 'uploaded_bytes', type: 'bigint', default: 0 })
uploadedBytes: number;
@Column({ name: 'upload_progress', type: 'decimal', precision: 5, scale: 2, default: 0 })
uploadProgress: number;
@Column({ name: 'total_chunks', type: 'int', nullable: true })
totalChunks: number;
@Column({ name: 'completed_chunks', type: 'int', default: 0 })
completedChunks: number;
@Column({ name: 'chunk_size_bytes', type: 'int', nullable: true })
chunkSizeBytes: number;
@Column({ name: 'chunks_status', type: 'jsonb', default: {} })
chunksStatus: Record<string, any>;
@Column({ name: 'metadata', type: 'jsonb', default: {} })
metadata: Record<string, any>;
@Column({ name: 'file_id', type: 'uuid', nullable: true })
fileId: string;
@Column({ name: 'error_message', type: 'text', nullable: true })
errorMessage: string;
@Column({ name: 'started_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
startedAt: Date;
@Column({ name: 'last_chunk_at', type: 'timestamptz', nullable: true })
lastChunkAt: Date;
@Column({ name: 'completed_at', type: 'timestamptz', nullable: true })
completedAt: Date;
@Index()
@Column({ name: 'expires_at', type: 'timestamptz', nullable: true })
expiresAt: Date;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdBy: string;
@ManyToOne(() => StorageBucket, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'bucket_id' })
bucket: StorageBucket;
@ManyToOne(() => StorageFolder, { nullable: true })
@JoinColumn({ name: 'folder_id' })
folder: StorageFolder;
@ManyToOne(() => StorageFile, { nullable: true })
@JoinColumn({ name: 'file_id' })
file: StorageFile;
}

View File

@ -0,0 +1,3 @@
export { Warehouse } from './warehouse.entity';
export { WarehouseLocation } from './warehouse-location.entity';
export { WarehouseZone } from './warehouse-zone.entity';

View File

@ -0,0 +1,111 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Warehouse } from './warehouse.entity';
@Entity({ name: 'warehouse_locations', schema: 'inventory' })
export class WarehouseLocation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'warehouse_id', type: 'uuid' })
warehouseId: string;
@ManyToOne(() => Warehouse, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'warehouse_id' })
warehouse: Warehouse;
@Index()
@Column({ name: 'parent_id', type: 'uuid', nullable: true })
parentId: string;
@ManyToOne(() => WarehouseLocation, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'parent_id' })
parent: WarehouseLocation;
// Identificacion
@Index()
@Column({ type: 'varchar', length: 30 })
code: string;
@Column({ type: 'varchar', length: 100 })
name: string;
@Index()
@Column({ type: 'varchar', length: 50, nullable: true })
barcode: string;
// Tipo de ubicacion
@Index()
@Column({ name: 'location_type', type: 'varchar', length: 20, default: 'shelf' })
locationType: 'zone' | 'aisle' | 'rack' | 'shelf' | 'bin';
// Jerarquia
@Column({ name: 'hierarchy_path', type: 'text', nullable: true })
hierarchyPath: string;
@Column({ name: 'hierarchy_level', type: 'int', default: 0 })
hierarchyLevel: number;
// Coordenadas dentro del almacen
@Column({ type: 'varchar', length: 10, nullable: true })
aisle: string;
@Column({ type: 'varchar', length: 10, nullable: true })
rack: string;
@Column({ type: 'varchar', length: 10, nullable: true })
shelf: string;
@Column({ type: 'varchar', length: 10, nullable: true })
bin: string;
// Capacidad
@Column({ name: 'capacity_units', type: 'int', nullable: true })
capacityUnits: number;
@Column({ name: 'capacity_volume', type: 'decimal', precision: 10, scale: 4, nullable: true })
capacityVolume: number;
@Column({ name: 'capacity_weight', type: 'decimal', precision: 10, scale: 4, nullable: true })
capacityWeight: number;
// Restricciones
@Column({ name: 'allowed_product_types', type: 'text', array: true, default: '{}' })
allowedProductTypes: string[];
@Column({ name: 'temperature_range', type: 'jsonb', nullable: true })
temperatureRange: { min?: number; max?: number };
@Column({ name: 'humidity_range', type: 'jsonb', nullable: true })
humidityRange: { min?: number; max?: number };
// Estado
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'is_pickable', type: 'boolean', default: true })
isPickable: boolean;
@Column({ name: 'is_receivable', type: 'boolean', default: true })
isReceivable: boolean;
// Metadata
@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;
}

View File

@ -0,0 +1,41 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
import { Warehouse } from './warehouse.entity';
@Entity({ name: 'warehouse_zones', schema: 'inventory' })
export class WarehouseZone {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'warehouse_id', type: 'uuid' })
warehouseId: string;
@ManyToOne(() => Warehouse, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'warehouse_id' })
warehouse: Warehouse;
@Column({ type: 'varchar', length: 20 })
code: string;
@Column({ type: 'varchar', length: 100 })
name: string;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ type: 'varchar', length: 20, nullable: true })
color?: string;
@Index()
@Column({ name: 'zone_type', type: 'varchar', length: 20, default: 'storage' })
zoneType: 'storage' | 'picking' | 'packing' | 'shipping' | 'receiving' | 'quarantine';
@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;
}

View File

@ -0,0 +1,138 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Company } from '../../auth/entities/company.entity';
/**
* Warehouse Entity (schema: inventory.warehouses)
*
* This is the CANONICAL warehouse entity for the ERP system.
* All warehouse-related imports should use this entity.
*
* Note: The deprecated entity at inventory/entities/warehouse.entity.ts
* has been superseded by this one and should not be used for new code.
*/
@Entity({ name: 'warehouses', schema: 'inventory' })
@Index('idx_warehouses_tenant_id', ['tenantId'])
@Index('idx_warehouses_company_id', ['companyId'])
@Index('idx_warehouses_code_company', ['companyId', 'code'], { unique: true })
export class Warehouse {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'company_id', type: 'uuid', nullable: true })
companyId: string | null;
@ManyToOne(() => Company)
@JoinColumn({ name: 'company_id' })
company: Company;
@Index()
@Column({ name: 'branch_id', type: 'uuid', nullable: true })
branchId: string;
// Identificacion
@Index()
@Column({ type: 'varchar', length: 20 })
code: string;
@Column({ type: 'varchar', length: 100 })
name: string;
@Column({ type: 'text', nullable: true })
description: string;
// Tipo
@Index()
@Column({ name: 'warehouse_type', type: 'varchar', length: 20, default: 'standard' })
warehouseType: 'standard' | 'transit' | 'returns' | 'quarantine' | 'virtual';
// Direccion
@Column({ name: 'address_line1', type: 'varchar', length: 200, nullable: true })
addressLine1: string;
@Column({ name: 'address_line2', type: 'varchar', length: 200, nullable: true })
addressLine2: string;
@Column({ type: 'varchar', length: 100, nullable: true })
city: string;
@Column({ type: 'varchar', length: 100, nullable: true })
state: string;
@Column({ name: 'postal_code', type: 'varchar', length: 20, nullable: true })
postalCode: string;
@Column({ type: 'varchar', length: 3, default: 'MEX' })
country: string;
// Contacto
@Column({ name: 'manager_name', type: 'varchar', length: 100, nullable: true })
managerName: string;
@Column({ type: 'varchar', length: 30, nullable: true })
phone: string;
@Column({ type: 'varchar', length: 255, nullable: true })
email: string;
// Geolocalizacion
@Column({ type: 'decimal', precision: 10, scale: 8, nullable: true })
latitude: number;
@Column({ type: 'decimal', precision: 11, scale: 8, nullable: true })
longitude: number;
// Capacidad
@Column({ name: 'capacity_units', type: 'int', nullable: true })
capacityUnits: number;
@Column({ name: 'capacity_volume', type: 'decimal', precision: 10, scale: 4, nullable: true })
capacityVolume: number;
@Column({ name: 'capacity_weight', type: 'decimal', precision: 10, scale: 4, nullable: true })
capacityWeight: number;
// Configuracion
@Column({ type: 'jsonb', default: {} })
settings: {
allowNegative?: boolean;
autoReorder?: boolean;
};
// Estado
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'is_default', type: 'boolean', default: false })
isDefault: boolean;
// Metadata
@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;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedBy: string;
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date;
}

View File

@ -0,0 +1,105 @@
/**
* WebhookDelivery Entity
* Delivery tracking with retry logic
* Compatible with erp-core delivery.entity
*
* @module Webhooks
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { WebhookEndpoint } from './endpoint.entity';
export type DeliveryStatus = 'pending' | 'sending' | 'delivered' | 'failed' | 'retrying' | 'cancelled';
@Entity({ name: 'deliveries', schema: 'webhooks' })
export class WebhookDelivery {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'endpoint_id', type: 'uuid' })
endpointId: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'event_type', type: 'varchar', length: 100 })
eventType: string;
@Column({ name: 'event_id', type: 'uuid' })
eventId: string;
@Column({ name: 'payload', type: 'jsonb' })
payload: Record<string, any>;
@Column({ name: 'payload_hash', type: 'varchar', length: 64, nullable: true })
payloadHash: string;
@Column({ name: 'request_url', type: 'text' })
requestUrl: string;
@Column({ name: 'request_method', type: 'varchar', length: 10 })
requestMethod: string;
@Column({ name: 'request_headers', type: 'jsonb', default: {} })
requestHeaders: Record<string, string>;
@Column({ name: 'response_status', type: 'int', nullable: true })
responseStatus: number;
@Column({ name: 'response_headers', type: 'jsonb', default: {} })
responseHeaders: Record<string, string>;
@Column({ name: 'response_body', type: 'text', nullable: true })
responseBody: string;
@Column({ name: 'response_time_ms', type: 'int', nullable: true })
responseTimeMs: number;
@Index()
@Column({ name: 'status', type: 'varchar', length: 20, default: 'pending' })
status: DeliveryStatus;
@Column({ name: 'attempt_number', type: 'int', default: 1 })
attemptNumber: number;
@Column({ name: 'max_attempts', type: 'int', default: 5 })
maxAttempts: number;
@Index()
@Column({ name: 'next_retry_at', type: 'timestamptz', nullable: true })
nextRetryAt: Date;
@Column({ name: 'error_message', type: 'text', nullable: true })
errorMessage: string;
@Column({ name: 'error_code', type: 'varchar', length: 50, nullable: true })
errorCode: string;
@Column({ name: 'scheduled_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
scheduledAt: Date;
@Column({ name: 'started_at', type: 'timestamptz', nullable: true })
startedAt: Date;
@Column({ name: 'completed_at', type: 'timestamptz', nullable: true })
completedAt: Date;
@Index()
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@ManyToOne(() => WebhookEndpoint, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'endpoint_id' })
endpoint: WebhookEndpoint;
}

View File

@ -0,0 +1,54 @@
/**
* WebhookEndpointLog Entity
* Activity log for webhook endpoints
* Compatible with erp-core endpoint-log.entity
*
* @module Webhooks
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { WebhookEndpoint } from './endpoint.entity';
export type WebhookLogType = 'config_changed' | 'activated' | 'deactivated' | 'verified' | 'error' | 'rate_limited' | 'created';
@Entity({ name: 'endpoint_logs', schema: 'webhooks' })
export class WebhookEndpointLog {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'endpoint_id', type: 'uuid' })
endpointId: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'log_type', type: 'varchar', length: 30 })
logType: WebhookLogType;
@Column({ name: 'message', type: 'text', nullable: true })
message: string;
@Column({ name: 'details', type: 'jsonb', default: {} })
details: Record<string, any>;
@Column({ name: 'actor_id', type: 'uuid', nullable: true })
actorId: string;
@Index()
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@ManyToOne(() => WebhookEndpoint, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'endpoint_id' })
endpoint: WebhookEndpoint;
}

View File

@ -0,0 +1,118 @@
/**
* WebhookEndpoint Entity
* Outbound webhook endpoint configuration with retry and rate limiting
* Compatible with erp-core endpoint.entity
*
* @module Webhooks
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
Unique,
} from 'typeorm';
export type AuthType = 'none' | 'basic' | 'bearer' | 'hmac' | 'oauth2';
@Entity({ name: 'endpoints', schema: 'webhooks' })
@Unique(['tenantId', 'url'])
export class WebhookEndpoint {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'name', type: 'varchar', length: 200 })
name: string;
@Column({ name: 'description', type: 'text', nullable: true })
description: string;
@Column({ name: 'url', type: 'text' })
url: string;
@Column({ name: 'http_method', type: 'varchar', length: 10, default: 'POST' })
httpMethod: string;
@Column({ name: 'auth_type', type: 'varchar', length: 30, default: 'none' })
authType: AuthType;
@Column({ name: 'auth_config', type: 'jsonb', default: {} })
authConfig: Record<string, any>;
@Column({ name: 'custom_headers', type: 'jsonb', default: {} })
customHeaders: Record<string, string>;
@Column({ name: 'subscribed_events', type: 'text', array: true, default: [] })
subscribedEvents: string[];
@Column({ name: 'filters', type: 'jsonb', default: {} })
filters: Record<string, any>;
@Column({ name: 'retry_enabled', type: 'boolean', default: true })
retryEnabled: boolean;
@Column({ name: 'max_retries', type: 'int', default: 5 })
maxRetries: number;
@Column({ name: 'retry_delay_seconds', type: 'int', default: 60 })
retryDelaySeconds: number;
@Column({ name: 'retry_backoff_multiplier', type: 'decimal', precision: 3, scale: 1, default: 2.0 })
retryBackoffMultiplier: number;
@Column({ name: 'timeout_seconds', type: 'int', default: 30 })
timeoutSeconds: number;
@Index()
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'is_verified', type: 'boolean', default: false })
isVerified: boolean;
@Column({ name: 'verified_at', type: 'timestamptz', nullable: true })
verifiedAt: Date;
@Column({ name: 'signing_secret', type: 'varchar', length: 255, nullable: true })
signingSecret: string;
@Column({ name: 'total_deliveries', type: 'int', default: 0 })
totalDeliveries: number;
@Column({ name: 'successful_deliveries', type: 'int', default: 0 })
successfulDeliveries: number;
@Column({ name: 'failed_deliveries', type: 'int', default: 0 })
failedDeliveries: number;
@Column({ name: 'last_delivery_at', type: 'timestamptz', nullable: true })
lastDeliveryAt: Date;
@Column({ name: 'last_success_at', type: 'timestamptz', nullable: true })
lastSuccessAt: Date;
@Column({ name: 'last_failure_at', type: 'timestamptz', nullable: true })
lastFailureAt: Date;
@Column({ name: 'rate_limit_per_minute', type: 'int', default: 60 })
rateLimitPerMinute: number;
@Column({ name: 'rate_limit_per_hour', type: 'int', default: 1000 })
rateLimitPerHour: 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;
}

View File

@ -0,0 +1,56 @@
/**
* WebhookEventType Entity
* Event type definitions for webhook subscriptions
* Compatible with erp-core event-type.entity
*
* @module Webhooks
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
} from 'typeorm';
export type EventCategory = 'sales' | 'inventory' | 'customers' | 'auth' | 'billing' | 'system';
@Entity({ name: 'event_types', schema: 'webhooks' })
export class WebhookEventType {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index({ unique: true })
@Column({ name: 'code', type: 'varchar', length: 100 })
code: string;
@Column({ name: 'name', type: 'varchar', length: 200 })
name: string;
@Column({ name: 'description', type: 'text', nullable: true })
description: string;
@Index()
@Column({ name: 'category', type: 'varchar', length: 50, nullable: true })
category: EventCategory;
@Column({ name: 'payload_schema', type: 'jsonb', default: {} })
payloadSchema: Record<string, any>;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'is_internal', type: 'boolean', default: false })
isInternal: boolean;
@Column({ name: 'metadata', type: 'jsonb', default: {} })
metadata: Record<string, any>;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,69 @@
/**
* WebhookEvent Entity
* Individual webhook events with dispatch tracking
* Compatible with erp-core event.entity
*
* @module Webhooks
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
} from 'typeorm';
export type WebhookEventStatus = 'pending' | 'processing' | 'dispatched' | 'failed';
@Entity({ name: 'events', schema: 'webhooks' })
export class WebhookEvent {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'event_type', type: 'varchar', length: 100 })
eventType: string;
@Column({ name: 'payload', type: 'jsonb' })
payload: Record<string, any>;
@Column({ name: 'resource_type', type: 'varchar', length: 100, nullable: true })
resourceType: string;
@Column({ name: 'resource_id', type: 'uuid', nullable: true })
resourceId: string;
@Column({ name: 'triggered_by', type: 'uuid', nullable: true })
triggeredBy: string;
@Index()
@Column({ name: 'status', type: 'varchar', length: 20, default: 'pending' })
status: WebhookEventStatus;
@Column({ name: 'processed_at', type: 'timestamptz', nullable: true })
processedAt: Date;
@Column({ name: 'dispatched_endpoints', type: 'int', default: 0 })
dispatchedEndpoints: number;
@Column({ name: 'failed_endpoints', type: 'int', default: 0 })
failedEndpoints: number;
@Index()
@Column({ name: 'idempotency_key', type: 'varchar', length: 255, nullable: true })
idempotencyKey: string;
@Column({ name: 'metadata', type: 'jsonb', default: {} })
metadata: Record<string, any>;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@Column({ name: 'expires_at', type: 'timestamptz', nullable: true })
expiresAt: Date;
}

View File

@ -0,0 +1,10 @@
/**
* Webhooks Entities - Export
*/
export { WebhookEventType, EventCategory } from './event-type.entity';
export { WebhookEndpoint, AuthType } from './endpoint.entity';
export { WebhookDelivery, DeliveryStatus } from './delivery.entity';
export { WebhookEvent, WebhookEventStatus } from './event.entity';
export { WebhookSubscription } from './subscription.entity';
export { WebhookEndpointLog, WebhookLogType } from './endpoint-log.entity';

View File

@ -0,0 +1,63 @@
/**
* WebhookSubscription Entity
* Links endpoints to specific event types
* Compatible with erp-core subscription.entity
*
* @module Webhooks
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
Unique,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { WebhookEndpoint } from './endpoint.entity';
import { WebhookEventType } from './event-type.entity';
@Entity({ name: 'subscriptions', schema: 'webhooks' })
@Unique(['endpointId', 'eventTypeId'])
export class WebhookSubscription {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'endpoint_id', type: 'uuid' })
endpointId: string;
@Index()
@Column({ name: 'event_type_id', type: 'uuid' })
eventTypeId: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'filters', type: 'jsonb', default: {} })
filters: Record<string, any>;
@Column({ name: 'payload_template', type: 'jsonb', nullable: true })
payloadTemplate: Record<string, any>;
@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(() => WebhookEndpoint, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'endpoint_id' })
endpoint: WebhookEndpoint;
@ManyToOne(() => WebhookEventType, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'event_type_id' })
eventType: WebhookEventType;
}

View File

@ -0,0 +1,102 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
Unique,
} from 'typeorm';
export type AccountStatus = 'pending' | 'active' | 'suspended' | 'disconnected';
@Entity({ name: 'accounts', schema: 'whatsapp' })
@Unique(['tenantId', 'phoneNumber'])
export class WhatsAppAccount {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'name', type: 'varchar', length: 200 })
name: string;
@Index()
@Column({ name: 'phone_number', type: 'varchar', length: 20 })
phoneNumber: string;
@Column({ name: 'phone_number_id', type: 'varchar', length: 50 })
phoneNumberId: string;
@Column({ name: 'business_account_id', type: 'varchar', length: 50 })
businessAccountId: string;
@Column({ name: 'access_token', type: 'text', nullable: true })
accessToken: string;
@Column({ name: 'webhook_verify_token', type: 'varchar', length: 255, nullable: true })
webhookVerifyToken: string;
@Column({ name: 'webhook_secret', type: 'varchar', length: 255, nullable: true })
webhookSecret: string;
@Column({ name: 'business_name', type: 'varchar', length: 200, nullable: true })
businessName: string;
@Column({ name: 'business_description', type: 'text', nullable: true })
businessDescription: string;
@Column({ name: 'business_category', type: 'varchar', length: 100, nullable: true })
businessCategory: string;
@Column({ name: 'business_website', type: 'text', nullable: true })
businessWebsite: string;
@Column({ name: 'profile_picture_url', type: 'text', nullable: true })
profilePictureUrl: string;
@Column({ name: 'default_language', type: 'varchar', length: 10, default: 'es_MX' })
defaultLanguage: string;
@Column({ name: 'auto_reply_enabled', type: 'boolean', default: false })
autoReplyEnabled: boolean;
@Column({ name: 'auto_reply_message', type: 'text', nullable: true })
autoReplyMessage: string;
@Column({ name: 'business_hours', type: 'jsonb', default: {} })
businessHours: Record<string, any>;
@Index()
@Column({ name: 'status', type: 'varchar', length: 20, default: 'pending' })
status: AccountStatus;
@Column({ name: 'verified_at', type: 'timestamptz', nullable: true })
verifiedAt: Date;
@Column({ name: 'daily_message_limit', type: 'int', default: 1000 })
dailyMessageLimit: number;
@Column({ name: 'messages_sent_today', type: 'int', default: 0 })
messagesSentToday: number;
@Column({ name: 'last_limit_reset', type: 'timestamptz', nullable: true })
lastLimitReset: Date;
@Column({ name: 'total_messages_sent', type: 'bigint', default: 0 })
totalMessagesSent: number;
@Column({ name: 'total_messages_received', type: 'bigint', default: 0 })
totalMessagesReceived: 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;
}

View File

@ -0,0 +1,75 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { WhatsAppAccount } from './account.entity';
export type AutomationTriggerType = 'keyword' | 'first_message' | 'after_hours' | 'no_response' | 'webhook';
export type AutomationActionType = 'send_message' | 'send_template' | 'assign_agent' | 'add_tag' | 'create_ticket';
@Entity({ name: 'automations', schema: 'whatsapp' })
export class WhatsAppAutomation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'account_id', type: 'uuid' })
accountId: string;
@Column({ name: 'name', type: 'varchar', length: 200 })
name: string;
@Column({ name: 'description', type: 'text', nullable: true })
description: string;
@Column({ name: 'trigger_type', type: 'varchar', length: 30 })
triggerType: AutomationTriggerType;
@Column({ name: 'trigger_config', type: 'jsonb', default: {} })
triggerConfig: Record<string, any>;
@Column({ name: 'action_type', type: 'varchar', length: 30 })
actionType: AutomationActionType;
@Column({ name: 'action_config', type: 'jsonb', default: {} })
actionConfig: Record<string, any>;
@Column({ name: 'conditions', type: 'jsonb', default: [] })
conditions: Record<string, any>[];
@Index()
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'priority', type: 'int', default: 0 })
priority: number;
@Column({ name: 'trigger_count', type: 'int', default: 0 })
triggerCount: number;
@Column({ name: 'last_triggered_at', type: 'timestamptz', nullable: true })
lastTriggeredAt: Date;
@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;
@ManyToOne(() => WhatsAppAccount, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'account_id' })
account: WhatsAppAccount;
}

View File

@ -0,0 +1,69 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
Unique,
} from 'typeorm';
import { Broadcast } from './broadcast.entity';
import { WhatsAppContact } from './contact.entity';
import { WhatsAppMessage } from './message.entity';
export type RecipientStatus = 'pending' | 'sent' | 'delivered' | 'read' | 'failed';
@Entity({ name: 'broadcast_recipients', schema: 'whatsapp' })
@Unique(['broadcastId', 'contactId'])
export class BroadcastRecipient {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'broadcast_id', type: 'uuid' })
broadcastId: string;
@Column({ name: 'contact_id', type: 'uuid' })
contactId: string;
@Column({ name: 'template_variables', type: 'jsonb', default: [] })
templateVariables: any[];
@Index()
@Column({ name: 'status', type: 'varchar', length: 20, default: 'pending' })
status: RecipientStatus;
@Column({ name: 'message_id', type: 'uuid', nullable: true })
messageId: string;
@Column({ name: 'error_code', type: 'varchar', length: 20, nullable: true })
errorCode: string;
@Column({ name: 'error_message', type: 'text', nullable: true })
errorMessage: string;
@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;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@ManyToOne(() => Broadcast, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'broadcast_id' })
broadcast: Broadcast;
@ManyToOne(() => WhatsAppContact, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'contact_id' })
contact: WhatsAppContact;
@ManyToOne(() => WhatsAppMessage, { nullable: true })
@JoinColumn({ name: 'message_id' })
message: WhatsAppMessage;
}

View File

@ -0,0 +1,102 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { WhatsAppAccount } from './account.entity';
import { WhatsAppTemplate } from './template.entity';
export type BroadcastStatus = 'draft' | 'scheduled' | 'sending' | 'completed' | 'cancelled' | 'failed';
export type AudienceType = 'all' | 'segment' | 'custom' | 'file';
@Entity({ name: 'broadcasts', schema: 'whatsapp' })
export class Broadcast {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'account_id', type: 'uuid' })
accountId: string;
@Column({ name: 'name', type: 'varchar', length: 200 })
name: string;
@Column({ name: 'description', type: 'text', nullable: true })
description: string;
@Column({ name: 'template_id', type: 'uuid' })
templateId: string;
@Column({ name: 'audience_type', type: 'varchar', length: 30 })
audienceType: AudienceType;
@Column({ name: 'audience_filter', type: 'jsonb', default: {} })
audienceFilter: Record<string, any>;
@Column({ name: 'recipient_count', type: 'int', default: 0 })
recipientCount: number;
@Index()
@Column({ name: 'scheduled_at', type: 'timestamptz', nullable: true })
scheduledAt: Date;
@Column({ name: 'timezone', type: 'varchar', length: 50, default: 'America/Mexico_City' })
timezone: string;
@Index()
@Column({ name: 'status', type: 'varchar', length: 20, default: 'draft' })
status: BroadcastStatus;
@Column({ name: 'sent_count', type: 'int', default: 0 })
sentCount: number;
@Column({ name: 'delivered_count', type: 'int', default: 0 })
deliveredCount: number;
@Column({ name: 'read_count', type: 'int', default: 0 })
readCount: number;
@Column({ name: 'failed_count', type: 'int', default: 0 })
failedCount: number;
@Column({ name: 'reply_count', type: 'int', default: 0 })
replyCount: number;
@Column({ name: 'started_at', type: 'timestamptz', nullable: true })
startedAt: Date;
@Column({ name: 'completed_at', type: 'timestamptz', nullable: true })
completedAt: Date;
@Column({ name: 'estimated_cost', type: 'decimal', precision: 10, scale: 2, nullable: true })
estimatedCost: number;
@Column({ name: 'actual_cost', type: 'decimal', precision: 10, scale: 2, nullable: true })
actualCost: 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;
@ManyToOne(() => WhatsAppAccount, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'account_id' })
account: WhatsAppAccount;
@ManyToOne(() => WhatsAppTemplate)
@JoinColumn({ name: 'template_id' })
template: WhatsAppTemplate;
}

View File

@ -0,0 +1,99 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
Unique,
} from 'typeorm';
import { WhatsAppAccount } from './account.entity';
export type ConversationStatus = 'active' | 'waiting' | 'resolved' | 'blocked';
export type MessageDirection = 'inbound' | 'outbound';
@Entity({ name: 'contacts', schema: 'whatsapp' })
@Unique(['accountId', 'phoneNumber'])
export class WhatsAppContact {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'account_id', type: 'uuid' })
accountId: string;
@Index()
@Column({ name: 'phone_number', type: 'varchar', length: 20 })
phoneNumber: string;
@Column({ name: 'wa_id', type: 'varchar', length: 50, nullable: true })
waId: string;
@Column({ name: 'profile_name', type: 'varchar', length: 200, nullable: true })
profileName: string;
@Column({ name: 'profile_picture_url', type: 'text', nullable: true })
profilePictureUrl: string;
@Column({ name: 'customer_id', type: 'uuid', nullable: true })
customerId: string;
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string;
@Column({ name: 'conversation_status', type: 'varchar', length: 20, default: 'active' })
conversationStatus: ConversationStatus;
@Column({ name: 'last_message_at', type: 'timestamptz', nullable: true })
lastMessageAt: Date;
@Column({ name: 'last_message_direction', type: 'varchar', length: 10, nullable: true })
lastMessageDirection: MessageDirection;
@Column({ name: 'conversation_window_expires_at', type: 'timestamptz', nullable: true })
conversationWindowExpiresAt: Date;
@Column({ name: 'can_send_template_only', type: 'boolean', default: true })
canSendTemplateOnly: boolean;
@Index()
@Column({ name: 'opted_in', type: 'boolean', default: false })
optedIn: boolean;
@Column({ name: 'opted_in_at', type: 'timestamptz', nullable: true })
optedInAt: Date;
@Column({ name: 'opted_out', type: 'boolean', default: false })
optedOut: boolean;
@Column({ name: 'opted_out_at', type: 'timestamptz', nullable: true })
optedOutAt: Date;
@Column({ name: 'tags', type: 'text', array: true, default: [] })
tags: string[];
@Column({ name: 'notes', type: 'text', nullable: true })
notes: string;
@Column({ name: 'total_messages_sent', type: 'int', default: 0 })
totalMessagesSent: number;
@Column({ name: 'total_messages_received', type: 'int', default: 0 })
totalMessagesReceived: number;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@ManyToOne(() => WhatsAppAccount, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'account_id' })
account: WhatsAppAccount;
}

View File

@ -0,0 +1,92 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { WhatsAppAccount } from './account.entity';
import { WhatsAppContact } from './contact.entity';
export type WAConversationStatus = 'open' | 'pending' | 'resolved' | 'closed';
export type WAConversationPriority = 'low' | 'normal' | 'high' | 'urgent';
@Entity({ name: 'conversations', schema: 'whatsapp' })
export class WhatsAppConversation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'account_id', type: 'uuid' })
accountId: string;
@Index()
@Column({ name: 'contact_id', type: 'uuid' })
contactId: string;
@Index()
@Column({ name: 'status', type: 'varchar', length: 20, default: 'open' })
status: WAConversationStatus;
@Column({ name: 'priority', type: 'varchar', length: 20, default: 'normal' })
priority: WAConversationPriority;
@Index()
@Column({ name: 'assigned_to', type: 'uuid', nullable: true })
assignedTo: string;
@Column({ name: 'assigned_at', type: 'timestamptz', nullable: true })
assignedAt: Date;
@Column({ name: 'team_id', type: 'uuid', nullable: true })
teamId: string;
@Column({ name: 'category', type: 'varchar', length: 50, nullable: true })
category: string;
@Column({ name: 'tags', type: 'text', array: true, default: [] })
tags: string[];
@Column({ name: 'context_type', type: 'varchar', length: 50, nullable: true })
contextType: string;
@Column({ name: 'context_id', type: 'uuid', nullable: true })
contextId: string;
@Column({ name: 'first_response_at', type: 'timestamptz', nullable: true })
firstResponseAt: Date;
@Column({ name: 'resolved_at', type: 'timestamptz', nullable: true })
resolvedAt: Date;
@Column({ name: 'message_count', type: 'int', default: 0 })
messageCount: number;
@Column({ name: 'unread_count', type: 'int', default: 0 })
unreadCount: number;
@Column({ name: 'metadata', type: 'jsonb', default: {} })
metadata: Record<string, any>;
@Index()
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@ManyToOne(() => WhatsAppAccount, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'account_id' })
account: WhatsAppAccount;
@ManyToOne(() => WhatsAppContact, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'contact_id' })
contact: WhatsAppContact;
}

View File

@ -0,0 +1,10 @@
export { WhatsAppAccount, AccountStatus } from './account.entity';
export { WhatsAppContact, ConversationStatus } from './contact.entity';
export { WhatsAppMessage, MessageType, MessageStatus, MessageDirection, CostCategory } from './message.entity';
export { WhatsAppTemplate, TemplateCategory, TemplateStatus, HeaderType } from './template.entity';
export { WhatsAppConversation, WAConversationStatus, WAConversationPriority } from './conversation.entity';
export { MessageStatusUpdate } from './message-status-update.entity';
export { QuickReply } from './quick-reply.entity';
export { WhatsAppAutomation, AutomationTriggerType, AutomationActionType } from './automation.entity';
export { Broadcast, BroadcastStatus, AudienceType } from './broadcast.entity';
export { BroadcastRecipient, RecipientStatus } from './broadcast-recipient.entity';

Some files were not shown because too many files have changed in this diff Show More