feat: Propagate core entities from erp-construccion

Modules added:
- audit (7 entities): audit logs, config changes, entity changes
- billing-usage (14 entities): subscriptions, invoices, coupons
- core (13 entities): countries, currencies, UoMs, sequences
- invoices (4 entities): invoices, payments, allocations
- notifications (6 entities): notifications, templates, channels

Total: 44 new entity files
Build: Clean (0 TypeScript errors)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-27 09:06:11 -06:00
parent a0ca2bd77a
commit 8c010221fb
47 changed files with 3453 additions and 0 deletions

View File

@ -0,0 +1,116 @@
/**
* AuditLog Entity
* General activity tracking with full request context
* Compatible with erp-core audit-log.entity
*
* @module Audit
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
} from 'typeorm';
export type AuditAction = 'create' | 'read' | 'update' | 'delete' | 'login' | 'logout' | 'export';
export type AuditCategory = 'data' | 'auth' | 'system' | 'config' | 'billing';
export type AuditStatus = 'success' | 'failure' | 'partial';
@Entity({ name: 'audit_logs', schema: 'audit' })
export class AuditLog {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string;
@Column({ name: 'user_email', type: 'varchar', length: 255, nullable: true })
userEmail: string;
@Column({ name: 'user_name', type: 'varchar', length: 200, nullable: true })
userName: string;
@Column({ name: 'session_id', type: 'uuid', nullable: true })
sessionId: string;
@Column({ name: 'impersonator_id', type: 'uuid', nullable: true })
impersonatorId: string;
@Index()
@Column({ name: 'action', type: 'varchar', length: 50 })
action: AuditAction;
@Index()
@Column({ name: 'action_category', type: 'varchar', length: 50, nullable: true })
actionCategory: AuditCategory;
@Index()
@Column({ name: 'resource_type', type: 'varchar', length: 100 })
resourceType: string;
@Column({ name: 'resource_id', type: 'uuid', nullable: true })
resourceId: string;
@Column({ name: 'resource_name', type: 'varchar', length: 255, nullable: true })
resourceName: string;
@Column({ name: 'old_values', type: 'jsonb', nullable: true })
oldValues: Record<string, any>;
@Column({ name: 'new_values', type: 'jsonb', nullable: true })
newValues: Record<string, any>;
@Column({ name: 'changed_fields', type: 'text', array: true, nullable: true })
changedFields: string[];
@Column({ name: 'ip_address', type: 'inet', nullable: true })
ipAddress: string;
@Column({ name: 'user_agent', type: 'text', nullable: true })
userAgent: string;
@Column({ name: 'device_info', type: 'jsonb', default: {} })
deviceInfo: Record<string, any>;
@Column({ name: 'location', type: 'jsonb', default: {} })
location: Record<string, any>;
@Column({ name: 'request_id', type: 'varchar', length: 100, nullable: true })
requestId: string;
@Column({ name: 'request_method', type: 'varchar', length: 10, nullable: true })
requestMethod: string;
@Column({ name: 'request_path', type: 'text', nullable: true })
requestPath: string;
@Column({ name: 'request_params', type: 'jsonb', default: {} })
requestParams: Record<string, any>;
@Index()
@Column({ name: 'status', type: 'varchar', length: 20, default: 'success' })
status: AuditStatus;
@Column({ name: 'error_message', type: 'text', nullable: true })
errorMessage: string;
@Column({ name: 'duration_ms', type: 'int', nullable: true })
durationMs: number;
@Column({ name: 'metadata', type: 'jsonb', default: {} })
metadata: Record<string, any>;
@Column({ name: 'tags', type: 'text', array: true, default: [] })
tags: string[];
@Index()
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
}

View File

@ -0,0 +1,55 @@
/**
* ConfigChange Entity
* System configuration change auditing
* Compatible with erp-core config-change.entity
*
* @module Audit
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
Index,
} from 'typeorm';
export type ConfigType = 'tenant_settings' | 'user_settings' | 'system_settings' | 'feature_flags';
@Entity({ name: 'config_changes', schema: 'audit' })
export class ConfigChange {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid', nullable: true })
tenantId: string;
@Column({ name: 'changed_by', type: 'uuid' })
changedBy: string;
@Index()
@Column({ name: 'config_type', type: 'varchar', length: 50 })
configType: ConfigType;
@Column({ name: 'config_key', type: 'varchar', length: 100 })
configKey: string;
@Column({ name: 'config_path', type: 'text', nullable: true })
configPath: string;
@Column({ name: 'old_value', type: 'jsonb', nullable: true })
oldValue: Record<string, any>;
@Column({ name: 'new_value', type: 'jsonb', nullable: true })
newValue: Record<string, any>;
@Column({ name: 'reason', type: 'text', nullable: true })
reason: string;
@Column({ name: 'ticket_id', type: 'varchar', length: 50, nullable: true })
ticketId: string;
@Index()
@Column({ name: 'changed_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
changedAt: Date;
}

View File

@ -0,0 +1,88 @@
/**
* DataExport Entity
* GDPR/reporting data export request management
* Compatible with erp-core data-export.entity
*
* @module Audit
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
Index,
} from 'typeorm';
export type ExportType = 'report' | 'backup' | 'gdpr_request' | 'bulk_export';
export type ExportFormat = 'csv' | 'xlsx' | 'pdf' | 'json';
export type ExportStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'expired';
@Entity({ name: 'data_exports', schema: 'audit' })
export class DataExport {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'user_id', type: 'uuid' })
userId: string;
@Column({ name: 'export_type', type: 'varchar', length: 50 })
exportType: ExportType;
@Column({ name: 'export_format', type: 'varchar', length: 20, nullable: true })
exportFormat: ExportFormat;
@Column({ name: 'entity_types', type: 'text', array: true })
entityTypes: string[];
@Column({ name: 'filters', type: 'jsonb', default: {} })
filters: Record<string, any>;
@Column({ name: 'date_range_start', type: 'timestamptz', nullable: true })
dateRangeStart: Date;
@Column({ name: 'date_range_end', type: 'timestamptz', nullable: true })
dateRangeEnd: Date;
@Column({ name: 'record_count', type: 'int', nullable: true })
recordCount: number;
@Column({ name: 'file_size_bytes', type: 'bigint', nullable: true })
fileSizeBytes: number;
@Column({ name: 'file_hash', type: 'varchar', length: 64, nullable: true })
fileHash: string;
@Index()
@Column({ name: 'status', type: 'varchar', length: 20, default: 'pending' })
status: ExportStatus;
@Column({ name: 'download_url', type: 'text', nullable: true })
downloadUrl: string;
@Column({ name: 'download_expires_at', type: 'timestamptz', nullable: true })
downloadExpiresAt: Date;
@Column({ name: 'download_count', type: 'int', default: 0 })
downloadCount: number;
@Column({ name: 'ip_address', type: 'inet', nullable: true })
ipAddress: string;
@Column({ name: 'user_agent', type: 'text', nullable: true })
userAgent: string;
@Index()
@Column({ name: 'requested_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
requestedAt: Date;
@Column({ name: 'completed_at', type: 'timestamptz', nullable: true })
completedAt: Date;
@Column({ name: 'expires_at', type: 'timestamptz', nullable: true })
expiresAt: Date;
}

View File

@ -0,0 +1,63 @@
/**
* EntityChange Entity
* Data modification versioning and change history
* Compatible with erp-core entity-change.entity
*
* @module Audit
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
Index,
} from 'typeorm';
export type ChangeType = 'create' | 'update' | 'delete' | 'restore';
@Entity({ name: 'entity_changes', schema: 'audit' })
export class EntityChange {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'entity_type', type: 'varchar', length: 100 })
entityType: string;
@Index()
@Column({ name: 'entity_id', type: 'uuid' })
entityId: string;
@Column({ name: 'entity_name', type: 'varchar', length: 255, nullable: true })
entityName: string;
@Column({ name: 'version', type: 'int', default: 1 })
version: number;
@Column({ name: 'previous_version', type: 'int', nullable: true })
previousVersion: number;
@Column({ name: 'data_snapshot', type: 'jsonb' })
dataSnapshot: Record<string, any>;
@Column({ name: 'changes', type: 'jsonb', default: [] })
changes: Record<string, any>[];
@Index()
@Column({ name: 'changed_by', type: 'uuid', nullable: true })
changedBy: string;
@Column({ name: 'change_reason', type: 'text', nullable: true })
changeReason: string;
@Column({ name: 'change_type', type: 'varchar', length: 20 })
changeType: ChangeType;
@Index()
@Column({ name: 'changed_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
changedAt: Date;
}

View File

@ -0,0 +1,11 @@
/**
* Audit Entities - Export
*/
export { AuditLog, AuditAction, AuditCategory, AuditStatus } from './audit-log.entity';
export { EntityChange, ChangeType } from './entity-change.entity';
export { LoginHistory, LoginStatus, AuthMethod, MfaMethod } from './login-history.entity';
export { SensitiveDataAccess, DataType, AccessType } from './sensitive-data-access.entity';
export { DataExport, ExportType, ExportFormat, ExportStatus } from './data-export.entity';
export { PermissionChange, PermissionChangeType, PermissionScope } from './permission-change.entity';
export { ConfigChange, ConfigType } from './config-change.entity';

View File

