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