demurrage invoices

This commit is contained in:
Hagernesh
2026-06-18 00:04:07 +00:00
parent 01aec12ee9
commit 33b74a5da9
7 changed files with 486 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
import { BaseEntity } from '@edr/api-common';
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { WarehouseFeeInvoice } from './warehouse-fee-invoice.entity';
export const WAREHOUSE_FEE_TYPES = [
'CONTAINER_DEMURRAGE',
'BULK_DEMURRAGE',
'STORAGE_FEE',
'HANDLING_FEE',
] as const;
export type WarehouseFeeType = (typeof WAREHOUSE_FEE_TYPES)[number];
@Entity({ schema: 'freight', name: 'warehouse_fee_invoice_items' })
@Index(['invoiceId'])
export class WarehouseFeeInvoiceItem extends BaseEntity {
@Column({ name: 'invoice_id', type: 'uuid' })
invoiceId!: string;
@ManyToOne(() => WarehouseFeeInvoice, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'invoice_id' })
invoice?: WarehouseFeeInvoice;
@Column({ name: 'fee_rule_id', type: 'uuid', nullable: true })
feeRuleId?: string | null;
@Column({ name: 'fee_type', type: 'varchar', length: 32 })
feeType!: WarehouseFeeType;
@Column({ name: 'description', type: 'varchar', length: 255 })
description!: string;
@Column({ name: 'quantity', type: 'numeric', precision: 12, scale: 2, default: 1 })
quantity!: number;
@Column({ name: 'unit_rate', type: 'numeric', precision: 14, scale: 2, default: 0 })
unitRate!: number;
@Column({ name: 'amount', type: 'numeric', precision: 14, scale: 2, default: 0 })
amount!: number;
@Column({ name: 'currency', type: 'varchar', length: 8, default: 'USD' })
currency!: string;
@Column({ name: 'chargeable_days', type: 'int', nullable: true })
chargeableDays?: number | null;
@Column({ name: 'free_days', type: 'int', nullable: true })
freeDays?: number | null;
}

View File

@@ -0,0 +1,107 @@
import { BaseEntity } from '@edr/api-common';
import { Column, Entity, Index } from 'typeorm';
export const WAREHOUSE_INVOICE_TYPES = ['DEMURRAGE', 'STORAGE_FEE', 'MIXED_WAREHOUSE_FEES'] as const;
export type WarehouseInvoiceType = (typeof WAREHOUSE_INVOICE_TYPES)[number];
export const WAREHOUSE_INVOICE_STATUSES = [
'DRAFT',
'ISSUED',
'PARTIALLY_PAID',
'PAID',
'CANCELLED',
] as const;
export type WarehouseInvoiceStatus = (typeof WAREHOUSE_INVOICE_STATUSES)[number];
/** A single recorded payment against a warehouse fee invoice (history). */
export interface WarehouseInvoicePayment {
amount: number;
method?: string | null;
reference?: string | null;
paidAt: string;
}
/**
* Batch 6 — invoice generated from Batch 5 demurrage/storage fee calculation.
* Owns warehouse fees; links to booking/customer/inventory/location so it can
* connect to the existing payment module without duplicating it.
*/
@Entity({ schema: 'freight', name: 'warehouse_fee_invoices' })
@Index(['invoiceNumber'], { unique: true })
@Index(['bookingId'])
@Index(['inventoryId'])
@Index(['status'])
export class WarehouseFeeInvoice extends BaseEntity {
@Column({ name: 'invoice_number', type: 'varchar', length: 40, unique: true })
invoiceNumber!: string;
@Column({ name: 'booking_id', type: 'uuid', nullable: true })
bookingId?: string | null;
@Column({ name: 'customer_id', type: 'uuid', nullable: true })
customerId?: string | null;
@Column({ name: 'inventory_id', type: 'uuid' })
inventoryId!: string;
@Column({ name: 'facility_id', type: 'uuid', nullable: true })
facilityId?: string | null;
@Column({ name: 'warehouse_id', type: 'uuid', nullable: true })
warehouseId?: string | null;
@Column({ name: 'yard_id', type: 'uuid', nullable: true })
yardId?: string | null;
@Column({ name: 'zone_id', type: 'uuid', nullable: true })
zoneId?: string | null;
@Column({ name: 'invoice_type', type: 'varchar', length: 32, default: 'MIXED_WAREHOUSE_FEES' })
invoiceType!: WarehouseInvoiceType;
@Column({ name: 'status', type: 'varchar', length: 20, default: 'DRAFT' })
status!: WarehouseInvoiceStatus;
@Column({ name: 'subtotal_amount', type: 'numeric', precision: 14, scale: 2, default: 0 })
subtotalAmount!: number;
@Column({ name: 'tax_amount', type: 'numeric', precision: 14, scale: 2, default: 0 })
taxAmount!: number;
@Column({ name: 'total_amount', type: 'numeric', precision: 14, scale: 2, default: 0 })
totalAmount!: number;
@Column({ name: 'paid_amount', type: 'numeric', precision: 14, scale: 2, default: 0 })
paidAmount!: number;
@Column({ name: 'balance_amount', type: 'numeric', precision: 14, scale: 2, default: 0 })
balanceAmount!: number;
@Column({ name: 'currency', type: 'varchar', length: 8, default: 'USD' })
currency!: string;
/** Charge window covered by this invoice — used to allow a later invoice for a new period. */
@Column({ name: 'period_start', type: 'timestamptz', nullable: true })
periodStart?: Date | null;
@Column({ name: 'period_end', type: 'timestamptz', nullable: true })
periodEnd?: Date | null;
@Column({ name: 'issued_at', type: 'timestamptz', nullable: true })
issuedAt?: Date | null;
@Column({ name: 'due_date', type: 'timestamptz', nullable: true })
dueDate?: Date | null;
@Column({ name: 'paid_at', type: 'timestamptz', nullable: true })
paidAt?: Date | null;
@Column({ name: 'cancelled_at', type: 'timestamptz', nullable: true })
cancelledAt?: Date | null;
@Column({ name: 'payments', type: 'jsonb', default: () => "'[]'" })
payments!: WarehouseInvoicePayment[];
@Column({ name: 'notes', type: 'text', nullable: true })
notes?: string | null;
}