@ -0,0 +1,114 @@
/**
* LoginHistory Entity
* Authentication event tracking with device, location and risk scoring
* Compatible with erp-core login-history.entity
*
* @module Audit
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
Index,
} from 'typeorm';
export type LoginStatus = 'success' | 'failed' | 'blocked' | 'mfa_required' | 'mfa_failed';
export type AuthMethod = 'password' | 'sso' | 'oauth' | 'mfa' | 'magic_link' | 'biometric';
export type MfaMethod = 'totp' | 'sms' | 'email' | 'push';
@Entity({ name: 'login_history', schema: 'audit' })
export class LoginHistory {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid', nullable: true })
tenantId: string;
@Index()
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string;
@Column({ name: 'email', type: 'varchar', length: 255, nullable: true })
email: string;
@Column({ name: 'username', type: 'varchar', length: 100, nullable: true })
username: string;
@Index()
@Column({ name: 'status', type: 'varchar', length: 20 })
status: LoginStatus;
@Column({ name: 'auth_method', type: 'varchar', length: 30, nullable: true })
authMethod: AuthMethod;
@Column({ name: 'oauth_provider', type: 'varchar', length: 30, nullable: true })
oauthProvider: string;
@Column({ name: 'mfa_method', type: 'varchar', length: 20, nullable: true })
mfaMethod: MfaMethod;
@Column({ name: 'mfa_verified', type: 'boolean', nullable: true })
mfaVerified: boolean;
@Column({ name: 'device_id', type: 'uuid', nullable: true })
deviceId: string;
@Column({ name: 'device_fingerprint', type: 'varchar', length: 255, nullable: true })
deviceFingerprint: string;
@Column({ name: 'device_type', type: 'varchar', length: 30, nullable: true })
deviceType: string;
@Column({ name: 'device_os', type: 'varchar', length: 50, nullable: true })
deviceOs: string;
@Column({ name: 'device_browser', type: 'varchar', length: 50, nullable: true })
deviceBrowser: string;
@Index()
@Column({ name: 'ip_address', type: 'inet', nullable: true })
ipAddress: string;
@Column({ name: 'user_agent', type: 'text', nullable: true })
userAgent: string;
@Column({ name: 'country_code', type: 'varchar', length: 2, nullable: true })
countryCode: string;
@Column({ name: 'city', type: 'varchar', length: 100, nullable: true })
city: string;
@Column({ name: 'latitude', type: 'decimal', precision: 10, scale: 8, nullable: true })
latitude: number;
@Column({ name: 'longitude', type: 'decimal', precision: 11, scale: 8, nullable: true })
longitude: number;
@Column({ name: 'risk_score', type: 'int', nullable: true })
riskScore: number;
@Column({ name: 'risk_factors', type: 'jsonb', default: [] })
riskFactors: string[];
@Index()
@Column({ name: 'is_suspicious', type: 'boolean', default: false })
isSuspicious: boolean;
@Column({ name: 'is_new_device', type: 'boolean', default: false })
isNewDevice: boolean;
@Column({ name: 'is_new_location', type: 'boolean', default: false })
isNewLocation: boolean;
@Column({ name: 'failure_reason', type: 'varchar', length: 100, nullable: true })
failureReason: string;
@Column({ name: 'failure_count', type: 'int', nullable: true })
failureCount: number;
@Index()
@Column({ name: 'attempted_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
attemptedAt: Date;
}

View File

@ -0,0 +1,71 @@
/**
* PermissionChange Entity
* Access control change auditing
* Compatible with erp-core permission-change.entity
*
* @module Audit
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
Index,
} from 'typeorm';
export type PermissionChangeType = 'role_assigned' | 'role_revoked' | 'permission_granted' | 'permission_revoked';
export type PermissionScope = 'global' | 'tenant' | 'branch';
@Entity({ name: 'permission_changes', schema: 'audit' })
export class PermissionChange {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'changed_by', type: 'uuid' })
changedBy: string;
@Index()
@Column({ name: 'target_user_id', type: 'uuid' })
targetUserId: string;
@Column({ name: 'target_user_email', type: 'varchar', length: 255, nullable: true })
targetUserEmail: string;
@Column({ name: 'change_type', type: 'varchar', length: 30 })
changeType: PermissionChangeType;
@Column({ name: 'role_id', type: 'uuid', nullable: true })
roleId: string;
@Column({ name: 'role_code', type: 'varchar', length: 50, nullable: true })
roleCode: string;
@Column({ name: 'permission_id', type: 'uuid', nullable: true })
permissionId: string;
@Column({ name: 'permission_code', type: 'varchar', length: 100, nullable: true })
permissionCode: string;
@Column({ name: 'branch_id', type: 'uuid', nullable: true })
branchId: string;
@Column({ name: 'scope', type: 'varchar', length: 30, nullable: true })
scope: PermissionScope;
@Column({ name: 'previous_roles', type: 'text', array: true, nullable: true })
previousRoles: string[];
@Column({ name: 'previous_permissions', type: 'text', array: true, nullable: true })
previousPermissions: string[];
@Column({ name: 'reason', type: 'text', nullable: true })
reason: string;
@Index()
@Column({ name: 'changed_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
changedAt: Date;
}

View File

@ -0,0 +1,70 @@
/**
* SensitiveDataAccess Entity
* Security/compliance logging for PII, financial, medical and credential access
* Compatible with erp-core sensitive-data-access.entity
*
* @module Audit
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
Index,
} from 'typeorm';
export type DataType = 'pii' | 'financial' | 'medical' | 'credentials';
export type AccessType = 'view' | 'export' | 'modify' | 'decrypt';
@Entity({ name: 'sensitive_data_access', schema: 'audit' })
export class SensitiveDataAccess {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'user_id', type: 'uuid' })
userId: string;
@Column({ name: 'session_id', type: 'uuid', nullable: true })
sessionId: string;
@Index()
@Column({ name: 'data_type', type: 'varchar', length: 100 })
dataType: DataType;
@Column({ name: 'data_category', type: 'varchar', length: 100, nullable: true })
dataCategory: string;
@Column({ name: 'entity_type', type: 'varchar', length: 100, nullable: true })
entityType: string;
@Column({ name: 'entity_id', type: 'uuid', nullable: true })
entityId: string;
@Column({ name: 'access_type', type: 'varchar', length: 30 })
accessType: AccessType;
@Column({ name: 'access_reason', type: 'text', nullable: true })
accessReason: string;
@Column({ name: 'ip_address', type: 'inet', nullable: true })
ipAddress: string;
@Column({ name: 'user_agent', type: 'text', nullable: true })
userAgent: string;
@Index()
@Column({ name: 'was_authorized', type: 'boolean', default: true })
wasAuthorized: boolean;
@Column({ name: 'denial_reason', type: 'text', nullable: true })
denialReason: string;
@Index()
@Column({ name: 'accessed_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
accessedAt: Date;
}

View File

@ -0,0 +1,72 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
} from 'typeorm';
export type BillingAlertType =
| 'usage_limit'
| 'payment_due'
| 'payment_failed'
| 'trial_ending'
| 'subscription_ending';
export type AlertSeverity = 'info' | 'warning' | 'critical';
export type AlertStatus = 'active' | 'acknowledged' | 'resolved';
/**
* Entidad para alertas de facturacion y limites de uso.
* Mapea a billing.billing_alerts (DDL: 05-billing-usage.sql)
*/
@Entity({ name: 'billing_alerts', schema: 'billing' })
export class BillingAlert {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
// Tipo de alerta
@Index()
@Column({ name: 'alert_type', type: 'varchar', length: 30 })
alertType: BillingAlertType;
// Detalles
@Column({ type: 'varchar', length: 200 })
title: string;
@Column({ type: 'text', nullable: true })
message: string;
@Column({ type: 'varchar', length: 20, default: 'info' })
severity: AlertSeverity;
// Estado
@Index()
@Column({ type: 'varchar', length: 20, default: 'active' })
status: AlertStatus;
// Notificacion
@Column({ name: 'notified_at', type: 'timestamptz', nullable: true })
notifiedAt: Date;
@Column({ name: 'acknowledged_at', type: 'timestamptz', nullable: true })
acknowledgedAt: Date;
@Column({ name: 'acknowledged_by', type: 'uuid', nullable: true })
acknowledgedBy: string;
// Metadata
@Column({ type: 'jsonb', default: {} })
metadata: Record<string, any>;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,44 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
CreateDateColumn,
Unique,
} from 'typeorm';
import { Coupon } from './coupon.entity';
import { TenantSubscription } from './tenant-subscription.entity';
@Entity({ name: 'coupon_redemptions', schema: 'billing' })
@Unique(['couponId', 'tenantId'])
export class CouponRedemption {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ name: 'coupon_id', type: 'uuid' })
couponId!: string;
@ManyToOne(() => Coupon, (coupon) => coupon.redemptions)
@JoinColumn({ name: 'coupon_id' })
coupon!: Coupon;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId!: string;
@Column({ name: 'subscription_id', type: 'uuid', nullable: true })
subscriptionId?: string;
@ManyToOne(() => TenantSubscription, { nullable: true })
@JoinColumn({ name: 'subscription_id' })
subscription?: TenantSubscription;
@Column({ name: 'discount_amount', type: 'decimal', precision: 10, scale: 2 })
discountAmount!: number;
@CreateDateColumn({ name: 'redeemed_at', type: 'timestamptz' })
redeemedAt!: Date;
@Column({ name: 'expires_at', type: 'timestamptz', nullable: true })
expiresAt?: Date;
}

View File

@ -0,0 +1,72 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
} from 'typeorm';
import { CouponRedemption } from './coupon-redemption.entity';
export type DiscountType = 'percentage' | 'fixed';
export type DurationPeriod = 'once' | 'forever' | 'months';
@Entity({ name: 'coupons', schema: 'billing' })
export class Coupon {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ type: 'varchar', length: 50, unique: true })
code!: string;
@Column({ type: 'varchar', length: 255 })
name!: string;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ name: 'discount_type', type: 'varchar', length: 20 })
discountType!: DiscountType;
@Column({ name: 'discount_value', type: 'decimal', precision: 10, scale: 2 })
discountValue!: number;
@Column({ type: 'varchar', length: 3, default: 'MXN' })
currency!: string;
@Column({ name: 'applicable_plans', type: 'uuid', array: true, default: [] })
applicablePlans!: string[];
@Column({ name: 'min_amount', type: 'decimal', precision: 10, scale: 2, default: 0 })
minAmount!: number;
@Column({ name: 'duration_period', type: 'varchar', length: 20, default: 'once' })
durationPeriod!: DurationPeriod;
@Column({ name: 'duration_months', type: 'integer', nullable: true })
durationMonths?: number;
@Column({ name: 'max_redemptions', type: 'integer', nullable: true })
maxRedemptions?: number;
@Column({ name: 'current_redemptions', type: 'integer', default: 0 })
currentRedemptions!: number;
@Column({ name: 'valid_from', type: 'timestamptz', nullable: true })
validFrom?: Date;
@Column({ name: 'valid_until', type: 'timestamptz', nullable: true })
validUntil?: Date;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive!: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt!: Date;
@OneToMany(() => CouponRedemption, (redemption) => redemption.coupon)
redemptions!: CouponRedemption[];
}

View File

