- 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>
230 lines
7.1 KiB
TypeScript
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[];
|
|
}
|