erp-core-backend-v2/src/modules/invoices/entities/invoice.entity.ts
rckrdmrd d9778eb632 feat(invoices): Unify billing and commercial Invoice entities
- Create unified Invoice entity in invoices module with all fields:
  - Commercial fields: salesOrderId, purchaseOrderId, partnerId
  - SaaS fields: subscriptionId, periodStart, periodEnd
  - Context discriminator: invoiceContext ('commercial' | 'saas')
- Mark billing-usage/invoice.entity.ts as deprecated, re-export from invoices
- Update billing-usage/services/invoices.service.ts for unified entity
- Add missing InvoiceStatus values (validated, cancelled, voided)
- Export types from both modules for backward compatibility

Note: financial/invoice.entity.ts remains separate (different schema)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 02:36:06 -06:00

230 lines
7.1 KiB
TypeScript

import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
OneToMany,
} from 'typeorm';
import { InvoiceItem } from './invoice-item.entity';
/**
* Unified Invoice Entity
*
* Combines fields from commercial invoices and SaaS billing invoices.
* Schema: billing
*
* Context discriminator:
* - 'commercial': Sales/purchase invoices (salesOrderId, purchaseOrderId, partnerId)
* - 'saas': SaaS subscription invoices (subscriptionId, periodStart, periodEnd)
*/
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;
// ============================================
// IDENTIFICATION
// ============================================
@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 INVOICE 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;
// Computed or handled by DB trigger
@Column({ name: 'amount_due', type: 'decimal', precision: 15, scale: 2, insert: false, update: false, nullable: true })
amountDue: number | null;
// Alias for SaaS compatibility
@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 Electronic Invoice)
// ============================================
@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[];
}