@ -0,0 +1,13 @@
export { SubscriptionPlan, PlanType } from './subscription-plan.entity';
export { TenantSubscription, BillingCycle, SubscriptionStatus } from './tenant-subscription.entity';
export { UsageTracking } from './usage-tracking.entity';
export { UsageEvent, EventCategory } from './usage-event.entity';
export { Invoice, InvoiceStatus, InvoiceContext, InvoiceType, InvoiceItem } from './invoice.entity';
export { InvoiceItemType } from './invoice-item.entity';
export { BillingPaymentMethod, PaymentProvider, PaymentMethodType } from './payment-method.entity';
export { BillingAlert, BillingAlertType, AlertSeverity, AlertStatus } from './billing-alert.entity';
export { PlanFeature } from './plan-feature.entity';
export { PlanLimit, LimitType } from './plan-limit.entity';
export { Coupon, DiscountType, DurationPeriod } from './coupon.entity';
export { CouponRedemption } from './coupon-redemption.entity';
export { StripeEvent } from './stripe-event.entity';

View File

@ -0,0 +1,65 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Invoice } from '../../invoices/entities/invoice.entity';
export type InvoiceItemType = 'subscription' | 'user' | 'profile' | 'overage' | 'addon';
@Entity({ name: 'invoice_items', schema: 'billing' })
export class InvoiceItem {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'invoice_id', type: 'uuid' })
invoiceId: string;
// Descripcion
@Column({ type: 'varchar', length: 500 })
description: string;
@Index()
@Column({ name: 'item_type', type: 'varchar', length: 30 })
itemType: InvoiceItemType;
// Cantidades
@Column({ type: 'integer', default: 1 })
quantity: number;
@Column({ name: 'unit_price', type: 'decimal', precision: 12, scale: 2 })
unitPrice: number;
@Column({ type: 'decimal', precision: 12, scale: 2 })
subtotal: number;
// Detalles adicionales
@Column({ name: 'profile_code', type: 'varchar', length: 10, nullable: true })
profileCode: string;
@Column({ type: 'varchar', length: 20, nullable: true })
platform: string;
@Column({ name: 'period_start', type: 'date', nullable: true })
periodStart: Date;
@Column({ name: 'period_end', type: 'date', nullable: true })
periodEnd: Date;
// Metadata
@Column({ type: 'jsonb', default: {} })
metadata: Record<string, any>;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
// Relaciones
@ManyToOne(() => Invoice, (invoice) => invoice.items, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'invoice_id' })
invoice: Invoice;
}

View File

@ -0,0 +1,17 @@
/**
* @deprecated Use Invoice from 'modules/invoices/entities' instead.
*
* This entity has been unified with the commercial Invoice entity.
* Both SaaS billing and commercial invoices now use the same table.
*
* Migration guide:
* - Import from: import { Invoice, InvoiceStatus, InvoiceContext } from '../../invoices/entities/invoice.entity';
* - Set invoiceContext: 'saas' for SaaS billing invoices
* - Use subscriptionId, periodStart, periodEnd for SaaS-specific fields
*/
// Re-export from unified invoice entity
export { Invoice, InvoiceStatus, InvoiceContext, InvoiceType } from '../../invoices/entities/invoice.entity';
// Re-export InvoiceItem as well since it's used together
export { InvoiceItem } from './invoice-item.entity';

View File

@ -0,0 +1,85 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
} from 'typeorm';
export type PaymentProvider = 'stripe' | 'mercadopago' | 'bank_transfer';
export type PaymentMethodType = 'card' | 'bank_account' | 'wallet';
/**
* Entidad para metodos de pago guardados por tenant.
* Almacena informacion tokenizada/encriptada de metodos de pago.
* Mapea a billing.payment_methods (DDL: 05-billing-usage.sql)
*/
@Entity({ name: 'payment_methods', schema: 'billing' })
export class BillingPaymentMethod {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
// Proveedor
@Index()
@Column({ type: 'varchar', length: 30 })
provider: PaymentProvider;
// Tipo
@Column({ name: 'method_type', type: 'varchar', length: 20 })
methodType: PaymentMethodType;
// Datos tokenizados del proveedor
@Column({ name: 'provider_customer_id', type: 'varchar', length: 255, nullable: true })
providerCustomerId: string;
@Column({ name: 'provider_method_id', type: 'varchar', length: 255, nullable: true })
providerMethodId: string;
// Display info (no sensible)
@Column({ name: 'display_name', type: 'varchar', length: 100, nullable: true })
displayName: string;
@Column({ name: 'card_brand', type: 'varchar', length: 20, nullable: true })
cardBrand: string;
@Column({ name: 'card_last_four', type: 'varchar', length: 4, nullable: true })
cardLastFour: string;
@Column({ name: 'card_exp_month', type: 'integer', nullable: true })
cardExpMonth: number;
@Column({ name: 'card_exp_year', type: 'integer', nullable: true })
cardExpYear: number;
@Column({ name: 'bank_name', type: 'varchar', length: 100, nullable: true })
bankName: string;
@Column({ name: 'bank_last_four', type: 'varchar', length: 4, nullable: true })
bankLastFour: string;
// Estado
@Index()
@Column({ name: 'is_default', type: 'boolean', default: false })
isDefault: boolean;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'is_verified', type: 'boolean', default: false })
isVerified: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date;
}

View File

@ -0,0 +1,61 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { SubscriptionPlan } from './subscription-plan.entity';
/**
* PlanFeature Entity
* Maps to billing.plan_features DDL table
* Features disponibles por plan de suscripcion
* Propagated from template-saas HU-REFACT-005
*/
@Entity({ schema: 'billing', name: 'plan_features' })
@Index('idx_plan_features_plan', ['planId'])
@Index('idx_plan_features_key', ['featureKey'])
export class PlanFeature {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', nullable: false, name: 'plan_id' })
planId: string;
@Column({ type: 'varchar', length: 100, nullable: false, name: 'feature_key' })
featureKey: string;
@Column({ type: 'varchar', length: 255, nullable: false, name: 'feature_name' })
featureName: string;
@Column({ type: 'varchar', length: 100, nullable: true })
category: string | null;
@Column({ type: 'boolean', default: true })
enabled: boolean;
@Column({ type: 'jsonb', default: {} })
configuration: Record<string, any>;
@Column({ type: 'text', nullable: true })
description: string | null;
@Column({ type: 'jsonb', default: {} })
metadata: Record<string, any>;
// Relaciones
@ManyToOne(() => SubscriptionPlan, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'plan_id' })
plan: SubscriptionPlan;
// Timestamps
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,52 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { SubscriptionPlan } from './subscription-plan.entity';
export type LimitType = 'monthly' | 'daily' | 'total' | 'per_user';
@Entity({ name: 'plan_limits', schema: 'billing' })
export class PlanLimit {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ name: 'plan_id', type: 'uuid' })
planId!: string;
@ManyToOne(() => SubscriptionPlan, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'plan_id' })
plan!: SubscriptionPlan;
@Column({ name: 'limit_key', type: 'varchar', length: 100 })
limitKey!: string;
@Column({ name: 'limit_name', type: 'varchar', length: 255 })
limitName!: string;
@Column({ name: 'limit_value', type: 'integer' })
limitValue!: number;
@Column({ name: 'limit_type', type: 'varchar', length: 50, default: 'monthly' })
limitType!: LimitType;
@Column({ name: 'allow_overage', type: 'boolean', default: false })
allowOverage!: boolean;
@Column({ name: 'overage_unit_price', type: 'decimal', precision: 10, scale: 4, default: 0 })
overageUnitPrice!: number;
@Column({ name: 'overage_currency', type: 'varchar', length: 3, default: 'MXN' })
overageCurrency!: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt!: Date;
}

View File

@ -0,0 +1,43 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
} from 'typeorm';
@Entity({ name: 'stripe_events', schema: 'billing' })
export class StripeEvent {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ name: 'stripe_event_id', type: 'varchar', length: 255, unique: true })
@Index()
stripeEventId!: string;
@Column({ name: 'event_type', type: 'varchar', length: 100 })
@Index()
eventType!: string;
@Column({ name: 'api_version', type: 'varchar', length: 20, nullable: true })
apiVersion?: string;
@Column({ type: 'jsonb' })
data!: Record<string, any>;
@Column({ type: 'boolean', default: false })
@Index()
processed!: boolean;
@Column({ name: 'processed_at', type: 'timestamptz', nullable: true })
processedAt?: Date;
@Column({ name: 'error_message', type: 'text', nullable: true })
errorMessage?: string;
@Column({ name: 'retry_count', type: 'integer', default: 0 })
retryCount!: number;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt!: Date;
}

View File

@ -0,0 +1,83 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
} from 'typeorm';
export type PlanType = 'saas' | 'on_premise' | 'hybrid';
@Entity({ name: 'subscription_plans', schema: 'billing' })
export class SubscriptionPlan {
@PrimaryGeneratedColumn('uuid')
id: string;
// Identificacion
@Index({ unique: true })
@Column({ type: 'varchar', length: 30 })
code: string;
@Column({ type: 'varchar', length: 100 })
name: string;
@Column({ type: 'text', nullable: true })
description: string;
// Tipo
@Column({ name: 'plan_type', type: 'varchar', length: 20, default: 'saas' })
planType: PlanType;
// Precios base
@Column({ name: 'base_monthly_price', type: 'decimal', precision: 12, scale: 2, default: 0 })
baseMonthlyPrice: number;
@Column({ name: 'base_annual_price', type: 'decimal', precision: 12, scale: 2, nullable: true })
baseAnnualPrice: number;
@Column({ name: 'setup_fee', type: 'decimal', precision: 12, scale: 2, default: 0 })
setupFee: number;
// Limites base
@Column({ name: 'max_users', type: 'integer', default: 5 })
maxUsers: number;
@Column({ name: 'max_branches', type: 'integer', default: 1 })
maxBranches: number;
@Column({ name: 'storage_gb', type: 'integer', default: 10 })
storageGb: number;
@Column({ name: 'api_calls_monthly', type: 'integer', default: 10000 })
apiCallsMonthly: number;
// Modulos incluidos
@Column({ name: 'included_modules', type: 'text', array: true, default: [] })
includedModules: string[];
// Plataformas incluidas
@Column({ name: 'included_platforms', type: 'text', array: true, default: ['web'] })
includedPlatforms: string[];
// Features
@Column({ type: 'jsonb', default: {} })
features: Record<string, boolean>;
// Estado
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'is_public', type: 'boolean', default: true })
isPublic: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date;
}

