Files
edr-platform/apps/edr-freight-api/src/modules/shipping-lines/entities/shipping-line-credit.entity.ts
marshalyordanos 9fff469ffa feat: implement shipping line bookings management
- Add ShippingLineBookingsPage for listing and managing shipping line bookings.
- Create ShippingLineDocumentsModal for document uploads related to bookings.
- Introduce ShippingLineInitiateModal for initiating new shipping line bookings.
- Implement booking document state management with booking-doc-state utility.
- Add shipping line bookings service for API interactions.
- Update index to export new components and services.
- Enhance types for freight to include shipping line credits.
2026-08-13 15:54:40 +03:00

111 lines
3.9 KiB
TypeScript

import { BaseEntity } from "@edr/api-common";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import { Booking } from "../../bookings/entities/booking.entity";
import { Invoice } from "../../billing/entities/invoice.entity";
import { ShippingLineCompany } from "./shipping-line-company.entity";
/** Where a credit sits between "service used" and "money received". */
export enum ShippingLineCreditStatus {
/** Service used, priced, not yet on any invoice. Counts as debt. */
Unbilled = "UNBILLED",
/** Finance put it on an invoice; awaiting payment. Still counts as debt. */
Billed = "BILLED",
/** The invoice settled. Terminal — no longer debt, and never re-billed. */
Paid = "PAID",
/** Written off / booking voided. Terminal, excluded from every total. */
Cancelled = "CANCELLED",
}
/** Statuses a shipping line still owes money for. */
export const OUTSTANDING_CREDIT_STATUSES = [
ShippingLineCreditStatus.Unbilled,
ShippingLineCreditStatus.Billed,
] as const;
/**
* What a shipping line owes for one booking.
*
* Shipping lines get the service first and pay later, so a booking of theirs
* raises no invoice and passes no payment gate — it raises one of these. The
* amount is frozen when the booking is priced and is never recalculated, so a
* later rate change cannot silently alter a debt already incurred.
*
* Finance batches unbilled credits into one invoice (see
* `ShippingLineCreditsService.generateInvoice`); the line pays that invoice
* through the ordinary CBE flow; settlement flips the batch to PAID and the
* debt disappears. The outstanding figure is always derived by summing
* {@link OUTSTANDING_CREDIT_STATUSES} rows — there is no balance column,
* because a stored balance is one missed UPDATE away from being a lie.
*/
@Entity({ schema: "freight", name: "shipping_line_credits" })
@Index(["shippingLineCompanyId", "status"])
@Index(["invoiceId"])
export class ShippingLineCredit extends BaseEntity {
/** The line that owes this. */
@Column({ name: "shipping_line_company_id", type: "uuid" })
shippingLineCompanyId!: string;
@ManyToOne(() => ShippingLineCompany)
@JoinColumn({ name: "shipping_line_company_id" })
shippingLineCompany?: ShippingLineCompany;
/**
* The booking that incurred the charge. Unique among live rows (partial
* index excludes soft-deleted and CANCELLED), so one booking can never be
* billed twice.
*/
@Column({ name: "booking_id", type: "uuid" })
bookingId!: string;
@ManyToOne(() => Booking)
@JoinColumn({ name: "booking_id" })
booking?: Booking;
/** Frozen at pricing time. Never recalculated. */
@Column({ name: "amount", type: "numeric", precision: 14, scale: 2 })
amount!: number;
@Column({ name: "currency", type: "varchar", length: 8, default: "ETB" })
currency!: string;
@Column({
name: "status",
type: "enum",
enum: ShippingLineCreditStatus,
default: ShippingLineCreditStatus.Unbilled,
})
status!: ShippingLineCreditStatus;
/** What the charge is for; becomes the invoice line description. */
@Column({ name: "description", type: "varchar", length: 255, nullable: true })
description?: string | null;
/** The invoice this credit was billed on; null while UNBILLED. */
@Column({ name: "invoice_id", type: "uuid", nullable: true })
invoiceId?: string | null;
@ManyToOne(() => Invoice)
@JoinColumn({ name: "invoice_id" })
invoice?: Invoice;
/** When finance put it on an invoice. */
@Column({ name: "billed_at", type: "timestamptz", nullable: true })
billedAt?: Date | null;
/** When that invoice settled. */
@Column({ name: "paid_at", type: "timestamptz", nullable: true })
paidAt?: Date | null;
@Column({ name: "cancelled_at", type: "timestamptz", nullable: true })
cancelledAt?: Date | null;
@Column({
name: "cancellation_reason",
type: "varchar",
length: 255,
nullable: true,
})
cancellationReason?: string | null;
}