feat: Implement Phase 1 & 2 - Full monorepo architecture
## Backend API (apps/api) - Express.js server with TypeScript - JWT authentication with access/refresh tokens - Multi-tenant middleware (schema per tenant) - Complete CRUD routes: auth, cfdis, transactions, contacts, categories, metrics, alerts - SAT integration: CFDI 4.0 XML parser, FIEL authentication - Metrics engine: 50+ financial metrics (Core, Startup, Enterprise) - Rate limiting, CORS, Helmet security ## Frontend Web (apps/web) - Next.js 14 with App Router - Authentication pages: login, register, forgot-password - Dashboard layout with Sidebar and Header - Dashboard pages: overview, cash-flow, revenue, expenses, metrics - Zustand stores for auth and UI state - Theme support with flash prevention ## Database Package (packages/database) - PostgreSQL migrations with multi-tenant architecture - Public schema: plans, tenants, users, sessions, subscriptions - Tenant schema: sat_credentials, cfdis, transactions, contacts, accounts, alerts - Tenant management functions - Seed data for plans and super admin ## Shared Package (packages/shared) - TypeScript types: auth, tenant, financial, metrics, reports - Zod validation schemas for all entities - Utility functions for formatting ## UI Package (packages/ui) - Chart components: LineChart, BarChart, AreaChart, PieChart - Data components: DataTable, MetricCard, KPICard, AlertBadge - PeriodSelector and Skeleton components ## Infrastructure - Docker Compose: PostgreSQL 15, Redis 7, MinIO, Mailhog - Makefile with 25+ development commands - Development scripts: dev-setup.sh, dev-down.sh - Complete .env.example template Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
676
packages/database/src/index.ts
Normal file
676
packages/database/src/index.ts
Normal file
@@ -0,0 +1,676 @@
|
||||
/**
|
||||
* @horux/database
|
||||
*
|
||||
* Database package for Horux Strategy - CFO Digital para Empresas Mexicanas
|
||||
*
|
||||
* Provides:
|
||||
* - PostgreSQL connection pool with multi-tenant support
|
||||
* - Tenant schema management (create, delete, suspend)
|
||||
* - Migration utilities
|
||||
* - Type definitions for database entities
|
||||
*/
|
||||
|
||||
// Connection management
|
||||
export {
|
||||
DatabaseConnection,
|
||||
TenantDatabase,
|
||||
getDatabase,
|
||||
createDatabase,
|
||||
createTenantDatabase,
|
||||
type DatabaseConfig,
|
||||
type TenantContext,
|
||||
type QueryOptions,
|
||||
type Pool,
|
||||
type PoolClient,
|
||||
type QueryResult,
|
||||
type QueryResultRow,
|
||||
} from './connection.js';
|
||||
|
||||
// Tenant management
|
||||
export {
|
||||
createTenantSchema,
|
||||
deleteTenantSchema,
|
||||
suspendTenant,
|
||||
reactivateTenant,
|
||||
getTenant,
|
||||
getTenantBySlug,
|
||||
listTenants,
|
||||
updateTenantSettings,
|
||||
validateTenantAccess,
|
||||
getSchemaName,
|
||||
createTenantContext,
|
||||
type CreateTenantOptions,
|
||||
type TenantSettings,
|
||||
type TenantInfo,
|
||||
type TenantStatus,
|
||||
} from './tenant.js';
|
||||
|
||||
// Migration utilities (for programmatic use)
|
||||
export {
|
||||
runMigrations,
|
||||
printStatus as getMigrationStatus,
|
||||
rollbackLast as rollbackMigration,
|
||||
ensureDatabase,
|
||||
loadMigrationFiles,
|
||||
getExecutedMigrations,
|
||||
ensureMigrationsTable,
|
||||
type MigrationFile,
|
||||
type MigrationRecord,
|
||||
} from './migrate.js';
|
||||
|
||||
// Seed data exports
|
||||
export {
|
||||
PLANS,
|
||||
SYSTEM_SETTINGS,
|
||||
} from './seed.js';
|
||||
|
||||
// ============================================================================
|
||||
// Type Definitions for Database Entities
|
||||
// ============================================================================
|
||||
|
||||
// User roles
|
||||
export type UserRole = 'super_admin' | 'owner' | 'admin' | 'manager' | 'analyst' | 'viewer';
|
||||
|
||||
// Subscription status
|
||||
export type SubscriptionStatus = 'active' | 'trial' | 'past_due' | 'cancelled' | 'suspended' | 'expired';
|
||||
|
||||
// Job status
|
||||
export type JobStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
|
||||
// Transaction types
|
||||
export type TransactionType = 'income' | 'expense' | 'transfer' | 'adjustment';
|
||||
export type TransactionStatus = 'pending' | 'confirmed' | 'reconciled' | 'voided';
|
||||
|
||||
// CFDI types
|
||||
export type CfdiStatus = 'active' | 'cancelled' | 'pending_cancellation';
|
||||
export type CfdiType = 'I' | 'E' | 'T' | 'N' | 'P'; // Ingreso, Egreso, Traslado, Nomina, Pago
|
||||
|
||||
// Contact types
|
||||
export type ContactType = 'customer' | 'supplier' | 'both' | 'employee';
|
||||
|
||||
// Category types
|
||||
export type CategoryType = 'income' | 'expense' | 'cost' | 'other';
|
||||
|
||||
// Account types
|
||||
export type AccountType = 'asset' | 'liability' | 'equity' | 'revenue' | 'expense';
|
||||
|
||||
// Alert severity
|
||||
export type AlertSeverity = 'info' | 'warning' | 'critical';
|
||||
|
||||
// Report status
|
||||
export type ReportStatus = 'draft' | 'generating' | 'completed' | 'failed' | 'archived';
|
||||
|
||||
// ============================================================================
|
||||
// Entity Interfaces
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Plan entity
|
||||
*/
|
||||
export interface Plan {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
priceMonthly: number;
|
||||
priceYearly: number;
|
||||
maxUsers: number;
|
||||
maxCfdisMonthly: number;
|
||||
maxStorageMb: number;
|
||||
maxApiCallsDaily: number;
|
||||
maxReportsMonthly: number;
|
||||
features: Record<string, boolean>;
|
||||
hasSatSync: boolean;
|
||||
hasBankSync: boolean;
|
||||
hasAiInsights: boolean;
|
||||
hasCustomReports: boolean;
|
||||
hasApiAccess: boolean;
|
||||
hasWhiteLabel: boolean;
|
||||
hasPrioritySupport: boolean;
|
||||
hasDedicatedAccountManager: boolean;
|
||||
dataRetentionMonths: number;
|
||||
displayOrder: number;
|
||||
isActive: boolean;
|
||||
isPopular: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* User entity
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
phone: string | null;
|
||||
avatarUrl: string | null;
|
||||
defaultRole: UserRole;
|
||||
isActive: boolean;
|
||||
isEmailVerified: boolean;
|
||||
emailVerifiedAt: Date | null;
|
||||
twoFactorEnabled: boolean;
|
||||
preferences: Record<string, unknown>;
|
||||
timezone: string;
|
||||
locale: string;
|
||||
lastLoginAt: Date | null;
|
||||
lastLoginIp: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tenant entity
|
||||
*/
|
||||
export interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
schemaName: string;
|
||||
rfc: string | null;
|
||||
razonSocial: string | null;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
ownerId: string;
|
||||
planId: string;
|
||||
status: TenantStatus;
|
||||
settings: TenantSettings;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription entity
|
||||
*/
|
||||
export interface Subscription {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
planId: string;
|
||||
status: SubscriptionStatus;
|
||||
billingCycle: 'monthly' | 'yearly';
|
||||
trialEndsAt: Date | null;
|
||||
currentPeriodStart: Date;
|
||||
currentPeriodEnd: Date;
|
||||
cancelledAt: Date | null;
|
||||
cancelAtPeriodEnd: boolean;
|
||||
paymentProcessor: string | null;
|
||||
externalSubscriptionId: string | null;
|
||||
externalCustomerId: string | null;
|
||||
priceCents: number;
|
||||
currency: string;
|
||||
usageCfdisCurrent: number;
|
||||
usageStorageMbCurrent: number;
|
||||
usageApiCallsCurrent: number;
|
||||
usageResetAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* User session entity
|
||||
*/
|
||||
export interface UserSession {
|
||||
id: string;
|
||||
userId: string;
|
||||
tenantId: string | null;
|
||||
userAgent: string | null;
|
||||
ipAddress: string | null;
|
||||
deviceType: string | null;
|
||||
deviceName: string | null;
|
||||
locationCity: string | null;
|
||||
locationCountry: string | null;
|
||||
isActive: boolean;
|
||||
expiresAt: Date;
|
||||
refreshExpiresAt: Date | null;
|
||||
lastActivityAt: Date;
|
||||
createdAt: Date;
|
||||
revokedAt: Date | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Background job entity
|
||||
*/
|
||||
export interface BackgroundJob {
|
||||
id: string;
|
||||
tenantId: string | null;
|
||||
userId: string | null;
|
||||
jobType: string;
|
||||
jobName: string | null;
|
||||
queue: string;
|
||||
priority: number;
|
||||
payload: Record<string, unknown>;
|
||||
status: JobStatus;
|
||||
progress: number;
|
||||
result: Record<string, unknown> | null;
|
||||
errorMessage: string | null;
|
||||
errorStack: string | null;
|
||||
attempts: number;
|
||||
maxAttempts: number;
|
||||
scheduledAt: Date;
|
||||
startedAt: Date | null;
|
||||
completedAt: Date | null;
|
||||
timeoutSeconds: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* API key entity
|
||||
*/
|
||||
export interface ApiKey {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
keyPrefix: string;
|
||||
scopes: string[];
|
||||
allowedIps: string[] | null;
|
||||
allowedOrigins: string[] | null;
|
||||
rateLimitPerMinute: number;
|
||||
rateLimitPerDay: number;
|
||||
lastUsedAt: Date | null;
|
||||
usageCount: number;
|
||||
isActive: boolean;
|
||||
expiresAt: Date | null;
|
||||
createdBy: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
revokedAt: Date | null;
|
||||
revokedBy: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit log entry entity
|
||||
*/
|
||||
export interface AuditLogEntry {
|
||||
id: string;
|
||||
tenantId: string | null;
|
||||
userId: string | null;
|
||||
action: string;
|
||||
entityType: string;
|
||||
entityId: string | null;
|
||||
oldValues: Record<string, unknown> | null;
|
||||
newValues: Record<string, unknown> | null;
|
||||
details: Record<string, unknown> | null;
|
||||
ipAddress: string | null;
|
||||
userAgent: string | null;
|
||||
requestId: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tenant Schema Entity Interfaces
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* SAT credentials entity (tenant schema)
|
||||
*/
|
||||
export interface SatCredentials {
|
||||
id: string;
|
||||
rfc: string;
|
||||
cerSerialNumber: string | null;
|
||||
cerIssuedAt: Date | null;
|
||||
cerExpiresAt: Date | null;
|
||||
cerIssuer: string | null;
|
||||
isActive: boolean;
|
||||
isValid: boolean;
|
||||
lastValidatedAt: Date | null;
|
||||
validationError: string | null;
|
||||
syncEnabled: boolean;
|
||||
syncFrequencyHours: number;
|
||||
lastSyncAt: Date | null;
|
||||
lastSyncStatus: string | null;
|
||||
lastSyncError: string | null;
|
||||
createdBy: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* CFDI entity (tenant schema)
|
||||
*/
|
||||
export interface Cfdi {
|
||||
id: string;
|
||||
uuidFiscal: string;
|
||||
serie: string | null;
|
||||
folio: string | null;
|
||||
tipoComprobante: CfdiType;
|
||||
status: CfdiStatus;
|
||||
fechaEmision: Date;
|
||||
fechaTimbrado: Date | null;
|
||||
fechaCancelacion: Date | null;
|
||||
emisorRfc: string;
|
||||
emisorNombre: string;
|
||||
emisorRegimenFiscal: string;
|
||||
receptorRfc: string;
|
||||
receptorNombre: string;
|
||||
receptorRegimenFiscal: string | null;
|
||||
receptorDomicilioFiscal: string | null;
|
||||
receptorUsoCfdi: string;
|
||||
subtotal: number;
|
||||
descuento: number;
|
||||
total: number;
|
||||
totalImpuestosTrasladados: number;
|
||||
totalImpuestosRetenidos: number;
|
||||
iva16: number;
|
||||
iva8: number;
|
||||
iva0: number;
|
||||
ivaExento: number;
|
||||
isrRetenido: number;
|
||||
ivaRetenido: number;
|
||||
moneda: string;
|
||||
tipoCambio: number;
|
||||
formaPago: string | null;
|
||||
metodoPago: string | null;
|
||||
condicionesPago: string | null;
|
||||
cfdiRelacionados: Record<string, unknown> | null;
|
||||
tipoRelacion: string | null;
|
||||
conceptos: Record<string, unknown>[];
|
||||
isEmitted: boolean;
|
||||
categoryId: string | null;
|
||||
contactId: string | null;
|
||||
isReconciled: boolean;
|
||||
reconciledAt: Date | null;
|
||||
reconciledBy: string | null;
|
||||
aiCategorySuggestion: string | null;
|
||||
aiConfidenceScore: number | null;
|
||||
source: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction entity (tenant schema)
|
||||
*/
|
||||
export interface Transaction {
|
||||
id: string;
|
||||
type: TransactionType;
|
||||
status: TransactionStatus;
|
||||
amount: number;
|
||||
currency: string;
|
||||
exchangeRate: number;
|
||||
amountMxn: number;
|
||||
transactionDate: Date;
|
||||
valueDate: Date | null;
|
||||
recordedAt: Date;
|
||||
description: string | null;
|
||||
reference: string | null;
|
||||
notes: string | null;
|
||||
categoryId: string | null;
|
||||
accountId: string | null;
|
||||
contactId: string | null;
|
||||
cfdiId: string | null;
|
||||
bankTransactionId: string | null;
|
||||
bankAccountId: string | null;
|
||||
bankDescription: string | null;
|
||||
isRecurring: boolean;
|
||||
recurringPattern: Record<string, unknown> | null;
|
||||
parentTransactionId: string | null;
|
||||
attachments: Record<string, unknown>[] | null;
|
||||
tags: string[] | null;
|
||||
isReconciled: boolean;
|
||||
reconciledAt: Date | null;
|
||||
reconciledBy: string | null;
|
||||
requiresApproval: boolean;
|
||||
approvedAt: Date | null;
|
||||
approvedBy: string | null;
|
||||
aiCategoryId: string | null;
|
||||
aiConfidence: number | null;
|
||||
aiNotes: string | null;
|
||||
createdBy: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
voidedAt: Date | null;
|
||||
voidedBy: string | null;
|
||||
voidReason: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact entity (tenant schema)
|
||||
*/
|
||||
export interface Contact {
|
||||
id: string;
|
||||
type: ContactType;
|
||||
name: string;
|
||||
tradeName: string | null;
|
||||
rfc: string | null;
|
||||
regimenFiscal: string | null;
|
||||
usoCfdiDefault: string | null;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
mobile: string | null;
|
||||
website: string | null;
|
||||
addressStreet: string | null;
|
||||
addressInterior: string | null;
|
||||
addressExterior: string | null;
|
||||
addressNeighborhood: string | null;
|
||||
addressCity: string | null;
|
||||
addressMunicipality: string | null;
|
||||
addressState: string | null;
|
||||
addressZip: string | null;
|
||||
addressCountry: string;
|
||||
bankName: string | null;
|
||||
bankAccount: string | null;
|
||||
bankClabe: string | null;
|
||||
creditDays: number;
|
||||
creditLimit: number;
|
||||
balanceReceivable: number;
|
||||
balancePayable: number;
|
||||
category: string | null;
|
||||
tags: string[] | null;
|
||||
isActive: boolean;
|
||||
notes: string | null;
|
||||
createdBy: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Category entity (tenant schema)
|
||||
*/
|
||||
export interface Category {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
type: CategoryType;
|
||||
parentId: string | null;
|
||||
level: number;
|
||||
path: string | null;
|
||||
satKey: string | null;
|
||||
budgetMonthly: number | null;
|
||||
budgetYearly: number | null;
|
||||
color: string | null;
|
||||
icon: string | null;
|
||||
displayOrder: number;
|
||||
isActive: boolean;
|
||||
isSystem: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Account entity (tenant schema)
|
||||
*/
|
||||
export interface Account {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
type: AccountType;
|
||||
parentId: string | null;
|
||||
level: number;
|
||||
path: string | null;
|
||||
satCode: string | null;
|
||||
satNature: 'D' | 'A' | null;
|
||||
balanceDebit: number;
|
||||
balanceCredit: number;
|
||||
balanceCurrent: number;
|
||||
isActive: boolean;
|
||||
isSystem: boolean;
|
||||
allowsMovements: boolean;
|
||||
displayOrder: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alert entity (tenant schema)
|
||||
*/
|
||||
export interface Alert {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
message: string;
|
||||
severity: AlertSeverity;
|
||||
entityType: string | null;
|
||||
entityId: string | null;
|
||||
thresholdType: string | null;
|
||||
thresholdValue: number | null;
|
||||
currentValue: number | null;
|
||||
actionUrl: string | null;
|
||||
actionLabel: string | null;
|
||||
actionData: Record<string, unknown> | null;
|
||||
isRead: boolean;
|
||||
isDismissed: boolean;
|
||||
readAt: Date | null;
|
||||
dismissedAt: Date | null;
|
||||
dismissedBy: string | null;
|
||||
isRecurring: boolean;
|
||||
lastTriggeredAt: Date | null;
|
||||
triggerCount: number;
|
||||
autoResolved: boolean;
|
||||
resolvedAt: Date | null;
|
||||
resolvedBy: string | null;
|
||||
resolutionNotes: string | null;
|
||||
createdAt: Date;
|
||||
expiresAt: Date | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report entity (tenant schema)
|
||||
*/
|
||||
export interface Report {
|
||||
id: string;
|
||||
type: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
periodStart: Date;
|
||||
periodEnd: Date;
|
||||
comparisonPeriodStart: Date | null;
|
||||
comparisonPeriodEnd: Date | null;
|
||||
status: ReportStatus;
|
||||
parameters: Record<string, unknown> | null;
|
||||
data: Record<string, unknown> | null;
|
||||
fileUrl: string | null;
|
||||
fileFormat: string | null;
|
||||
isScheduled: boolean;
|
||||
scheduleCron: string | null;
|
||||
nextScheduledAt: Date | null;
|
||||
lastGeneratedAt: Date | null;
|
||||
isShared: boolean;
|
||||
sharedWith: string[] | null;
|
||||
shareToken: string | null;
|
||||
shareExpiresAt: Date | null;
|
||||
generatedBy: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metric cache entry entity (tenant schema)
|
||||
*/
|
||||
export interface MetricCache {
|
||||
id: string;
|
||||
metricKey: string;
|
||||
periodType: 'daily' | 'weekly' | 'monthly' | 'yearly';
|
||||
periodStart: Date;
|
||||
periodEnd: Date;
|
||||
dimensionType: string | null;
|
||||
dimensionId: string | null;
|
||||
valueNumeric: number | null;
|
||||
valueJson: Record<string, unknown> | null;
|
||||
previousValue: number | null;
|
||||
changePercent: number | null;
|
||||
changeAbsolute: number | null;
|
||||
computedAt: Date;
|
||||
validUntil: Date | null;
|
||||
isStale: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting entity (tenant schema)
|
||||
*/
|
||||
export interface Setting {
|
||||
key: string;
|
||||
value: string;
|
||||
valueType: 'string' | 'integer' | 'boolean' | 'json';
|
||||
category: string;
|
||||
label: string | null;
|
||||
description: string | null;
|
||||
isSensitive: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bank account entity (tenant schema)
|
||||
*/
|
||||
export interface BankAccount {
|
||||
id: string;
|
||||
bankName: string;
|
||||
bankCode: string | null;
|
||||
accountNumber: string | null;
|
||||
clabe: string | null;
|
||||
accountType: string | null;
|
||||
alias: string | null;
|
||||
currency: string;
|
||||
balanceAvailable: number | null;
|
||||
balanceCurrent: number | null;
|
||||
balanceUpdatedAt: Date | null;
|
||||
connectionProvider: string | null;
|
||||
connectionId: string | null;
|
||||
connectionStatus: string | null;
|
||||
lastSyncAt: Date | null;
|
||||
lastSyncError: string | null;
|
||||
accountId: string | null;
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Budget item entity (tenant schema)
|
||||
*/
|
||||
export interface BudgetItem {
|
||||
id: string;
|
||||
year: number;
|
||||
month: number;
|
||||
categoryId: string | null;
|
||||
accountId: string | null;
|
||||
amountBudgeted: number;
|
||||
amountActual: number;
|
||||
amountVariance: number;
|
||||
notes: string | null;
|
||||
isLocked: boolean;
|
||||
createdBy: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attachment entity (tenant schema)
|
||||
*/
|
||||
export interface Attachment {
|
||||
id: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
fileName: string;
|
||||
fileType: string | null;
|
||||
fileSize: number | null;
|
||||
fileUrl: string;
|
||||
storageProvider: string;
|
||||
storagePath: string | null;
|
||||
uploadedBy: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
Reference in New Issue
Block a user