View File

@ -0,0 +1,132 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
Unique,
} from 'typeorm';
import { SubscriptionPlan } from './subscription-plan.entity';
export type BillingCycle = 'monthly' | 'annual';
export type SubscriptionStatus = 'trial' | 'active' | 'past_due' | 'cancelled' | 'suspended';
@Entity({ name: 'tenant_subscriptions', schema: 'billing' })
@Unique(['tenantId'])
export class TenantSubscription {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'plan_id', type: 'uuid' })
planId: string;
// Periodo
@Column({ name: 'billing_cycle', type: 'varchar', length: 20, default: 'monthly' })
billingCycle: BillingCycle;
@Column({ name: 'current_period_start', type: 'timestamptz' })
currentPeriodStart: Date;
@Column({ name: 'current_period_end', type: 'timestamptz' })
currentPeriodEnd: Date;
// Estado
@Index()
@Column({ type: 'varchar', length: 20, default: 'active' })
status: SubscriptionStatus;
// Trial
@Column({ name: 'trial_start', type: 'timestamptz', nullable: true })
trialStart: Date;
@Column({ name: 'trial_end', type: 'timestamptz', nullable: true })
trialEnd: Date;
// Configuracion de facturacion
@Column({ name: 'billing_email', type: 'varchar', length: 255, nullable: true })
billingEmail: string;
@Column({ name: 'billing_name', type: 'varchar', length: 200, nullable: true })
billingName: string;
@Column({ name: 'billing_address', type: 'jsonb', default: {} })
billingAddress: Record<string, any>;
@Column({ name: 'tax_id', type: 'varchar', length: 20, nullable: true })
taxId: string; // RFC para Mexico
// Metodo de pago
@Column({ name: 'payment_method_id', type: 'uuid', nullable: true })
paymentMethodId: string;
@Column({ name: 'payment_provider', type: 'varchar', length: 30, nullable: true })
paymentProvider: string; // stripe, mercadopago, bank_transfer
// Stripe integration
@Index()
@Column({ name: 'stripe_customer_id', type: 'varchar', length: 255, nullable: true })
stripeCustomerId?: string;
@Index()
@Column({ name: 'stripe_subscription_id', type: 'varchar', length: 255, nullable: true })
stripeSubscriptionId?: string;
@Column({ name: 'last_payment_at', type: 'timestamptz', nullable: true })
lastPaymentAt?: Date;
@Column({ name: 'last_payment_amount', type: 'decimal', precision: 12, scale: 2, nullable: true })
lastPaymentAmount?: number;
// Precios actuales
@Column({ name: 'current_price', type: 'decimal', precision: 12, scale: 2 })
currentPrice: number;
@Column({ name: 'discount_percent', type: 'decimal', precision: 5, scale: 2, default: 0 })
discountPercent: number;
@Column({ name: 'discount_reason', type: 'varchar', length: 100, nullable: true })
discountReason: string;
// Uso contratado
@Column({ name: 'contracted_users', type: 'integer', nullable: true })
contractedUsers: number;
@Column({ name: 'contracted_branches', type: 'integer', nullable: true })
contractedBranches: number;
// Facturacion automatica
@Column({ name: 'auto_renew', type: 'boolean', default: true })
autoRenew: boolean;
@Column({ name: 'next_invoice_date', type: 'date', nullable: true })
nextInvoiceDate: Date;
// Cancelacion
@Column({ name: 'cancel_at_period_end', type: 'boolean', default: false })
cancelAtPeriodEnd: boolean;
@Column({ name: 'cancelled_at', type: 'timestamptz', nullable: true })
cancelledAt: Date;
@Column({ name: 'cancellation_reason', type: 'text', nullable: true })
cancellationReason: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
// Relaciones
@ManyToOne(() => SubscriptionPlan)
@JoinColumn({ name: 'plan_id' })
plan: SubscriptionPlan;
}

View File

@ -0,0 +1,73 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
} from 'typeorm';
export type EventCategory = 'user' | 'api' | 'storage' | 'transaction' | 'mobile';
/**
* Entidad para eventos de uso en tiempo real.
* Utilizada para calculo de billing y tracking granular.
* Mapea a billing.usage_events (DDL: 05-billing-usage.sql)
*/
@Entity({ name: 'usage_events', schema: 'billing' })
export class UsageEvent {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string;
@Column({ name: 'device_id', type: 'uuid', nullable: true })
deviceId: string;
@Column({ name: 'branch_id', type: 'uuid', nullable: true })
branchId: string;
// Evento
@Index()
@Column({ name: 'event_type', type: 'varchar', length: 50 })
eventType: string; // login, api_call, document_upload, sale, invoice, sync
@Index()
@Column({ name: 'event_category', type: 'varchar', length: 30 })
eventCategory: EventCategory;
// Detalles
@Column({ name: 'profile_code', type: 'varchar', length: 10, nullable: true })
profileCode: string;
@Column({ type: 'varchar', length: 20, nullable: true })
platform: string;
@Column({ name: 'resource_id', type: 'uuid', nullable: true })
resourceId: string;
@Column({ name: 'resource_type', type: 'varchar', length: 50, nullable: true })
resourceType: string;
// Metricas
@Column({ type: 'integer', default: 1 })
quantity: number;
@Column({ name: 'bytes_used', type: 'bigint', default: 0 })
bytesUsed: number;
@Column({ name: 'duration_ms', type: 'integer', nullable: true })
durationMs: number;
// Metadata
@Column({ type: 'jsonb', default: {} })
metadata: Record<string, any>;
@Index()
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
}

View File

@ -0,0 +1,91 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
Unique,
} from 'typeorm';
@Entity({ name: 'usage_tracking', schema: 'billing' })
@Unique(['tenantId', 'periodStart'])
export class UsageTracking {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
// Periodo
@Index()
@Column({ name: 'period_start', type: 'date' })
periodStart: Date;
@Column({ name: 'period_end', type: 'date' })
periodEnd: Date;
// Usuarios
@Column({ name: 'active_users', type: 'integer', default: 0 })
activeUsers: number;
@Column({ name: 'peak_concurrent_users', type: 'integer', default: 0 })
peakConcurrentUsers: number;
// Por perfil
@Column({ name: 'users_by_profile', type: 'jsonb', default: {} })
usersByProfile: Record<string, number>; // {"ADM": 2, "VNT": 5, "ALM": 3}
// Por plataforma
@Column({ name: 'users_by_platform', type: 'jsonb', default: {} })
usersByPlatform: Record<string, number>; // {"web": 8, "mobile": 5, "desktop": 0}
// Sucursales
@Column({ name: 'active_branches', type: 'integer', default: 0 })
activeBranches: number;
// Storage
@Column({ name: 'storage_used_gb', type: 'decimal', precision: 10, scale: 2, default: 0 })
storageUsedGb: number;
@Column({ name: 'documents_count', type: 'integer', default: 0 })
documentsCount: number;
// API
@Column({ name: 'api_calls', type: 'integer', default: 0 })
apiCalls: number;
@Column({ name: 'api_errors', type: 'integer', default: 0 })
apiErrors: number;
// Transacciones
@Column({ name: 'sales_count', type: 'integer', default: 0 })
salesCount: number;
@Column({ name: 'sales_amount', type: 'decimal', precision: 14, scale: 2, default: 0 })
salesAmount: number;
@Column({ name: 'invoices_generated', type: 'integer', default: 0 })
invoicesGenerated: number;
// Mobile
@Column({ name: 'mobile_sessions', type: 'integer', default: 0 })
mobileSessions: number;
@Column({ name: 'offline_syncs', type: 'integer', default: 0 })
offlineSyncs: number;
@Column({ name: 'payment_transactions', type: 'integer', default: 0 })
paymentTransactions: number;
// Calculado
@Column({ name: 'total_billable_amount', type: 'decimal', precision: 12, scale: 2, default: 0 })
totalBillableAmount: number;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,35 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
} from 'typeorm';
@Entity({ schema: 'core', name: 'countries' })
@Index('idx_countries_code', ['code'], { unique: true })
export class Country {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 2, nullable: false, unique: true })
code: string;
@Column({ type: 'varchar', length: 255, nullable: false })
name: string;
@Column({ type: 'varchar', length: 10, nullable: true, name: 'phone_code' })
phoneCode: string | null;
@Column({
type: 'varchar',
length: 3,
nullable: true,
name: 'currency_code',
})
currencyCode: string | null;
// Audit fields
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
createdAt: Date;
}

View File

@ -0,0 +1,55 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Currency } from './currency.entity';
export type RateSource = 'manual' | 'banxico' | 'xe' | 'openexchange';
@Entity({ schema: 'core', name: 'currency_rates' })
@Index('idx_currency_rates_tenant', ['tenantId'])
@Index('idx_currency_rates_from', ['fromCurrencyId'])
@Index('idx_currency_rates_to', ['toCurrencyId'])
@Index('idx_currency_rates_date', ['rateDate'])
@Index('idx_currency_rates_lookup', ['fromCurrencyId', 'toCurrencyId', 'rateDate'])
export class CurrencyRate {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', name: 'tenant_id', nullable: true })
tenantId: string | null;
@Column({ type: 'uuid', name: 'from_currency_id', nullable: false })
fromCurrencyId: string;
@ManyToOne(() => Currency)
@JoinColumn({ name: 'from_currency_id' })
fromCurrency: Currency;
@Column({ type: 'uuid', name: 'to_currency_id', nullable: false })
toCurrencyId: string;
@ManyToOne(() => Currency)
@JoinColumn({ name: 'to_currency_id' })
toCurrency: Currency;
@Column({ type: 'decimal', precision: 18, scale: 8, nullable: false })
rate: number;
@Column({ type: 'date', name: 'rate_date', nullable: false })
rateDate: Date;
@Column({ type: 'varchar', length: 50, default: 'manual' })
source: RateSource;
@Column({ type: 'uuid', name: 'created_by', nullable: true })
createdBy: string | null;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
}

View File

