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:
parent
06d79e1c52
commit
ec59053bbe
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
84
src/modules/biometrics/entities/device-session.entity.ts
Normal file
84
src/modules/biometrics/entities/device-session.entity.ts
Normal 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;
|
||||
}
|
||||
121
src/modules/biometrics/entities/device.entity.ts
Normal file
121
src/modules/biometrics/entities/device.entity.ts
Normal 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[];
|
||||
}
|
||||
4
src/modules/biometrics/entities/index.ts
Normal file
4
src/modules/biometrics/entities/index.ts
Normal 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';
|
||||
54
src/modules/feature-flags/entities/flag-evaluation.entity.ts
Normal file
54
src/modules/feature-flags/entities/flag-evaluation.entity.ts
Normal 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;
|
||||
}
|
||||
65
src/modules/feature-flags/entities/flag.entity.ts
Normal file
65
src/modules/feature-flags/entities/flag.entity.ts
Normal 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[];
|
||||
}
|
||||
7
src/modules/feature-flags/entities/index.ts
Normal file
7
src/modules/feature-flags/entities/index.ts
Normal 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';
|
||||
58
src/modules/feature-flags/entities/tenant-override.entity.ts
Normal file
58
src/modules/feature-flags/entities/tenant-override.entity.ts
Normal 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;
|
||||
}
|
||||
161
src/modules/hr/entities/contract.entity.ts
Normal file
161
src/modules/hr/entities/contract.entity.ts
Normal 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;
|
||||
}
|
||||
75
src/modules/hr/entities/department.entity.ts
Normal file
75
src/modules/hr/entities/department.entity.ts
Normal 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;
|
||||
}
|
||||
65
src/modules/hr/entities/employee-fraccionamiento.entity.ts
Normal file
65
src/modules/hr/entities/employee-fraccionamiento.entity.ts
Normal 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;
|
||||
}
|
||||
136
src/modules/hr/entities/employee.entity.ts
Normal file
136
src/modules/hr/entities/employee.entity.ts
Normal 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(' ');
|
||||
}
|
||||
}
|
||||
16
src/modules/hr/entities/index.ts
Normal file
16
src/modules/hr/entities/index.ts
Normal 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';
|
||||
85
src/modules/hr/entities/leave-allocation.entity.ts
Normal file
85
src/modules/hr/entities/leave-allocation.entity.ts
Normal 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;
|
||||
}
|
||||
131
src/modules/hr/entities/leave-type.entity.ts
Normal file
131
src/modules/hr/entities/leave-type.entity.ts
Normal 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;
|
||||
}
|
||||
138
src/modules/hr/entities/leave.entity.ts
Normal file
138
src/modules/hr/entities/leave.entity.ts
Normal 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;
|
||||
}
|
||||
68
src/modules/hr/entities/puesto.entity.ts
Normal file
68
src/modules/hr/entities/puesto.entity.ts
Normal 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[];
|
||||
}
|
||||
8
src/modules/invoices/entities/index.ts
Normal file
8
src/modules/invoices/entities/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Invoices Entities - Export
|
||||
*/
|
||||
|
||||
export { Invoice, InvoiceType, InvoiceStatus, InvoiceContext } from './invoice.entity';
|
||||
export { InvoiceItem } from './invoice-item.entity';
|
||||
export { Payment } from './payment.entity';
|
||||
export { PaymentAllocation } from './payment-allocation.entity';
|
||||
95
src/modules/invoices/entities/invoice-item.entity.ts
Normal file
95
src/modules/invoices/entities/invoice-item.entity.ts
Normal file
@ -0,0 +1,95 @@
|
||||
/**
|
||||
* InvoiceItem Entity
|
||||
* Line items for unified invoices
|
||||
* Compatible with erp-core invoice-item.entity
|
||||
*
|
||||
* @module Invoices
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Invoice } from './invoice.entity';
|
||||
|
||||
@Entity({ name: 'invoice_items', schema: 'billing' })
|
||||
export class InvoiceItem {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'invoice_id', type: 'uuid' })
|
||||
invoiceId: string;
|
||||
|
||||
@ManyToOne(() => Invoice, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'invoice_id' })
|
||||
invoice: Invoice;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'product_id', type: 'uuid', nullable: true })
|
||||
productId?: string;
|
||||
|
||||
@Column({ name: 'line_number', type: 'int', default: 1 })
|
||||
lineNumber: number;
|
||||
|
||||
@Column({ name: 'product_sku', type: 'varchar', length: 50, nullable: true })
|
||||
productSku?: string;
|
||||
|
||||
@Column({ name: 'product_name', type: 'varchar', length: 200 })
|
||||
productName: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description?: string;
|
||||
|
||||
// SAT (Mexico)
|
||||
@Column({ name: 'sat_product_code', type: 'varchar', length: 20, nullable: true })
|
||||
satProductCode?: string;
|
||||
|
||||
@Column({ name: 'sat_unit_code', type: 'varchar', length: 10, nullable: true })
|
||||
satUnitCode?: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 15, scale: 4, default: 1 })
|
||||
quantity: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, default: 'PZA' })
|
||||
uom: string;
|
||||
|
||||
@Column({ name: 'unit_price', type: 'decimal', precision: 15, scale: 4, default: 0 })
|
||||
unitPrice: number;
|
||||
|
||||
@Column({ name: 'discount_percent', type: 'decimal', precision: 5, scale: 2, default: 0 })
|
||||
discountPercent: number;
|
||||
|
||||
@Column({ name: 'discount_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||
discountAmount: number;
|
||||
|
||||
@Column({ name: 'tax_rate', type: 'decimal', precision: 5, scale: 2, default: 16.00 })
|
||||
taxRate: number;
|
||||
|
||||
@Column({ name: 'tax_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||
taxAmount: number;
|
||||
|
||||
@Column({ name: 'withholding_rate', type: 'decimal', precision: 5, scale: 2, default: 0 })
|
||||
withholdingRate: number;
|
||||
|
||||
@Column({ name: 'withholding_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||
withholdingAmount: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||
subtotal: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||
total: number;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
187
src/modules/invoices/entities/invoice.entity.ts
Normal file
187
src/modules/invoices/entities/invoice.entity.ts
Normal file
@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Unified Invoice Entity
|
||||
* Combines commercial and SaaS billing invoices
|
||||
* Compatible with erp-core invoice.entity
|
||||
*
|
||||
* @module Invoices
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Index,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { InvoiceItem } from './invoice-item.entity';
|
||||
|
||||
export type InvoiceType = 'sale' | 'purchase' | 'credit_note' | 'debit_note';
|
||||
export type InvoiceStatus = 'draft' | 'validated' | 'sent' | 'partial' | 'paid' | 'overdue' | 'void' | 'refunded' | 'cancelled' | 'voided';
|
||||
export type InvoiceContext = 'commercial' | 'saas';
|
||||
|
||||
@Entity({ name: 'invoices', schema: 'billing' })
|
||||
export class Invoice {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column({ name: 'invoice_number', type: 'varchar', length: 30 })
|
||||
invoiceNumber: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'invoice_type', type: 'varchar', length: 20, default: 'sale' })
|
||||
invoiceType: InvoiceType;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'invoice_context', type: 'varchar', length: 20, default: 'commercial' })
|
||||
invoiceContext: InvoiceContext;
|
||||
|
||||
// Commercial fields
|
||||
@Column({ name: 'sales_order_id', type: 'uuid', nullable: true })
|
||||
salesOrderId: string | null;
|
||||
|
||||
@Column({ name: 'purchase_order_id', type: 'uuid', nullable: true })
|
||||
purchaseOrderId: string | null;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'partner_id', type: 'uuid', nullable: true })
|
||||
partnerId: string | null;
|
||||
|
||||
@Column({ name: 'partner_name', type: 'varchar', length: 200, nullable: true })
|
||||
partnerName: string | null;
|
||||
|
||||
@Column({ name: 'partner_tax_id', type: 'varchar', length: 50, nullable: true })
|
||||
partnerTaxId: string | null;
|
||||
|
||||
// SaaS billing fields
|
||||
@Index()
|
||||
@Column({ name: 'subscription_id', type: 'uuid', nullable: true })
|
||||
subscriptionId: string | null;
|
||||
|
||||
@Column({ name: 'period_start', type: 'date', nullable: true })
|
||||
periodStart: Date | null;
|
||||
|
||||
@Column({ name: 'period_end', type: 'date', nullable: true })
|
||||
periodEnd: Date | null;
|
||||
|
||||
// Billing information
|
||||
@Column({ name: 'billing_name', type: 'varchar', length: 200, nullable: true })
|
||||
billingName: string | null;
|
||||
|
||||
@Column({ name: 'billing_email', type: 'varchar', length: 255, nullable: true })
|
||||
billingEmail: string | null;
|
||||
|
||||
@Column({ name: 'billing_address', type: 'jsonb', nullable: true })
|
||||
billingAddress: Record<string, any> | null;
|
||||
|
||||
@Column({ name: 'tax_id', type: 'varchar', length: 50, nullable: true })
|
||||
taxId: string | null;
|
||||
|
||||
// Dates
|
||||
@Index()
|
||||
@Column({ name: 'invoice_date', type: 'date', default: () => 'CURRENT_DATE' })
|
||||
invoiceDate: Date;
|
||||
|
||||
@Column({ name: 'due_date', type: 'date', nullable: true })
|
||||
dueDate: Date | null;
|
||||
|
||||
@Column({ name: 'payment_date', type: 'date', nullable: true })
|
||||
paymentDate: Date | null;
|
||||
|
||||
@Column({ name: 'paid_at', type: 'timestamptz', nullable: true })
|
||||
paidAt: Date | null;
|
||||
|
||||
// Amounts
|
||||
@Column({ type: 'varchar', length: 3, default: 'MXN' })
|
||||
currency: string;
|
||||
|
||||
@Column({ name: 'exchange_rate', type: 'decimal', precision: 10, scale: 6, default: 1 })
|
||||
exchangeRate: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||
subtotal: number;
|
||||
|
||||
@Column({ name: 'tax_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||
taxAmount: number;
|
||||
|
||||
@Column({ name: 'withholding_tax', type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||
withholdingTax: number;
|
||||
|
||||
@Column({ name: 'discount_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||
discountAmount: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||
total: number;
|
||||
|
||||
@Column({ name: 'amount_paid', type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||
amountPaid: number;
|
||||
|
||||
@Column({ name: 'amount_due', type: 'decimal', precision: 15, scale: 2, insert: false, update: false, nullable: true })
|
||||
amountDue: number | null;
|
||||
|
||||
@Column({ name: 'paid_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||
paidAmount: number;
|
||||
|
||||
// Payment details
|
||||
@Column({ name: 'payment_term_days', type: 'int', default: 0 })
|
||||
paymentTermDays: number;
|
||||
|
||||
@Column({ name: 'payment_method', type: 'varchar', length: 50, nullable: true })
|
||||
paymentMethod: string | null;
|
||||
|
||||
@Column({ name: 'payment_reference', type: 'varchar', length: 100, nullable: true })
|
||||
paymentReference: string | null;
|
||||
|
||||
// Status
|
||||
@Index()
|
||||
@Column({ type: 'varchar', length: 20, default: 'draft' })
|
||||
status: InvoiceStatus;
|
||||
|
||||
// CFDI (Mexico)
|
||||
@Index()
|
||||
@Column({ name: 'cfdi_uuid', type: 'varchar', length: 40, nullable: true })
|
||||
cfdiUuid: string | null;
|
||||
|
||||
@Column({ name: 'cfdi_status', type: 'varchar', length: 20, nullable: true })
|
||||
cfdiStatus: string | null;
|
||||
|
||||
@Column({ name: 'cfdi_xml', type: 'text', nullable: true })
|
||||
cfdiXml: string | null;
|
||||
|
||||
@Column({ name: 'cfdi_pdf_url', type: 'varchar', length: 500, nullable: true })
|
||||
cfdiPdfUrl: string | null;
|
||||
|
||||
// Notes
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes: string | null;
|
||||
|
||||
@Column({ name: 'internal_notes', type: 'text', nullable: true })
|
||||
internalNotes: string | null;
|
||||
|
||||
// Audit
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@Column({ name: 'created_by', type: 'uuid', nullable: true })
|
||||
createdBy: string | null;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
|
||||
updatedBy: string | null;
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
|
||||
deletedAt: Date | null;
|
||||
|
||||
// Relations
|
||||
@OneToMany(() => InvoiceItem, (item) => item.invoice, { cascade: true })
|
||||
items: InvoiceItem[];
|
||||
}
|
||||
53
src/modules/invoices/entities/payment-allocation.entity.ts
Normal file
53
src/modules/invoices/entities/payment-allocation.entity.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* PaymentAllocation Entity
|
||||
* Links payments to invoices
|
||||
* Compatible with erp-core payment-allocation.entity
|
||||
*
|
||||
* @module Invoices
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Index,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Payment } from './payment.entity';
|
||||
import { Invoice } from './invoice.entity';
|
||||
|
||||
@Entity({ name: 'payment_allocations', schema: 'billing' })
|
||||
export class PaymentAllocation {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'payment_id', type: 'uuid' })
|
||||
paymentId: string;
|
||||
|
||||
@ManyToOne(() => Payment, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'payment_id' })
|
||||
payment: Payment;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'invoice_id', type: 'uuid' })
|
||||
invoiceId: string;
|
||||
|
||||
@ManyToOne(() => Invoice, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'invoice_id' })
|
||||
invoice: Invoice;
|
||||
|
||||
@Column({ type: 'decimal', precision: 15, scale: 2 })
|
||||
amount: number;
|
||||
|
||||
@Column({ name: 'allocation_date', type: 'date', default: () => 'CURRENT_DATE' })
|
||||
allocationDate: Date;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@Column({ name: 'created_by', type: 'uuid', nullable: true })
|
||||
createdBy?: string;
|
||||
}
|
||||
89
src/modules/invoices/entities/payment.entity.ts
Normal file
89
src/modules/invoices/entities/payment.entity.ts
Normal file
@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Payment Entity
|
||||
* Payment received or made
|
||||
* Compatible with erp-core payment.entity
|
||||
*
|
||||
* @module Invoices
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'payments', schema: 'billing' })
|
||||
export class Payment {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'payment_number', type: 'varchar', length: 30 })
|
||||
paymentNumber: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'payment_type', type: 'varchar', length: 20, default: 'received' })
|
||||
paymentType: 'received' | 'made';
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'partner_id', type: 'uuid' })
|
||||
partnerId: string;
|
||||
|
||||
@Column({ name: 'partner_name', type: 'varchar', length: 200, nullable: true })
|
||||
partnerName: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 3, default: 'MXN' })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 15, scale: 2 })
|
||||
amount: number;
|
||||
|
||||
@Column({ name: 'exchange_rate', type: 'decimal', precision: 10, scale: 6, default: 1 })
|
||||
exchangeRate: number;
|
||||
|
||||
@Column({ name: 'payment_date', type: 'date', default: () => 'CURRENT_DATE' })
|
||||
paymentDate: Date;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'payment_method', type: 'varchar', length: 50 })
|
||||
paymentMethod: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, nullable: true })
|
||||
reference: string;
|
||||
|
||||
@Column({ name: 'bank_account_id', type: 'uuid', nullable: true })
|
||||
bankAccountId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'varchar', length: 20, default: 'draft' })
|
||||
status: 'draft' | 'confirmed' | 'reconciled' | 'cancelled';
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes: string;
|
||||
|
||||
@Column({ name: 'cfdi_uuid', type: 'varchar', length: 40, nullable: true })
|
||||
cfdiUuid: string;
|
||||
|
||||
@Column({ name: 'cfdi_status', type: 'varchar', length: 20, nullable: true })
|
||||
cfdiStatus: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@Column({ name: 'created_by', type: 'uuid', nullable: true })
|
||||
createdBy?: string;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
|
||||
deletedAt: Date;
|
||||
}
|
||||
7
src/modules/products/entities/index.ts
Normal file
7
src/modules/products/entities/index.ts
Normal 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';
|
||||
@ -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;
|
||||
}
|
||||
60
src/modules/products/entities/product-attribute.entity.ts
Normal file
60
src/modules/products/entities/product-attribute.entity.ts
Normal 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[];
|
||||
}
|
||||
69
src/modules/products/entities/product-category.entity.ts
Normal file
69
src/modules/products/entities/product-category.entity.ts
Normal 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;
|
||||
}
|
||||
48
src/modules/products/entities/product-price.entity.ts
Normal file
48
src/modules/products/entities/product-price.entity.ts
Normal 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;
|
||||
}
|
||||
51
src/modules/products/entities/product-supplier.entity.ts
Normal file
51
src/modules/products/entities/product-supplier.entity.ts
Normal 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;
|
||||
}
|
||||
72
src/modules/products/entities/product-variant.entity.ts
Normal file
72
src/modules/products/entities/product-variant.entity.ts
Normal 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;
|
||||
}
|
||||
206
src/modules/products/entities/product.entity.ts
Normal file
206
src/modules/products/entities/product.entity.ts
Normal 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;
|
||||
}
|
||||
9
src/modules/profiles/entities/index.ts
Normal file
9
src/modules/profiles/entities/index.ts
Normal 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';
|
||||
78
src/modules/profiles/entities/person.entity.ts
Normal file
78
src/modules/profiles/entities/person.entity.ts
Normal 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;
|
||||
}
|
||||
45
src/modules/profiles/entities/profile-module.entity.ts
Normal file
45
src/modules/profiles/entities/profile-module.entity.ts
Normal 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;
|
||||
}
|
||||
68
src/modules/profiles/entities/profile-tool.entity.ts
Normal file
68
src/modules/profiles/entities/profile-tool.entity.ts
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
90
src/modules/profiles/entities/user-profile.entity.ts
Normal file
90
src/modules/profiles/entities/user-profile.entity.ts
Normal 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[];
|
||||
}
|
||||
1
src/modules/projects/entities/index.ts
Normal file
1
src/modules/projects/entities/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './timesheet.entity';
|
||||
93
src/modules/projects/entities/timesheet.entity.ts
Normal file
93
src/modules/projects/entities/timesheet.entity.ts
Normal 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;
|
||||
}
|
||||
101
src/modules/purchase/entities/comparativo-cotizaciones.entity.ts
Normal file
101
src/modules/purchase/entities/comparativo-cotizaciones.entity.ts
Normal 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[];
|
||||
}
|
||||
75
src/modules/purchase/entities/comparativo-producto.entity.ts
Normal file
75
src/modules/purchase/entities/comparativo-producto.entity.ts
Normal 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;
|
||||
}
|
||||
@ -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[];
|
||||
}
|
||||
20
src/modules/purchase/entities/index.ts
Normal file
20
src/modules/purchase/entities/index.ts
Normal 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';
|
||||
78
src/modules/purchase/entities/matching-exception.entity.ts
Normal file
78
src/modules/purchase/entities/matching-exception.entity.ts
Normal 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;
|
||||
}
|
||||
@ -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[];
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
102
src/modules/purchase/entities/purchase-order-matching.entity.ts
Normal file
102
src/modules/purchase/entities/purchase-order-matching.entity.ts
Normal 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[];
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
52
src/modules/purchase/entities/purchase-receipt.entity.ts
Normal file
52
src/modules/purchase/entities/purchase-receipt.entity.ts
Normal 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;
|
||||
}
|
||||
130
src/modules/purchase/entities/supplier-construction.entity.ts
Normal file
130
src/modules/purchase/entities/supplier-construction.entity.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
67
src/modules/reports/entities/custom-report.entity.ts
Normal file
67
src/modules/reports/entities/custom-report.entity.ts
Normal 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;
|
||||
}
|
||||
222
src/modules/reports/entities/dashboard-widget.entity.ts
Normal file
222
src/modules/reports/entities/dashboard-widget.entity.ts
Normal 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;
|
||||
}
|
||||
205
src/modules/reports/entities/dashboard.entity.ts
Normal file
205
src/modules/reports/entities/dashboard.entity.ts
Normal 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[];
|
||||
}
|
||||
69
src/modules/reports/entities/data-model-entity.entity.ts
Normal file
69
src/modules/reports/entities/data-model-entity.entity.ts
Normal 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[];
|
||||
}
|
||||
77
src/modules/reports/entities/data-model-field.entity.ts
Normal file
77
src/modules/reports/entities/data-model-field.entity.ts
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
21
src/modules/reports/entities/index.ts
Normal file
21
src/modules/reports/entities/index.ts
Normal 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';
|
||||
220
src/modules/reports/entities/kpi-snapshot.entity.ts
Normal file
220
src/modules/reports/entities/kpi-snapshot.entity.ts
Normal 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;
|
||||
}
|
||||
191
src/modules/reports/entities/report-execution.entity.ts
Normal file
191
src/modules/reports/entities/report-execution.entity.ts
Normal 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;
|
||||
}
|
||||
47
src/modules/reports/entities/report-recipient.entity.ts
Normal file
47
src/modules/reports/entities/report-recipient.entity.ts
Normal 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;
|
||||
}
|
||||
142
src/modules/reports/entities/report-schedule.entity.ts
Normal file
142
src/modules/reports/entities/report-schedule.entity.ts
Normal 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[];
|
||||
}
|
||||
222
src/modules/reports/entities/report.entity.ts
Normal file
222
src/modules/reports/entities/report.entity.ts
Normal 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[];
|
||||
}
|
||||
59
src/modules/reports/entities/schedule-execution.entity.ts
Normal file
59
src/modules/reports/entities/schedule-execution.entity.ts
Normal 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;
|
||||
}
|
||||
59
src/modules/reports/entities/widget-query.entity.ts
Normal file
59
src/modules/reports/entities/widget-query.entity.ts
Normal 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;
|
||||
}
|
||||
4
src/modules/sales/entities/index.ts
Normal file
4
src/modules/sales/entities/index.ts
Normal 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';
|
||||
65
src/modules/sales/entities/quotation-item.entity.ts
Normal file
65
src/modules/sales/entities/quotation-item.entity.ts
Normal 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;
|
||||
}
|
||||
101
src/modules/sales/entities/quotation.entity.ts
Normal file
101
src/modules/sales/entities/quotation.entity.ts
Normal 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;
|
||||
}
|
||||
90
src/modules/sales/entities/sales-order-item.entity.ts
Normal file
90
src/modules/sales/entities/sales-order-item.entity.ts
Normal 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;
|
||||
}
|
||||
138
src/modules/sales/entities/sales-order.entity.ts
Normal file
138
src/modules/sales/entities/sales-order.entity.ts
Normal 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;
|
||||
}
|
||||
8
src/modules/settings/entities/index.ts
Normal file
8
src/modules/settings/entities/index.ts
Normal 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';
|
||||
42
src/modules/settings/entities/plan-setting.entity.ts
Normal file
42
src/modules/settings/entities/plan-setting.entity.ts
Normal 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;
|
||||
}
|
||||
67
src/modules/settings/entities/system-setting.entity.ts
Normal file
67
src/modules/settings/entities/system-setting.entity.ts
Normal 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;
|
||||
}
|
||||
53
src/modules/settings/entities/tenant-setting.entity.ts
Normal file
53
src/modules/settings/entities/tenant-setting.entity.ts
Normal 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;
|
||||
}
|
||||
44
src/modules/settings/entities/user-preference.entity.ts
Normal file
44
src/modules/settings/entities/user-preference.entity.ts
Normal 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;
|
||||
}
|
||||
74
src/modules/storage/entities/bucket.entity.ts
Normal file
74
src/modules/storage/entities/bucket.entity.ts
Normal 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;
|
||||
}
|
||||
71
src/modules/storage/entities/file-access-token.entity.ts
Normal file
71
src/modules/storage/entities/file-access-token.entity.ts
Normal 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;
|
||||
}
|
||||
96
src/modules/storage/entities/file-share.entity.ts
Normal file
96
src/modules/storage/entities/file-share.entity.ts
Normal 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;
|
||||
}
|
||||
162
src/modules/storage/entities/file.entity.ts
Normal file
162
src/modules/storage/entities/file.entity.ts
Normal 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;
|
||||
}
|
||||
91
src/modules/storage/entities/folder.entity.ts
Normal file
91
src/modules/storage/entities/folder.entity.ts
Normal 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[];
|
||||
}
|
||||
11
src/modules/storage/entities/index.ts
Normal file
11
src/modules/storage/entities/index.ts
Normal 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';
|
||||
65
src/modules/storage/entities/tenant-usage.entity.ts
Normal file
65
src/modules/storage/entities/tenant-usage.entity.ts
Normal 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;
|
||||
}
|
||||
110
src/modules/storage/entities/upload.entity.ts
Normal file
110
src/modules/storage/entities/upload.entity.ts
Normal 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;
|
||||
}
|
||||
3
src/modules/warehouses/entities/index.ts
Normal file
3
src/modules/warehouses/entities/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { Warehouse } from './warehouse.entity';
|
||||
export { WarehouseLocation } from './warehouse-location.entity';
|
||||
export { WarehouseZone } from './warehouse-zone.entity';
|
||||
111
src/modules/warehouses/entities/warehouse-location.entity.ts
Normal file
111
src/modules/warehouses/entities/warehouse-location.entity.ts
Normal 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;
|
||||
}
|
||||
41
src/modules/warehouses/entities/warehouse-zone.entity.ts
Normal file
41
src/modules/warehouses/entities/warehouse-zone.entity.ts
Normal 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;
|
||||
}
|
||||
138
src/modules/warehouses/entities/warehouse.entity.ts
Normal file
138
src/modules/warehouses/entities/warehouse.entity.ts
Normal 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;
|
||||
}
|
||||
105
src/modules/webhooks/entities/delivery.entity.ts
Normal file
105
src/modules/webhooks/entities/delivery.entity.ts
Normal 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;
|
||||
}
|
||||
54
src/modules/webhooks/entities/endpoint-log.entity.ts
Normal file
54
src/modules/webhooks/entities/endpoint-log.entity.ts
Normal 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;
|
||||
}
|
||||
118
src/modules/webhooks/entities/endpoint.entity.ts
Normal file
118
src/modules/webhooks/entities/endpoint.entity.ts
Normal 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;
|
||||
}
|
||||
56
src/modules/webhooks/entities/event-type.entity.ts
Normal file
56
src/modules/webhooks/entities/event-type.entity.ts
Normal 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;
|
||||
}
|
||||
69
src/modules/webhooks/entities/event.entity.ts
Normal file
69
src/modules/webhooks/entities/event.entity.ts
Normal 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;
|
||||
}
|
||||
10
src/modules/webhooks/entities/index.ts
Normal file
10
src/modules/webhooks/entities/index.ts
Normal 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';
|
||||
63
src/modules/webhooks/entities/subscription.entity.ts
Normal file
63
src/modules/webhooks/entities/subscription.entity.ts
Normal 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;
|
||||
}
|
||||
102
src/modules/whatsapp/entities/account.entity.ts
Normal file
102
src/modules/whatsapp/entities/account.entity.ts
Normal 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;
|
||||
}
|
||||
75
src/modules/whatsapp/entities/automation.entity.ts
Normal file
75
src/modules/whatsapp/entities/automation.entity.ts
Normal 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;
|
||||
}
|
||||
69
src/modules/whatsapp/entities/broadcast-recipient.entity.ts
Normal file
69
src/modules/whatsapp/entities/broadcast-recipient.entity.ts
Normal 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;
|
||||
}
|
||||
102
src/modules/whatsapp/entities/broadcast.entity.ts
Normal file
102
src/modules/whatsapp/entities/broadcast.entity.ts
Normal 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;
|
||||
}
|
||||
99
src/modules/whatsapp/entities/contact.entity.ts
Normal file
99
src/modules/whatsapp/entities/contact.entity.ts
Normal 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;
|
||||
}
|
||||
92
src/modules/whatsapp/entities/conversation.entity.ts
Normal file
92
src/modules/whatsapp/entities/conversation.entity.ts
Normal 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;
|
||||
}
|
||||
10
src/modules/whatsapp/entities/index.ts
Normal file
10
src/modules/whatsapp/entities/index.ts
Normal 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
Loading…
Reference in New Issue
Block a user