@ -0,0 +1,43 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
} from 'typeorm';
@Entity({ schema: 'core', name: 'currencies' })
@Index('idx_currencies_code', ['code'], { unique: true })
@Index('idx_currencies_active', ['active'])
export class Currency {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 3, nullable: false, unique: true })
code: string;
@Column({ type: 'varchar', length: 100, nullable: false })
name: string;
@Column({ type: 'varchar', length: 10, nullable: false })
symbol: string;
@Column({ type: 'integer', nullable: false, default: 2, name: 'decimals' })
decimals: number;
@Column({
type: 'decimal',
precision: 12,
scale: 6,
nullable: true,
default: 0.01,
})
rounding: number;
@Column({ type: 'boolean', nullable: false, default: true })
active: boolean;
// Audit fields
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
createdAt: Date;
}

View File

@ -0,0 +1,163 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
} from 'typeorm';
/**
* Tipo de descuento
*/
export enum DiscountType {
PERCENTAGE = 'percentage', // Porcentaje del total
FIXED = 'fixed', // Monto fijo
PRICE_OVERRIDE = 'price_override', // Precio especial
}
/**
* Aplicacion del descuento
*/
export enum DiscountAppliesTo {
ALL = 'all', // Todos los productos
CATEGORY = 'category', // Categoria especifica
PRODUCT = 'product', // Producto especifico
CUSTOMER = 'customer', // Cliente especifico
CUSTOMER_GROUP = 'customer_group', // Grupo de clientes
}
/**
* Condicion de activacion
*/
export enum DiscountCondition {
NONE = 'none', // Sin condicion
MIN_QUANTITY = 'min_quantity', // Cantidad minima
MIN_AMOUNT = 'min_amount', // Monto minimo
DATE_RANGE = 'date_range', // Rango de fechas
FIRST_PURCHASE = 'first_purchase', // Primera compra
}
/**
* Regla de descuento
*/
@Entity({ schema: 'core', name: 'discount_rules' })
@Index('idx_discount_rules_tenant_id', ['tenantId'])
@Index('idx_discount_rules_code_tenant', ['tenantId', 'code'], { unique: true })
@Index('idx_discount_rules_active', ['tenantId', 'isActive'])
@Index('idx_discount_rules_dates', ['tenantId', 'startDate', 'endDate'])
@Index('idx_discount_rules_priority', ['tenantId', 'priority'])
export class DiscountRule {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', nullable: false, name: 'tenant_id' })
tenantId: string;
@Column({ type: 'uuid', nullable: true, name: 'company_id' })
companyId: string | null;
@Column({ type: 'varchar', length: 50, nullable: false })
code: string;
@Column({ type: 'varchar', length: 255, nullable: false })
name: string;
@Column({ type: 'text', nullable: true })
description: string | null;
@Column({
type: 'enum',
enum: DiscountType,
default: DiscountType.PERCENTAGE,
name: 'discount_type',
})
discountType: DiscountType;
@Column({
type: 'decimal',
precision: 15,
scale: 4,
nullable: false,
name: 'discount_value',
})
discountValue: number;
@Column({
type: 'decimal',
precision: 15,
scale: 2,
nullable: true,
name: 'max_discount_amount',
})
maxDiscountAmount: number | null;
@Column({
type: 'enum',
enum: DiscountAppliesTo,
default: DiscountAppliesTo.ALL,
name: 'applies_to',
})
appliesTo: DiscountAppliesTo;
@Column({ type: 'uuid', nullable: true, name: 'applies_to_id' })
appliesToId: string | null;
@Column({
type: 'enum',
enum: DiscountCondition,
default: DiscountCondition.NONE,
name: 'condition_type',
})
conditionType: DiscountCondition;
@Column({
type: 'decimal',
precision: 15,
scale: 4,
nullable: true,
name: 'condition_value',
})
conditionValue: number | null;
@Column({ type: 'timestamp', nullable: true, name: 'start_date' })
startDate: Date | null;
@Column({ type: 'timestamp', nullable: true, name: 'end_date' })
endDate: Date | null;
@Column({ type: 'integer', nullable: false, default: 10 })
priority: number;
@Column({ type: 'boolean', nullable: false, default: true, name: 'combinable' })
combinable: boolean;
@Column({ type: 'integer', nullable: true, name: 'usage_limit' })
usageLimit: number | null;
@Column({ type: 'integer', nullable: false, default: 0, name: 'usage_count' })
usageCount: number;
@Column({ type: 'boolean', nullable: false, default: true, name: 'is_active' })
isActive: boolean;
// Audit fields
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
createdAt: Date;
@Column({ type: 'uuid', nullable: true, name: 'created_by' })
createdBy: string | null;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', nullable: true })
updatedAt: Date | null;
@Column({ type: 'uuid', nullable: true, name: 'updated_by' })
updatedBy: string | null;
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date | null;
@Column({ type: 'uuid', nullable: true, name: 'deleted_by' })
deletedBy: string | null;
}

View File

@ -0,0 +1,19 @@
/**
* Core Entities Index
*/
// Existing entities
export { Tenant } from './tenant.entity';
export { User } from './user.entity';
// Catalog entities (propagated from erp-core)
export { Country } from './country.entity';
export { Currency } from './currency.entity';
export { CurrencyRate, RateSource } from './currency-rate.entity';
export { DiscountRule, DiscountType, DiscountAppliesTo, DiscountCondition } from './discount-rule.entity';
export { PaymentTerm, PaymentTermLine, PaymentTermLineType } from './payment-term.entity';
export { ProductCategory } from './product-category.entity';
export { Sequence, ResetPeriod } from './sequence.entity';
export { State } from './state.entity';
export { Uom, UomType } from './uom.entity';
export { UomCategory } from './uom-category.entity';

View File

@ -0,0 +1,144 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
OneToMany,
} from 'typeorm';
/**
* Tipo de calculo para la linea del termino de pago
*/
export enum PaymentTermLineType {
BALANCE = 'balance', // Saldo restante
PERCENT = 'percent', // Porcentaje del total
FIXED = 'fixed', // Monto fijo
}
/**
* Linea de termino de pago (para terminos con multiples vencimientos)
*/
@Entity({ schema: 'core', name: 'payment_term_lines' })
@Index('idx_payment_term_lines_term', ['paymentTermId'])
export class PaymentTermLine {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', nullable: false, name: 'payment_term_id' })
paymentTermId: string;
@Column({ type: 'integer', nullable: false, default: 1 })
sequence: number;
@Column({
type: 'enum',
enum: PaymentTermLineType,
default: PaymentTermLineType.BALANCE,
name: 'line_type',
})
lineType: PaymentTermLineType;
@Column({
type: 'decimal',
precision: 5,
scale: 2,
nullable: true,
name: 'value_percent',
})
valuePercent: number | null;
@Column({
type: 'decimal',
precision: 15,
scale: 2,
nullable: true,
name: 'value_amount',
})
valueAmount: number | null;
@Column({ type: 'integer', nullable: false, default: 0 })
days: number;
@Column({ type: 'integer', nullable: true, name: 'day_of_month' })
dayOfMonth: number | null;
@Column({ type: 'boolean', nullable: false, default: false, name: 'end_of_month' })
endOfMonth: boolean;
}
/**
* Termino de pago (Net 30, 50% advance + 50% on delivery, etc.)
*/
@Entity({ schema: 'core', name: 'payment_terms' })
@Index('idx_payment_terms_tenant_id', ['tenantId'])
@Index('idx_payment_terms_code_tenant', ['tenantId', 'code'], { unique: true })
@Index('idx_payment_terms_active', ['tenantId', 'isActive'])
export class PaymentTerm {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', nullable: false, name: 'tenant_id' })
tenantId: string;
@Column({ type: 'uuid', nullable: true, name: 'company_id' })
companyId: string | null;
@Column({ type: 'varchar', length: 50, nullable: false })
code: string;
@Column({ type: 'varchar', length: 255, nullable: false })
name: string;
@Column({ type: 'text', nullable: true })
description: string | null;
@Column({ type: 'integer', nullable: false, default: 0, name: 'due_days' })
dueDays: number;
@Column({
type: 'decimal',
precision: 5,
scale: 2,
nullable: true,
default: 0,
name: 'discount_percent',
})
discountPercent: number | null;
@Column({ type: 'integer', nullable: true, default: 0, name: 'discount_days' })
discountDays: number | null;
@Column({ type: 'boolean', nullable: false, default: false, name: 'is_immediate' })
isImmediate: boolean;
@Column({ type: 'boolean', nullable: false, default: true, name: 'is_active' })
isActive: boolean;
@Column({ type: 'integer', nullable: false, default: 0 })
sequence: number;
@OneToMany(() => PaymentTermLine, (line) => line.paymentTermId, { eager: true })
lines: PaymentTermLine[];
// Audit fields
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
createdAt: Date;
@Column({ type: 'uuid', nullable: true, name: 'created_by' })
createdBy: string | null;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', nullable: true })
updatedAt: Date | null;
@Column({ type: 'uuid', nullable: true, name: 'updated_by' })
updatedBy: string | null;
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date | null;
@Column({ type: 'uuid', nullable: true, name: 'deleted_by' })
deletedBy: string | null;
}

View File

@ -0,0 +1,79 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
OneToMany,
} from 'typeorm';
@Entity({ schema: 'core', name: 'product_categories' })
@Index('idx_product_categories_tenant_id', ['tenantId'])
@Index('idx_product_categories_parent_id', ['parentId'])
@Index('idx_product_categories_code_tenant', ['tenantId', 'code'], {
unique: true,
})
@Index('idx_product_categories_active', ['tenantId', 'active'], {
where: 'deleted_at IS NULL',
})
export class ProductCategory {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', nullable: false, name: 'tenant_id' })
tenantId: string;
@Column({ type: 'varchar', length: 255, nullable: false })
name: string;
@Column({ type: 'varchar', length: 50, nullable: true })
code: string | null;
@Column({ type: 'uuid', nullable: true, name: 'parent_id' })
parentId: string | null;
@Column({ type: 'text', nullable: true, name: 'full_path' })
fullPath: string | null;
@Column({ type: 'text', nullable: true })
notes: string | null;
@Column({ type: 'boolean', nullable: false, default: true })
active: boolean;
// Relations
@ManyToOne(() => ProductCategory, (category) => category.children, {
nullable: true,
})
@JoinColumn({ name: 'parent_id' })
parent: ProductCategory | null;
@OneToMany(() => ProductCategory, (category) => category.parent)
children: ProductCategory[];
// Audit fields
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
createdAt: Date;
@Column({ type: 'uuid', nullable: true, name: 'created_by' })
createdBy: string | null;
@UpdateDateColumn({
name: 'updated_at',
type: 'timestamp',
nullable: true,
})
updatedAt: Date | null;
@Column({ type: 'uuid', nullable: true, name: 'updated_by' })
updatedBy: string | null;
@Column({ type: 'timestamp', nullable: true, name: 'deleted_at' })
deletedAt: Date | null;
@Column({ type: 'uuid', nullable: true, name: 'deleted_by' })
deletedBy: string | null;
}

View File

@ -0,0 +1,83 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
} from 'typeorm';
export enum ResetPeriod {
NONE = 'none',
YEAR = 'year',
MONTH = 'month',
}
@Entity({ schema: 'core', name: 'sequences' })
@Index('idx_sequences_tenant_id', ['tenantId'])
@Index('idx_sequences_code_tenant', ['tenantId', 'code'], { unique: true })
@Index('idx_sequences_active', ['tenantId', 'isActive'])
export class Sequence {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', nullable: false, name: 'tenant_id' })
tenantId: string;
@Column({ type: 'uuid', nullable: true, name: 'company_id' })
companyId: string | null;
@Column({ type: 'varchar', length: 100, nullable: false })
code: string;
@Column({ type: 'varchar', length: 255, nullable: false })
name: string;
@Column({ type: 'varchar', length: 50, nullable: true })
prefix: string | null;
@Column({ type: 'varchar', length: 50, nullable: true })
suffix: string | null;
@Column({ type: 'integer', nullable: false, default: 1, name: 'next_number' })
nextNumber: number;
@Column({ type: 'integer', nullable: false, default: 4 })
padding: number;
@Column({
type: 'enum',
enum: ResetPeriod,
nullable: true,
default: ResetPeriod.NONE,
name: 'reset_period',
})
resetPeriod: ResetPeriod | null;
@Column({
type: 'timestamp',
nullable: true,
name: 'last_reset_date',
})
lastResetDate: Date | null;
@Column({ type: 'boolean', nullable: false, default: true, name: 'is_active' })
isActive: boolean;
// Audit fields
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
createdAt: Date;
@Column({ type: 'uuid', nullable: true, name: 'created_by' })
createdBy: string | null;
@UpdateDateColumn({
name: 'updated_at',
type: 'timestamp',
nullable: true,
})
updatedAt: Date | null;
@Column({ type: 'uuid', nullable: true, name: 'updated_by' })
updatedBy: string | null;
}

View File

@ -0,0 +1,45 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Country } from './country.entity';
@Entity({ schema: 'core', name: 'states' })
@Index('idx_states_country', ['countryId'])
@Index('idx_states_code', ['code'])
@Index('idx_states_country_code', ['countryId', 'code'], { unique: true })
export class State {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', name: 'country_id', nullable: false })
countryId: string;
@ManyToOne(() => Country, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'country_id' })
country: Country;
@Column({ type: 'varchar', length: 10, nullable: false })
code: string;
@Column({ type: 'varchar', length: 255, nullable: false })
name: string;
@Column({ type: 'varchar', length: 50, nullable: true })
timezone: string | null;
@Column({ type: 'boolean', name: 'is_active', default: true })
isActive: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,50 @@
/**
* Tenant Entity
* Entidad para multi-tenancy
*
* @module Core
* @table core.tenants
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
Index,
} from 'typeorm';
import { User } from './user.entity';
@Entity({ schema: 'auth', name: 'tenants' })
export class Tenant {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 50, unique: true })
@Index()
code: string;
@Column({ type: 'varchar', length: 200 })
name: string;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ type: 'jsonb', default: {} })
settings: Record<string, unknown>;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date;
// Relations
@OneToMany(() => User, (user) => user.tenant)
users: User[];
}

View File

@ -0,0 +1,45 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
OneToMany,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Uom } from './uom.entity';
import { Tenant } from './tenant.entity';
@Entity({ schema: 'core', name: 'uom_categories' })
@Index('idx_uom_categories_tenant', ['tenantId'])
@Index('idx_uom_categories_tenant_name', ['tenantId', 'name'], { unique: true })
export class UomCategory {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', nullable: false, name: 'tenant_id' })
tenantId: string;
@Column({ type: 'varchar', length: 100, nullable: false })
name: string;
@Column({ type: 'text', nullable: true })
description: string | null;
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@OneToMany(() => Uom, (uom) => uom.category)
uoms: Uom[];
// Audit fields
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,89 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { UomCategory } from './uom-category.entity';
import { Tenant } from './tenant.entity';
export enum UomType {
REFERENCE = 'reference',
BIGGER = 'bigger',
SMALLER = 'smaller',
}
@Entity({ schema: 'core', name: 'uom' })
@Index('idx_uom_tenant', ['tenantId'])
@Index('idx_uom_category_id', ['categoryId'])
@Index('idx_uom_code', ['code'])
@Index('idx_uom_active', ['active'])
@Index('idx_uom_tenant_category_name', ['tenantId', 'categoryId', 'name'], { unique: true })
export class Uom {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', nullable: false, name: 'tenant_id' })
tenantId: string;
@Column({ type: 'uuid', nullable: false, name: 'category_id' })
categoryId: string;
@Column({ type: 'varchar', length: 100, nullable: false })
name: string;
@Column({ type: 'varchar', length: 20, nullable: true })
code: string | null;
@Column({
type: 'enum',
enum: UomType,
nullable: false,
default: UomType.REFERENCE,
name: 'uom_type',
})
uomType: UomType;
@Column({
type: 'decimal',
precision: 12,
scale: 6,
nullable: false,
default: 1.0,
})
factor: number;
@Column({
type: 'decimal',
precision: 12,
scale: 6,
nullable: true,
default: 0.01,
})
rounding: number;
@Column({ type: 'boolean', nullable: false, default: true })
active: boolean;
// Relations
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@ManyToOne(() => UomCategory, (category) => category.uoms, {
nullable: false,
})
@JoinColumn({ name: 'category_id' })
category: UomCategory;
// Audit fields
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,78 @@
/**
* User Entity
* Entidad de usuarios del sistema
*
* @module Core
* @table core.users
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Index,
} from 'typeorm';
import { Tenant } from './tenant.entity';
@Entity({ schema: 'auth', name: 'users' })
@Index(['tenantId', 'email'], { unique: true })
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid', nullable: true })
tenantId: string;
@Column({ type: 'varchar', length: 255 })
email: string;
@Column({ type: 'varchar', length: 100, nullable: true })
username: string;
@Column({ name: 'password_hash', type: 'varchar', length: 255, select: false })
passwordHash: string;
@Column({ name: 'first_name', type: 'varchar', length: 100, nullable: true })
firstName: string;
@Column({ name: 'last_name', type: 'varchar', length: 100, nullable: true })
lastName: string;
@Column({ type: 'varchar', array: true, default: ['viewer'] })
roles: string[];
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'last_login_at', type: 'timestamptz', nullable: true })
lastLoginAt: Date;
@Column({ name: 'default_tenant_id', type: 'uuid', nullable: true })
defaultTenantId: string;
@Column({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date;
// Placeholder para relación de roles (se implementará en ST-004)
userRoles?: { role: { code: string } }[];
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
// Relations
@ManyToOne(() => Tenant, (tenant) => tenant.users)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
// Computed property
get fullName(): string {
return [this.firstName, this.lastName].filter(Boolean).join(' ') || this.email;
}
}

View File

@ -0,0 +1,8 @@
/**
* Invoices Entities - Export
*/
export { Invoice, InvoiceType, InvoiceStatus, InvoiceContext } from './invoice.entity';
export { InvoiceItem } from './invoice-item.entity';
export { Payment } from './payment.entity';
export { PaymentAllocation } from './payment-allocation.entity';

View File

@ -0,0 +1,95 @@
/**
* InvoiceItem Entity
* Line items for unified invoices
* Compatible with erp-core invoice-item.entity
*
* @module Invoices
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Invoice } from './invoice.entity';
@Entity({ name: 'invoice_items', schema: 'billing' })
export class InvoiceItem {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'invoice_id', type: 'uuid' })
invoiceId: string;
@ManyToOne(() => Invoice, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'invoice_id' })
invoice: Invoice;
@Index()
@Column({ name: 'product_id', type: 'uuid', nullable: true })
productId?: string;
@Column({ name: 'line_number', type: 'int', default: 1 })
lineNumber: number;
@Column({ name: 'product_sku', type: 'varchar', length: 50, nullable: true })
productSku?: string;
@Column({ name: 'product_name', type: 'varchar', length: 200 })
productName: string;
@Column({ type: 'text', nullable: true })
description?: string;
// SAT (Mexico)
@Column({ name: 'sat_product_code', type: 'varchar', length: 20, nullable: true })
satProductCode?: string;
@Column({ name: 'sat_unit_code', type: 'varchar', length: 10, nullable: true })
satUnitCode?: string;
@Column({ type: 'decimal', precision: 15, scale: 4, default: 1 })
quantity: number;
@Column({ type: 'varchar', length: 20, default: 'PZA' })
uom: string;
@Column({ name: 'unit_price', type: 'decimal', precision: 15, scale: 4, default: 0 })
unitPrice: number;
@Column({ name: 'discount_percent', type: 'decimal', precision: 5, scale: 2, default: 0 })
discountPercent: number;
@Column({ name: 'discount_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
discountAmount: number;
@Column({ name: 'tax_rate', type: 'decimal', precision: 5, scale: 2, default: 16.00 })
taxRate: number;
@Column({ name: 'tax_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
taxAmount: number;
@Column({ name: 'withholding_rate', type: 'decimal', precision: 5, scale: 2, default: 0 })
withholdingRate: number;
@Column({ name: 'withholding_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
withholdingAmount: number;
@Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
subtotal: number;
@Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
total: number;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,187 @@
/**
* Unified Invoice Entity
* Combines commercial and SaaS billing invoices
* Compatible with erp-core invoice.entity
*
* @module Invoices
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
OneToMany,
} from 'typeorm';
import { InvoiceItem } from './invoice-item.entity';
export type InvoiceType = 'sale' | 'purchase' | 'credit_note' | 'debit_note';
export type InvoiceStatus = 'draft' | 'validated' | 'sent' | 'partial' | 'paid' | 'overdue' | 'void' | 'refunded' | 'cancelled' | 'voided';
export type InvoiceContext = 'commercial' | 'saas';
@Entity({ name: 'invoices', schema: 'billing' })
export class Invoice {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index({ unique: true })
@Column({ name: 'invoice_number', type: 'varchar', length: 30 })
invoiceNumber: string;
@Index()
@Column({ name: 'invoice_type', type: 'varchar', length: 20, default: 'sale' })
invoiceType: InvoiceType;
@Index()
@Column({ name: 'invoice_context', type: 'varchar', length: 20, default: 'commercial' })
invoiceContext: InvoiceContext;
// Commercial fields
@Column({ name: 'sales_order_id', type: 'uuid', nullable: true })
salesOrderId: string | null;
@Column({ name: 'purchase_order_id', type: 'uuid', nullable: true })
purchaseOrderId: string | null;
@Index()
@Column({ name: 'partner_id', type: 'uuid', nullable: true })
partnerId: string | null;
@Column({ name: 'partner_name', type: 'varchar', length: 200, nullable: true })
partnerName: string | null;
@Column({ name: 'partner_tax_id', type: 'varchar', length: 50, nullable: true })
partnerTaxId: string | null;
// SaaS billing fields
@Index()
@Column({ name: 'subscription_id', type: 'uuid', nullable: true })
subscriptionId: string | null;
@Column({ name: 'period_start', type: 'date', nullable: true })
periodStart: Date | null;
@Column({ name: 'period_end', type: 'date', nullable: true })
periodEnd: Date | null;
// Billing information
@Column({ name: 'billing_name', type: 'varchar', length: 200, nullable: true })
billingName: string | null;
@Column({ name: 'billing_email', type: 'varchar', length: 255, nullable: true })
billingEmail: string | null;
@Column({ name: 'billing_address', type: 'jsonb', nullable: true })
billingAddress: Record<string, any> | null;
@Column({ name: 'tax_id', type: 'varchar', length: 50, nullable: true })
taxId: string | null;
// Dates
@Index()
@Column({ name: 'invoice_date', type: 'date', default: () => 'CURRENT_DATE' })
invoiceDate: Date;
@Column({ name: 'due_date', type: 'date', nullable: true })
dueDate: Date | null;
@Column({ name: 'payment_date', type: 'date', nullable: true })
paymentDate: Date | null;
@Column({ name: 'paid_at', type: 'timestamptz', nullable: true })
paidAt: Date | null;
// Amounts
@Column({ type: 'varchar', length: 3, default: 'MXN' })
currency: string;
@Column({ name: 'exchange_rate', type: 'decimal', precision: 10, scale: 6, default: 1 })
exchangeRate: number;
@Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
subtotal: number;
@Column({ name: 'tax_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
taxAmount: number;
@Column({ name: 'withholding_tax', type: 'decimal', precision: 15, scale: 2, default: 0 })
withholdingTax: number;
@Column({ name: 'discount_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
discountAmount: number;
@Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
total: number;
@Column({ name: 'amount_paid', type: 'decimal', precision: 15, scale: 2, default: 0 })
amountPaid: number;
@Column({ name: 'amount_due', type: 'decimal', precision: 15, scale: 2, insert: false, update: false, nullable: true })
amountDue: number | null;
@Column({ name: 'paid_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
paidAmount: number;
// Payment details
@Column({ name: 'payment_term_days', type: 'int', default: 0 })
paymentTermDays: number;
@Column({ name: 'payment_method', type: 'varchar', length: 50, nullable: true })
paymentMethod: string | null;
@Column({ name: 'payment_reference', type: 'varchar', length: 100, nullable: true })
paymentReference: string | null;
// Status
@Index()
@Column({ type: 'varchar', length: 20, default: 'draft' })
status: InvoiceStatus;
// CFDI (Mexico)
@Index()
@Column({ name: 'cfdi_uuid', type: 'varchar', length: 40, nullable: true })
cfdiUuid: string | null;
@Column({ name: 'cfdi_status', type: 'varchar', length: 20, nullable: true })
cfdiStatus: string | null;
@Column({ name: 'cfdi_xml', type: 'text', nullable: true })
cfdiXml: string | null;
@Column({ name: 'cfdi_pdf_url', type: 'varchar', length: 500, nullable: true })
cfdiPdfUrl: string | null;
// Notes
@Column({ type: 'text', nullable: true })
notes: string | null;
@Column({ name: 'internal_notes', type: 'text', nullable: true })
internalNotes: string | null;
// Audit
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdBy: string | null;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedBy: string | null;
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date | null;
// Relations
@OneToMany(() => InvoiceItem, (item) => item.invoice, { cascade: true })
items: InvoiceItem[];
}

View File

@ -0,0 +1,53 @@
/**
* PaymentAllocation Entity
* Links payments to invoices
* Compatible with erp-core payment-allocation.entity
*
* @module Invoices
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Payment } from './payment.entity';
import { Invoice } from './invoice.entity';
@Entity({ name: 'payment_allocations', schema: 'billing' })
export class PaymentAllocation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'payment_id', type: 'uuid' })
paymentId: string;
@ManyToOne(() => Payment, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'payment_id' })
payment: Payment;
@Index()
@Column({ name: 'invoice_id', type: 'uuid' })
invoiceId: string;
@ManyToOne(() => Invoice, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'invoice_id' })
invoice: Invoice;
@Column({ type: 'decimal', precision: 15, scale: 2 })
amount: number;
@Column({ name: 'allocation_date', type: 'date', default: () => 'CURRENT_DATE' })
allocationDate: Date;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdBy?: string;
}

View File

@ -0,0 +1,89 @@
/**
* Payment Entity
* Payment received or made
* Compatible with erp-core payment.entity
*
* @module Invoices
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
} from 'typeorm';
@Entity({ name: 'payments', schema: 'billing' })
export class Payment {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'payment_number', type: 'varchar', length: 30 })
paymentNumber: string;
@Index()
@Column({ name: 'payment_type', type: 'varchar', length: 20, default: 'received' })
paymentType: 'received' | 'made';
@Index()
@Column({ name: 'partner_id', type: 'uuid' })
partnerId: string;
@Column({ name: 'partner_name', type: 'varchar', length: 200, nullable: true })
partnerName: string;
@Column({ type: 'varchar', length: 3, default: 'MXN' })
currency: string;
@Column({ type: 'decimal', precision: 15, scale: 2 })
amount: number;
@Column({ name: 'exchange_rate', type: 'decimal', precision: 10, scale: 6, default: 1 })
exchangeRate: number;
@Column({ name: 'payment_date', type: 'date', default: () => 'CURRENT_DATE' })
paymentDate: Date;
@Index()
@Column({ name: 'payment_method', type: 'varchar', length: 50 })
paymentMethod: string;
@Column({ type: 'varchar', length: 100, nullable: true })
reference: string;
@Column({ name: 'bank_account_id', type: 'uuid', nullable: true })
bankAccountId: string;
@Index()
@Column({ type: 'varchar', length: 20, default: 'draft' })
status: 'draft' | 'confirmed' | 'reconciled' | 'cancelled';
@Column({ type: 'text', nullable: true })
notes: string;
@Column({ name: 'cfdi_uuid', type: 'varchar', length: 40, nullable: true })
cfdiUuid: string;
@Column({ name: 'cfdi_status', type: 'varchar', length: 20, nullable: true })
cfdiStatus: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdBy?: string;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt: Date;
}

View File

@ -0,0 +1,65 @@
/**
* Channel Entity
* Notification delivery channel configuration
* Compatible with erp-core channel.entity
*
* @module Notifications
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
export type ChannelType = 'email' | 'sms' | 'push' | 'whatsapp' | 'in_app' | 'webhook';
@Entity({ name: 'channels', schema: 'notifications' })
export class Channel {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 30, unique: true })
code: string;
@Column({ type: 'varchar', length: 100 })
name: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({ name: 'channel_type', type: 'varchar', length: 30 })
channelType: ChannelType;
@Column({ type: 'varchar', length: 50, nullable: true })
provider: string;
@Column({ name: 'provider_config', type: 'jsonb', default: {} })
providerConfig: Record<string, any>;
@Column({ name: 'rate_limit_per_minute', type: 'int', nullable: true })
rateLimitPerMinute: number;
@Column({ name: 'rate_limit_per_hour', type: 'int', nullable: true })
rateLimitPerHour: number;
@Column({ name: 'rate_limit_per_day', type: 'int', nullable: true })
rateLimitPerDay: number;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'is_default', type: 'boolean', default: false })
isDefault: boolean;
@Column({ type: 'jsonb', default: {} })
metadata: Record<string, any>;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,85 @@
/**
* InAppNotification Entity
* In-app notification with read/archive tracking
* Compatible with erp-core in-app-notification.entity
*
* @module Notifications
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
} from 'typeorm';
export type InAppCategory = 'info' | 'success' | 'warning' | 'error' | 'task';
export type InAppPriority = 'low' | 'normal' | 'high' | 'urgent';
export type InAppActionType = 'link' | 'modal' | 'function';
@Entity({ name: 'in_app_notifications', schema: 'notifications' })
export class InAppNotification {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'user_id', type: 'uuid' })
userId: string;
@Column({ type: 'varchar', length: 200 })
title: string;
@Column({ type: 'text' })
message: string;
@Column({ type: 'varchar', length: 50, nullable: true })
icon: string;
@Column({ type: 'varchar', length: 20, nullable: true })
color: string;
@Column({ name: 'action_type', type: 'varchar', length: 20, nullable: true })
actionType: InAppActionType;
@Column({ name: 'action_url', type: 'text', nullable: true })
actionUrl: string;
@Column({ name: 'action_data', type: 'jsonb', nullable: true })
actionData: Record<string, any>;
@Column({ type: 'varchar', length: 30, default: 'info' })
category: InAppCategory;
@Column({ name: 'context_type', type: 'varchar', length: 100, nullable: true })
contextType: string;
@Column({ name: 'context_id', type: 'uuid', nullable: true })
contextId: string;
@Column({ name: 'is_read', type: 'boolean', default: false })
isRead: boolean;
@Column({ name: 'read_at', type: 'timestamptz', nullable: true })
readAt: Date;
@Column({ name: 'is_archived', type: 'boolean', default: false })
isArchived: boolean;
@Column({ name: 'archived_at', type: 'timestamptz', nullable: true })
archivedAt: Date;
@Column({ type: 'varchar', length: 20, default: 'normal' })
priority: InAppPriority;
@Column({ name: 'expires_at', type: 'timestamptz', nullable: true })
expiresAt: Date;
@Index()
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
}

View File

@ -0,0 +1,10 @@
/**
* Notifications Entities - Export
*/
export { Channel, ChannelType } from './channel.entity';
export { NotificationTemplate, TemplateTranslation, TemplateCategory } from './template.entity';
export { NotificationPreference, DigestFrequency } from './preference.entity';
export { Notification, NotificationStatus, NotificationPriority } from './notification.entity';
export { NotificationBatch, BatchStatus, AudienceType } from './notification-batch.entity';
export { InAppNotification, InAppCategory, InAppPriority, InAppActionType } from './in-app-notification.entity';

View File

@ -0,0 +1,96 @@
/**
* NotificationBatch Entity
* Batch notification campaigns with audience targeting
* Compatible with erp-core notification-batch.entity
*
* @module Notifications
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { NotificationTemplate } from './template.entity';
import { ChannelType } from './channel.entity';
export type BatchStatus = 'draft' | 'scheduled' | 'processing' | 'completed' | 'failed' | 'cancelled';
export type AudienceType = 'all_users' | 'segment' | 'custom';
@Entity({ name: 'notification_batches', schema: 'notifications' })
export class NotificationBatch {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ type: 'varchar', length: 200 })
name: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({ name: 'template_id', type: 'uuid', nullable: true })
templateId: string;
@Column({ name: 'channel_type', type: 'varchar', length: 30 })
channelType: ChannelType;
@Column({ name: 'audience_type', type: 'varchar', length: 30 })
audienceType: AudienceType;
@Column({ name: 'audience_filter', type: 'jsonb', default: {} })
audienceFilter: Record<string, any>;
@Column({ type: 'jsonb', default: {} })
variables: Record<string, any>;
@Index()
@Column({ name: 'scheduled_at', type: 'timestamptz', nullable: true })
scheduledAt: Date;
@Index()
@Column({ type: 'varchar', length: 20, default: 'draft' })
status: BatchStatus;
@Column({ name: 'total_recipients', type: 'int', default: 0 })
totalRecipients: number;
@Column({ name: 'sent_count', type: 'int', default: 0 })
sentCount: number;
@Column({ name: 'delivered_count', type: 'int', default: 0 })
deliveredCount: number;
@Column({ name: 'failed_count', type: 'int', default: 0 })
failedCount: number;
@Column({ name: 'read_count', type: 'int', default: 0 })
readCount: number;
@Column({ name: 'started_at', type: 'timestamptz', nullable: true })
startedAt: Date;
@Column({ name: 'completed_at', type: 'timestamptz', nullable: true })
completedAt: Date;
@ManyToOne(() => NotificationTemplate, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'template_id' })
template: NotificationTemplate | null;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdBy: string | null;
}

View File

@ -0,0 +1,138 @@
/**
* Notification Entity
* Individual notification with delivery tracking
* Compatible with erp-core notification.entity
*
* @module Notifications
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { NotificationTemplate } from './template.entity';
import { Channel, ChannelType } from './channel.entity';
export type NotificationStatus = 'pending' | 'queued' | 'sending' | 'sent' | 'delivered' | 'read' | 'failed' | 'cancelled';
export type NotificationPriority = 'low' | 'normal' | 'high' | 'urgent';
@Entity({ name: 'notifications', schema: 'notifications' })
export class Notification {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string;
@Column({ name: 'recipient_email', type: 'varchar', length: 255, nullable: true })
recipientEmail: string;
@Column({ name: 'recipient_phone', type: 'varchar', length: 20, nullable: true })
recipientPhone: string;
@Column({ name: 'recipient_device_id', type: 'varchar', length: 255, nullable: true })
recipientDeviceId: string;
@Index()
@Column({ name: 'template_id', type: 'uuid', nullable: true })
templateId: string;
@Column({ name: 'template_code', type: 'varchar', length: 100, nullable: true })
templateCode: string;
@Column({ name: 'channel_type', type: 'varchar', length: 30 })
channelType: ChannelType;
@Index()
@Column({ name: 'channel_id', type: 'uuid', nullable: true })
channelId: string;
@Column({ type: 'varchar', length: 500, nullable: true })
subject: string;
@Column({ type: 'text', nullable: true })
body: string;
@Column({ name: 'body_html', type: 'text', nullable: true })
bodyHtml: string;
@Column({ type: 'jsonb', default: {} })
variables: Record<string, any>;
@Column({ name: 'context_type', type: 'varchar', length: 100, nullable: true })
contextType: string;
@Column({ name: 'context_id', type: 'uuid', nullable: true })
contextId: string;
@Column({ type: 'varchar', length: 20, default: 'normal' })
priority: NotificationPriority;
@Index()
@Column({ type: 'varchar', length: 20, default: 'pending' })
status: NotificationStatus;
@Column({ name: 'queued_at', type: 'timestamptz', nullable: true })
queuedAt: Date;
@Column({ name: 'sent_at', type: 'timestamptz', nullable: true })
sentAt: Date;
@Column({ name: 'delivered_at', type: 'timestamptz', nullable: true })
deliveredAt: Date;
@Column({ name: 'read_at', type: 'timestamptz', nullable: true })
readAt: Date;
@Column({ name: 'failed_at', type: 'timestamptz', nullable: true })
failedAt: Date;
@Column({ name: 'error_message', type: 'text', nullable: true })
errorMessage: string;
@Column({ name: 'retry_count', type: 'int', default: 0 })
retryCount: number;
@Column({ name: 'max_retries', type: 'int', default: 3 })
maxRetries: number;
@Column({ name: 'next_retry_at', type: 'timestamptz', nullable: true })
nextRetryAt: Date;
@Column({ name: 'provider_message_id', type: 'varchar', length: 255, nullable: true })
providerMessageId: string;
@Column({ name: 'provider_response', type: 'jsonb', nullable: true })
providerResponse: Record<string, any>;
@Column({ type: 'jsonb', default: {} })
metadata: Record<string, any>;
@Column({ name: 'expires_at', type: 'timestamptz', nullable: true })
expiresAt: Date;
@ManyToOne(() => NotificationTemplate, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'template_id' })
template: NotificationTemplate | null;
@ManyToOne(() => Channel, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'channel_id' })
channel: Channel | null;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,82 @@
/**
* NotificationPreference Entity
* User notification preferences per tenant
* Compatible with erp-core preference.entity
*
* @module Notifications
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
Unique,
} from 'typeorm';
export type DigestFrequency = 'instant' | 'hourly' | 'daily' | 'weekly';
@Entity({ name: 'notification_preferences', schema: 'notifications' })
@Unique(['userId', 'tenantId'])
export class NotificationPreference {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'user_id', type: 'uuid' })
userId: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'global_enabled', type: 'boolean', default: true })
globalEnabled: boolean;
@Column({ name: 'quiet_hours_start', type: 'time', nullable: true })
quietHoursStart: string;
@Column({ name: 'quiet_hours_end', type: 'time', nullable: true })
quietHoursEnd: string;
@Column({ type: 'varchar', length: 50, default: 'America/Mexico_City' })
timezone: string;
@Column({ name: 'email_enabled', type: 'boolean', default: true })
emailEnabled: boolean;
@Column({ name: 'sms_enabled', type: 'boolean', default: false })
smsEnabled: boolean;
@Column({ name: 'push_enabled', type: 'boolean', default: true })
pushEnabled: boolean;
@Column({ name: 'whatsapp_enabled', type: 'boolean', default: false })
whatsappEnabled: boolean;
@Column({ name: 'in_app_enabled', type: 'boolean', default: true })
inAppEnabled: boolean;
@Column({ name: 'category_preferences', type: 'jsonb', default: {} })
categoryPreferences: Record<string, any>;
@Column({ name: 'digest_frequency', type: 'varchar', length: 20, default: 'instant' })
digestFrequency: DigestFrequency;
@Column({ name: 'digest_day', type: 'int', nullable: true })
digestDay: number;
@Column({ name: 'digest_hour', type: 'int', nullable: true })
digestHour: number;
@Column({ type: 'jsonb', default: {} })
metadata: Record<string, any>;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,126 @@
/**
* NotificationTemplate + TemplateTranslation Entities
* Template system with i18n support
* Compatible with erp-core template.entity
*
* @module Notifications
*/
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
Unique,
OneToMany,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { ChannelType } from './channel.entity';
export type TemplateCategory = 'system' | 'marketing' | 'transactional' | 'alert';
@Entity({ name: 'notification_templates', schema: 'notifications' })
@Unique(['tenantId', 'code', 'channelType'])
export class NotificationTemplate {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid', nullable: true })
tenantId: string | null;
@Index()
@Column({ type: 'varchar', length: 100 })
code: string;
@Column({ type: 'varchar', length: 200 })
name: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({ type: 'varchar', length: 30, nullable: true })
category: TemplateCategory;
@Index()
@Column({ name: 'channel_type', type: 'varchar', length: 30 })
channelType: ChannelType;
@Column({ type: 'varchar', length: 500, nullable: true })
subject: string;
@Column({ name: 'body_template', type: 'text', nullable: true })
bodyTemplate: string;
@Column({ name: 'body_html', type: 'text', nullable: true })
bodyHtml: string;
@Column({ name: 'available_variables', type: 'jsonb', default: [] })
availableVariables: string[];
@Column({ name: 'default_locale', type: 'varchar', length: 10, default: 'es-MX' })
defaultLocale: string;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'is_system', type: 'boolean', default: false })
isSystem: boolean;
@Column({ type: 'int', default: 1 })
version: number;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdBy: string | null;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedBy: string | null;
@OneToMany(() => TemplateTranslation, (t) => t.template)
translations: TemplateTranslation[];
}
@Entity({ name: 'template_translations', schema: 'notifications' })
@Unique(['templateId', 'locale'])
export class TemplateTranslation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'template_id', type: 'uuid' })
templateId: string;
@Column({ type: 'varchar', length: 10 })
locale: string;
@Column({ type: 'varchar', length: 500, nullable: true })
subject: string;
@Column({ name: 'body_template', type: 'text', nullable: true })
bodyTemplate: string;
@Column({ name: 'body_html', type: 'text', nullable: true })
bodyHtml: string;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@ManyToOne(() => NotificationTemplate, (t) => t.translations, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'template_id' })
template: NotificationTemplate;
}