@invoicing-sdk/domain
Version:
Core domain logic for the invoicing system including entities, value objects, services, and events
1,203 lines (1,184 loc) • 41.8 kB
TypeScript
type Currency = 'EUR' | 'USD';
type MoneyPlainObject = {
amount: number;
currency: string;
};
/**
* Money value object representing monetary amounts with currency
* Stores amounts in cents to avoid floating point precision issues
*/
declare class Money {
readonly amount: number;
readonly currency: Currency;
private static readonly EUR_RATES;
/**
* Constructs a new Money value with 0 amount.
* @param currency The currency to create the menu in.
*/
static zero(currency: Currency): Money;
constructor(amount: number, // in cents
currency: Currency);
/**
* Convert this money amount to EUR
*/
toEUR(): number;
/**
* Create Money from EUR amount
*/
static fromEUR(amount: number, targetCurrency: Currency): Money;
/**
* Add two money amounts (must be same currency)
*/
add(...others: Money[]): Money;
/**
* Subtract two money amounts (must be same currency)
*/
subtract(other: Money): Money;
/**
* Multiply money by a factor
*/
multiply(factor: number): Money;
/**
* Check if two money amounts are equal
*/
equals(other: Money): boolean;
/**
* Get the amount in the major currency unit (e.g., euros instead of cents)
*/
toMajorUnit(): number;
/**
* Create Money from major currency unit
*/
static fromMajorUnit(amount: number, currency: Currency): Money;
/**
* String representation
*/
toString(): string;
/**
* Convert to plain object for serialization
*/
toPlainObject(): {
amount: number;
currency: string;
};
}
/**
* CustomerInfo value object representing customer data snapshot
* Immutable snapshot of customer information at invoice creation time
*/
declare class CustomerInfo {
readonly firstName: string;
readonly lastName: string;
readonly street: string;
readonly zip: string;
readonly city: string;
readonly country: string;
readonly email: string;
readonly phone?: string | undefined;
constructor(firstName: string, lastName: string, street: string, zip: string, city: string, country: string, email: string, phone?: string | undefined);
private validate;
/**
* Get full name
*/
getFullName(): string;
/**
* Get formatted address
*/
getFormattedAddress(): string;
/**
* Check if two customer info objects are equal
*/
equals(other: CustomerInfo): boolean;
/**
* Create a copy with updated fields
*/
update(updates: Partial<Omit<CustomerInfo, 'validate' | 'getFullName' | 'getFormattedAddress' | 'equals' | 'update'>>): CustomerInfo;
/**
* Convert to plain object for serialization
*/
toPlainObject(): {
firstName: string;
lastName: string;
street: string;
zip: string;
city: string;
country: string;
email: string;
phone?: string;
};
/**
* Create from plain object
*/
static fromPlainObject(data: {
firstName: string;
lastName: string;
street: string;
zip: string;
city: string;
country: string;
email: string;
phone?: string;
}): CustomerInfo;
}
/**
* OrderItem value object representing a line item in an order/invoice
* Immutable snapshot of order item data with price calculations
*/
declare class OrderItem {
readonly unitPrice: Money;
readonly quantity: number;
readonly name: string;
readonly variant?: string | undefined;
readonly productNumber?: string | undefined;
readonly linePrice: Money;
constructor(unitPrice: Money, quantity: number, name: string, variant?: string | undefined, productNumber?: string | undefined);
private validate;
private calculateLinePrice;
/**
* Get the total price for this line item
*/
getTotalPrice(): Money;
/**
* Get display name including variant if present
*/
getDisplayName(): string;
/**
* Get full item description including product number
*/
getFullDescription(): string;
/**
* Check if two order items are equal
*/
equals(other: OrderItem): boolean;
/**
* Create a copy with updated quantity
*/
withQuantity(newQuantity: number): OrderItem;
/**
* Create a copy with updated unit price
*/
withUnitPrice(newUnitPrice: Money): OrderItem;
/**
* Convert to plain object for serialization
*/
toPlainObject(): {
unitPrice: {
amount: number;
currency: string;
};
quantity: number;
linePrice: {
amount: number;
currency: string;
};
name: string;
variant?: string;
productNumber?: string;
};
/**
* Create from plain object
*/
static fromPlainObject(data: {
unitPrice: MoneyPlainObject;
quantity: number;
name: string;
variant?: string;
productNumber?: string;
}): OrderItem;
}
interface ShippingRateProvider {
getStandardShippingRate: () => Money;
}
interface ShippingCostPolicy {
isFreeShippingAllowed: (grandTotal: Money) => boolean;
}
/**
* Base class for invoice-related domain events
*/
declare abstract class InvoiceEvent implements DomainEvent {
readonly eventId: UUID;
readonly eventType: string;
readonly aggregateId: UUID;
readonly occurredOn: Date;
readonly version: 1;
readonly invoiceId: UUID;
constructor(eventId: UUID, eventType: string, aggregateId: UUID, occurredOn: Date, version: 1, invoiceId: UUID);
}
/**
* Domain event published when an invoice is issued
* Contains essential information for downstream processing
*/
declare class InvoiceIssuedEvent extends InvoiceEvent {
readonly invoiceId: UUID;
readonly orderId: string;
readonly invoiceNumber: string;
readonly grandTotal: Money;
readonly pdfBytes: Uint8Array;
readonly issuedBy: string;
readonly eventType: "InvoiceIssued";
constructor(eventId: UUID, aggregateId: UUID, occurredOn: Date, version: 1, invoiceId: UUID, orderId: string, invoiceNumber: string, grandTotal: Money, pdfBytes: Uint8Array, issuedBy: string);
/**
* Convert to plain object for serialization
*/
toPlainObject(): {
eventId: string;
eventType: "InvoiceIssued";
aggregateId: string;
occurredOn: string;
version: 1;
invoiceId: string;
orderId: string;
invoiceNumber: string;
grandTotal: {
amount: number;
currency: string;
};
issuedBy: string;
};
}
/**
* Domain event published when an invoice is created
* Contains essential information for downstream processing
*
* @deprecated Use {@link InvoiceIssuedEvent} instead. "Issue Invoice" is the preferred terminology
* as it aligns with real-world accounting language where invoices are issued to customers.
* This event will be removed in the next major version.
*/
declare class InvoiceCreatedEvent extends InvoiceEvent {
readonly invoiceId: UUID;
readonly orderId: string;
readonly invoiceNumber: string;
readonly grandTotal: Money;
readonly pdfBytes: Uint8Array;
readonly createdBy: string;
readonly eventType: "InvoiceCreated";
constructor(eventId: UUID, aggregateId: UUID, occurredOn: Date, version: 1, invoiceId: UUID, orderId: string, invoiceNumber: string, grandTotal: Money, pdfBytes: Uint8Array, createdBy: string);
/**
* Convert to plain object for serialization
*/
toPlainObject(): {
eventId: string;
eventType: "InvoiceCreated";
aggregateId: string;
occurredOn: string;
version: 1;
invoiceId: string;
orderId: string;
invoiceNumber: string;
grandTotal: {
amount: number;
currency: string;
};
createdBy: string;
};
}
/**
* Domain event published when an invoice is requested.
*
* Contains essential information for downstream processing.
*/
declare class InvoiceRequested implements DomainEvent, SerializableEvent<InvoiceRequested> {
eventId: UUID;
aggregateId: UUID;
occurredOn: Date;
version: 1;
readonly orderId: string;
readonly requestedBy: string;
readonly invoiceNumber: string;
readonly eventType: string;
constructor(eventId: UUID, aggregateId: UUID, occurredOn: Date, version: 1, orderId: string, requestedBy: string, invoiceNumber: string);
/**
* Convert to plain object for serialization
*/
toPlainObject(): PlainEvent<InvoiceRequested>;
}
/**
* Domain event published when an invoice PDF is generated
* Separate from creation to handle PDF generation failures
*/
declare class InvoicePdfGeneratedEvent extends InvoiceEvent {
readonly invoiceId: UUID;
readonly invoiceNumber: string;
readonly pdfSize: number;
readonly pdfBytes: Uint8Array;
readonly eventType: "InvoicePdfGenerated";
constructor(eventId: UUID, aggregateId: UUID, occurredOn: Date, version: 1, invoiceId: UUID, invoiceNumber: string, pdfSize: number, pdfBytes: Uint8Array);
/**
* Convert to plain object for serialization
*/
toPlainObject(): {
eventId: string;
eventType: "InvoicePdfGenerated";
aggregateId: string;
occurredOn: string;
version: 1;
invoiceId: string;
invoiceNumber: string;
pdfSize: number;
};
}
/**
* Domain event published when an invoice validation fails
* Used for monitoring and alerting purposes
*/
declare class InvoiceValidationFailedEvent extends InvoiceEvent {
readonly invoiceId: UUID;
readonly orderId: string;
readonly invoiceNumber: string;
readonly validationErrors: string[];
readonly attemptedBy: string;
readonly eventType: "InvoiceValidationFailed";
constructor(eventId: UUID, aggregateId: UUID, occurredOn: Date, version: 1, invoiceId: UUID, orderId: string, invoiceNumber: string, validationErrors: string[], attemptedBy: string);
/**
* Convert to plain object for serialization
*/
toPlainObject(): {
eventId: string;
eventType: "InvoiceValidationFailed";
aggregateId: string;
occurredOn: string;
version: 1;
invoiceId: string;
orderId: string;
invoiceNumber: string;
validationErrors: string[];
attemptedBy: string;
};
}
/**
* Generates a new event ID as a uuidv4 string.
* @returns A UUID string
*/
declare function generateEventId(): string;
/**
* Domain event publisher interface
* Abstracts the infrastructure event bus from domain logic
*/
interface DomainEventPublisher {
/**
* Publish a single domain event
*/
publish<T extends DomainEvent>(event: T): Promise<void>;
/**
* Publish multiple domain events
*/
publishAll(events: DomainEvent[]): Promise<void>;
}
/**
* Domain service for generating invoice ids.
*
* This is _not_ for public-facing invoice numbers, it is a service
* to generate the internal IDs.
*
* By default, Invoicing SDK generates random UUIDv4 IDs.
* You can override this behavior by providing your own implementation.
*
* When you use it, it becomes _your responsibility_ that the generated IDs
* are unique and properly formatted, whatever matches to your underlying infra.
*
* @example
* ```
* class RandomInvoiceIdGenerator implements InvoiceIdGenerator {
* async generate(): Promise<string> {
* return Math.random().toString(36).substring(2, 15);
* }
* }
* ```
*
* @see {@link Invoice.create}
* @since 0.4.0
*/
interface InvoiceIdGenerator {
/**
* Generates a unique identifier for an invoice.
* The Invoice aggregate root doesn't care about the response,
* it will just use the generated ID.
*/
generate(): Promise<string>;
}
/**
* Configuration for invoice number validation
*/
interface InvoiceNumberConfig {
readonly pattern: RegExp;
readonly prefix?: string;
readonly suffix?: string;
readonly minLength: number;
readonly maxLength: number;
readonly requireSequential: boolean;
}
/**
* Default German invoice number configuration
* Format: YYYY-NNNN (e.g., 2024-0001)
*/
declare const DEFAULT_INVOICE_NUMBER_CONFIG: InvoiceNumberConfig;
/**
* Repository interface for checking invoice number uniqueness
*/
interface InvoiceNumberRepository {
exists(invoiceNumber: string): Promise<boolean>;
getLastSequenceNumber(prefix?: string): Promise<number>;
}
/**
* Domain service for validating invoice numbers
* Handles format validation and uniqueness checking
*/
interface InvoiceNumberValidator {
/**
* Validates an invoice number against format and uniqueness rules
* @param invoiceNumber The invoice number to validate
* @throws InvalidInvoiceNumberError if format is invalid
* @throws DuplicateInvoiceNumberError if number already exists
*
* @deprecated Use {@link validateUniqueness} and {@link validateFormat} instead
*/
validate(invoiceNumber: string): Promise<void>;
/**
* Validates just the format without checking uniqueness
* @param invoiceNumber The invoice number to validate
* @throws InvalidInvoiceNumberError if format is invalid
*/
validateFormat(invoiceNumber: string): void;
/**
* Checks if an invoice number is unique
* @param invoiceNumber The invoice number to check
* @throws DuplicateInvoiceNumberError if number already exists
*/
validateUniqueness(invoiceNumber: string): Promise<void>;
}
/**
* Implementation of InvoiceNumberValidator
*/
declare class InvoiceNumberValidatorImpl implements InvoiceNumberValidator {
private readonly repository;
private readonly config;
constructor(repository: InvoiceNumberRepository, config?: InvoiceNumberConfig);
validate(invoiceNumber: string): Promise<void>;
validateFormat(invoiceNumber: string): void;
validateUniqueness(invoiceNumber: string): Promise<void>;
/**
* Validates that the invoice number follows sequential numbering
* For format YYYY-NNNN, ensures NNNN is the next expected sequence number
*/
private validateSequential;
}
/**
* Configuration for invoice number generation
*/
interface InvoiceNumberGeneratorConfig {
readonly format: "YYYY-NNNN" | "YYYYNNNN" | "PREFIX-YYYY-NNNN" | "custom";
readonly prefix?: string;
readonly suffix?: string;
readonly sequenceLength: number;
readonly startSequence: number;
readonly customFormatter?: (year: number, sequence: number, prefix?: string, suffix?: string) => string;
}
/**
* Default German invoice number generator configuration
*/
declare const DEFAULT_GENERATOR_CONFIG: InvoiceNumberGeneratorConfig;
/**
* Domain service for generating invoice numbers
* Handles automatic generation with configurable formats and sequences
*/
interface InvoiceNumberGenerator {
/**
* Generates the next invoice number in sequence (preview only - doesn't reserve)
* @param year Optional year (defaults to current year)
* @returns Promise resolving to the generated invoice number
*/
generate(year?: number): Promise<string>;
/**
* Generates a preview of what the next invoice number would be
* without actually reserving it
* @param year Optional year (defaults to current year)
* @returns Promise resolving to the preview invoice number
*/
preview(year?: number): Promise<string>;
/**
* Generates and reserves the next invoice number in sequence
* This method should be used when actually creating an invoice
* @param year Optional year (defaults to current year)
* @returns Promise resolving to the generated and reserved invoice number
*/
generateAndReserve(year?: number): Promise<string>;
/**
* Resets the sequence for a given year (admin function)
* @param year The year to reset
* @param startFrom The sequence number to start from
*/
resetSequence(year: number, startFrom: number): Promise<void>;
}
/**
* Implementation of InvoiceNumberGenerator
*/
declare class InvoiceNumberGeneratorImpl implements InvoiceNumberGenerator {
private readonly repository;
private readonly config;
constructor(repository: InvoiceNumberRepository, config?: InvoiceNumberGeneratorConfig);
generate(year?: number): Promise<string>;
preview(year?: number): Promise<string>;
resetSequence(year: number, startFrom: number): Promise<void>;
/**
* Generates and reserves the next invoice number in sequence
* This method should be used when actually creating an invoice as it increments the sequence
* @param year Optional year (defaults to current year)
* @returns Promise resolving to the generated and reserved invoice number
*/
generateAndReserve(year?: number): Promise<string>;
/**
* Gets the next sequence number for the given year (for preview only)
*/
private getNextSequence;
/**
* Formats the invoice number according to the configured format
*/
private formatInvoiceNumber;
}
/**
* Predefined generator configurations for common formats
*/
declare const GENERATOR_PRESETS: {
/**
* German standard: 2024-0001
*/
readonly GERMAN_STANDARD: {
readonly format: "YYYY-NNNN";
readonly sequenceLength: 4;
readonly startSequence: 1;
};
/**
* Compact format: 20240001
*/
readonly COMPACT: {
readonly format: "YYYYNNNN";
readonly sequenceLength: 4;
readonly startSequence: 1;
};
/**
* Prefixed format: INV-2024-0001
*/
readonly PREFIXED: {
readonly format: "PREFIX-YYYY-NNNN";
readonly prefix: "INV";
readonly sequenceLength: 4;
readonly startSequence: 1;
};
/**
* Monthly format: 2024-01-001 (custom)
*/
readonly MONTHLY: {
readonly format: "custom";
readonly sequenceLength: 3;
readonly startSequence: 1;
readonly customFormatter: (year: number, sequence: number) => string;
};
};
/**
* Factory function to create generator with preset configuration
*/
declare function createInvoiceNumberGenerator(repository: InvoiceNumberRepository, preset?: keyof typeof GENERATOR_PRESETS): InvoiceNumberGenerator;
/**
* Utility function to validate generated invoice numbers
*/
declare function validateGeneratedNumber(invoiceNumber: string, config: InvoiceNumberGeneratorConfig): boolean;
/**
* Aggregate root base class that supports domain event collection
*/
declare abstract class AggregateRoot {
private _domainEvents;
/**
* Adds a domain event to the aggregate's list of domain events.
*
* @typeParam T - The type of the domain event, extending `DomainEvent`.
* @param event - The domain event to add. Must include an `eventType` property.
*/
protected addDomainEvent<T extends DomainEvent>(event: T & {
eventType: T["eventType"];
}): void;
/**
* Get all unpublished domain events
*/
getUncommittedEvents(): DomainEvent[];
/**
* Clear all domain events (called after publishing)
*/
markEventsAsCommitted(): void;
/**
* Check if there are unpublished events
*/
hasUncommittedEvents(): boolean;
/**
* Publishes all uncommitted events using the provided publisher,
* and marks all domains as commited.
*
* It is a convenience wrapper around:
*
* ```typescript
* const publisher = //...;
* const uncommitted = this.getUncommittedEvents();
* publisher.publishAll(uncommitted);
* this.markEventsAsCommitted();
* ```
*/
publishAllUncommitted(publisher: DomainEventPublisher): void;
}
type InvoiceAggregateDependencies = {
invoiceNumberValidator: InvoiceNumberValidator;
/**
* The invoice ID generator.
* If not provided, it will use {@link InvoiceIdGeneratorDefault}.
*/
invoiceIdGenerator?: InvoiceIdGenerator;
};
/**
* Invoice aggregate root
* Manages invoice lifecycle, validation, and business rules
* Immutable once created to ensure data integrity
*/
declare class Invoice extends AggregateRoot {
#private;
readonly invoiceId: UUID;
readonly orderId: string;
readonly invoiceNumber: string;
readonly customerSnapshot: CustomerInfo;
readonly itemsSnapshot: readonly OrderItem[];
readonly financials: InvoiceFinancials;
readonly createdOn: Date;
readonly createdBy: string;
readonly pdfBytes: Uint8Array;
private readonly status;
constructor(invoiceId: UUID, orderId: string, invoiceNumber: string, customerSnapshot: CustomerInfo, itemsSnapshot: readonly OrderItem[], financials: InvoiceFinancials, createdOn: Date, createdBy: string, pdfBytes: Uint8Array, status: InvoiceStatus);
/**
* Factory method to create a new invoice.
*
* Validates business rules and calculates totals.
*
* **NOTE**: The invoice information you pass must be in `DRAFT` status.
* If that's not the case, a `InvoiceTransitionViolationError` will be thrown.
*
* Requires dependencies, such as the invoice number validator,
* because it's a core business rule that they are unique.
*
* @deprecated Use {@link issue} instead.
* It provides a more aligned naming convention and,
* under the hood, this method work the same way.
* Will be removed in the next major version.
*
* @see https://github.com/benjamin-kraatz/invoicing-sdk/issues/14
*/
static create(data: CreateInvoiceData, pdfBytes: Uint8Array, deps: InvoiceAggregateDependencies): Promise<Invoice>;
/**
* Factory method to issue a new invoice.
*
* Validates business rules and calculates totals.
*
* **NOTE**: The invoice information you pass must be in `DRAFT` status.
* If that's not the case, a `InvoiceTransitionViolationError` will be thrown.
*
* Requires dependencies, such as the invoice number validator,
* because it's a core business rule that they are unique.
*
* This is the preferred method for issuing invoices as it aligns with real-world
* accounting terminology where invoices are "issued" to customers.
*
* @see {@link InvoiceNumberValidator}
* @see {@link InvoiceStateViolationError}
* @see {@link InvoiceAggregateDependencies}
*/
static issue(data: IssueInvoiceData, pdfBytes: Uint8Array, deps: InvoiceAggregateDependencies): Promise<Invoice>;
/**
* Creates a new draft invoice and emits an `InvoiceRequested` domain event.
*
* It creates a new invoice number (without reservation).
* The returned invoice can be used for actual issuing.
*
* A draft is required before issuing an invoice.
* @param data - The data required to create the invoice draft.
* @param deps - The dependencies required for invoice creation, including optional `invoiceIdGenerator` and required `invoiceNumberGenerator`.
* @returns A promise that resolves to the invoice draft when the draft invoice is created and the domain event is added.
*
* @see {@link InvoiceIdGenerator}
* @see {@link InvoiceNumberGenerator}
*/
static createDraft(data: Omit<IssueInvoiceData, "invoiceNumber" | "status">, deps: {
invoiceIdGenerator?: InvoiceIdGenerator;
invoiceNumberGenerator: InvoiceNumberGenerator;
}): Promise<InvoiceDraft>;
private static generateId;
/**
* Transitions the invoice to the given status in a type-safe way.
*
* - To transition to FINALIZED, you must provide PDF bytes.
* - To transition to DRAFT, you must not provide PDF bytes.
*
* @param args - Discriminated union for status and pdfBytes.
* @returns A new invoice instance with the updated status.
* @throws InvoiceTransitionFailedError when the transition is not allowed
*
* @deprecated In preparation to an upcoming issue fix, the {@link Invoice.create}
* method will be renamed and makes the `transitionTo` function obsolete.
* Consider calling {@link Invoice.create} directly.
* This function will be removed in the next major release.
*/
transitionTo(args: {
status: InvoiceStatus.FINALIZED;
pdfBytes: Uint8Array;
} | {
status: InvoiceStatus.DRAFT;
}): Invoice;
/**
* Checks if the current invoice status matches the specified status.
*
* @param status - The status to compare with the invoice's current status.
* @returns `true` if the invoice's status matches the specified status, otherwise `false`.
*/
requireStatus(status: InvoiceStatus): boolean;
/**
* Returns the current status of the invoice.
*
* @returns {InvoiceStatus} The status of the invoice.
*/
getStatus(): InvoiceStatus;
/**
* Validates that invoice totals are mathematically correct
* Business rule: grandTotal = subtotal + taxes + shippingCost - discount
*/
validateTotals(): void;
/**
* Creates the InvoiceIssuedEvent domain event
*/
toDomainEvent(): InvoiceIssuedEvent;
/**
* Get the total number of items in the invoice
*/
getTotalItemCount(): number;
/**
* Get the currency used in this invoice
*/
getCurrency(): string;
/**
* Check if the invoice contains a specific product
*/
containsProduct(productNumber: string): boolean;
/**
* Get formatted invoice summary for display
*/
getSummary(): string;
/**
* Convert to plain object for serialization
*/
toPlainObject(): {
invoiceId: string;
orderId: string;
invoiceNumber: string;
customerSnapshot: ReturnType<CustomerInfo["toPlainObject"]>;
itemsSnapshot: ReturnType<OrderItem["toPlainObject"]>[];
financials: {
subtotal: MoneyPlainObject;
taxes: MoneyPlainObject;
shippingCost: MoneyPlainObject;
discount: MoneyPlainObject;
grandTotal: MoneyPlainObject;
};
createdOn: string;
createdBy: string;
};
/**
* Reconstruct Invoice from plain object (for repository loading)
*/
static fromPlainObject(data: {
invoiceId: string;
orderId: string;
invoiceNumber: string;
customerSnapshot: Parameters<typeof CustomerInfo.fromPlainObject>[0];
itemsSnapshot: Parameters<typeof OrderItem.fromPlainObject>[0][];
financials: {
subtotal: MoneyPlainObject;
taxes: MoneyPlainObject;
shippingCost: MoneyPlainObject;
discount: MoneyPlainObject;
grandTotal: MoneyPlainObject;
};
createdOn: string;
createdBy: string;
pdfBytes: Uint8Array;
status: InvoiceStatus;
}): Invoice;
}
/**
* Represents a draft version of an invoice, extending the base `Invoice` class.
*
* The `InvoiceDraft` class is used to create invoices that are in the draft state,
* allowing for further modifications before finalization. It initializes the invoice
* with the provided details and sets its status to `DRAFT`.
*
* @remarks
* This class is typically used when an invoice is being prepared but has not yet been issued.
*
* @param invoiceId - The unique identifier for the invoice.
* @param orderId - The identifier of the associated order.
* @param invoiceNumber - The invoice number assigned to this draft.
* @param customerSnapshot - A snapshot of the customer information at the time of draft creation.
* @param itemsSnapshot - A readonly array of order items included in the invoice.
* @param financials - The financial details associated with the invoice.
* @param createdBy - The identifier of the user who created the draft.
*/
declare class InvoiceDraft extends Invoice {
constructor(invoiceId: string, orderId: string, invoiceNumber: string, customerSnapshot: CustomerInfo, itemsSnapshot: readonly OrderItem[], financials: InvoiceFinancials, createdBy: string);
}
/**
* Unique identifier type for entities
*/
type UUID = string;
/**
* Represents the data required to issue a new invoice.
*
* This type includes only the fields of {@link InvoiceDraft}, omitting any methods
* and some fields that are not relevant for the issuance process.
*
* The alias exists just to provide clarity to the meaning in the domain model.
*/
type IssueInvoiceData = Omit<{
[K in keyof InvoiceDraft as InvoiceDraft[K] extends Function ? never : K]: InvoiceDraft[K];
}, "invoiceId" | "createdOn" | "pdfBytes"> & {
status: InvoiceStatus.DRAFT;
};
/**
* Represents the data required to create a new invoice.
*
* @deprecated Use {@link IssueInvoiceData} instead. "Issue Invoice" is the preferred terminology
* as it aligns with real-world accounting language where invoices are issued to customers.
* This alias will be removed in the next major version.
*
* This type includes only the fields of {@link InvoiceDraft}, omitting any methods
* and some fields that are not relevant for the creation process.
*
* The alias exists just to provide clarity to the meaning in the domain model.
*/
type CreateInvoiceData = IssueInvoiceData;
/**
* Order entity interface (read-only reference)
*/
interface Order {
readonly orderId: string;
readonly customer: CustomerInfo;
readonly items: OrderItem[];
readonly subtotal: Money;
readonly taxes: Money;
/**
* @deprecated This value is ignored and exists for migration purposes only. The domain enforces its own Shipping policies. **Expected version of removal: 0.2.0**
*/
readonly shippingCost: ShippingCost;
readonly discount: Money;
readonly status: OrderStatus;
readonly createdAt: Date;
}
/**
* Order status enumeration
*/
declare enum OrderStatus {
PENDING = "pending",
CONFIRMED = "confirmed",
SHIPPED = "shipped",
DELIVERED = "delivered",
CANCELLED = "cancelled"
}
/**
* Invoice status enumeration
*/
declare enum InvoiceStatus {
DRAFT = "draft",
FINALIZED = "finalized"
}
/**
* Pagination result wrapper
*/
interface PaginatedResult<T> {
readonly items: T[];
readonly totalCount: number;
readonly page: number;
readonly limit: number;
readonly hasNextPage: boolean;
readonly hasPreviousPage: boolean;
}
/**
* Base domain event interface
*/
interface DomainEvent {
readonly eventId: UUID;
readonly eventType: string;
readonly aggregateId: UUID;
readonly occurredOn: Date;
readonly version: 1;
}
/**
* Utility type for making properties optional
*/
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
/**
* Utility type for making properties required
*/
type Required<T, K extends keyof T> = T & {
[P in K]-?: T[P];
};
/**
* An interface that contains functions to make a {@link DomainEvent}
* serializable.
*/
interface SerializableEvent<T extends DomainEvent> {
toPlainObject(): PlainEvent<T>;
}
/**
* Utility type for serializing a DomainEvent to a plain object.
*
* For example, it requires `occurredOn` to be a string instead of a Date.
*/
type PlainEvent<T extends DomainEvent> = Omit<T, "occurredOn" | "toPlainObject"> & {
occurredOn: string;
};
/**
* Base domain error class
*/
declare abstract class DomainError extends Error {
readonly code: ErrorCode;
readonly details?: Record<string, any> | undefined;
constructor(message: string, code: ErrorCode, details?: Record<string, any> | undefined);
}
/**
* Invoice-specific domain errors
*/
declare class InvalidInvoiceNumberError extends DomainError {
constructor(invoiceNumber: string, reason?: string);
}
declare class DuplicateInvoiceNumberError extends DomainError {
constructor(invoiceNumber: string);
}
declare class InvoiceTotalMismatchError extends DomainError {
constructor(expected: Money, actual: Money, because?: Omit<InvoiceFinancials, "grandTotal">);
}
/**
* Error thrown when an invoice draft cannot be found for a given order.
*
* @remarks
* This error is typically used in scenarios where an operation expects an existing invoice draft
* associated with a specific order, but none is found in the system, e.g. when issuing an invoice.
*
* @extends DomainError
*
* @param orderId - The identifier of the order for which the invoice draft was not found.
*
* @example
* ```typescript
* throw new InvoiceDraftNotFoundError('order-123');
* ```
*/
declare class InvoiceDraftNotFoundError extends DomainError {
constructor(orderId: string);
}
/**
* @deprecated An invoice that already exists is likely an invoice with the same invoice number.
* Therefore, use {@link DuplicateInvoiceNumberError} instead.
* This error will be removed in the next major version.
*/
declare class InvoiceAlreadyExistsError extends DomainError {
constructor(orderId: string);
}
declare class InvoiceIntegrityCheckError extends DomainError {
readonly invoiceId: string;
readonly reason: "INVOICE_NUMBER_MISMATCH";
readonly reasonDetails?: (string | Record<string, string | number>) | undefined;
constructor(invoiceId: string, reason: "INVOICE_NUMBER_MISMATCH", reasonDetails?: (string | Record<string, string | number>) | undefined);
}
declare class InvoiceStateViolationError extends DomainError {
constructor(invoiceId: string, expected: InvoiceStatus, received: InvoiceStatus);
}
declare class InvoiceTransitionFailedError extends DomainError {
constructor(invoiceId: string, fromStatus: InvoiceStatus, toStatus: InvoiceStatus);
}
declare class OrderNotFoundError extends DomainError {
constructor(orderId: string);
}
/**
* Error codes for domain errors
*/
declare enum ErrorCode {
INVALID_INVOICE_NUMBER = "INVALID_INVOICE_NUMBER",
DUPLICATE_INVOICE_NUMBER = "DUPLICATE_INVOICE_NUMBER",
TOTAL_MISMATCH = "TOTAL_MISMATCH",
ORDER_NOT_FOUND = "ORDER_NOT_FOUND",
INVOICE_ALREADY_EXISTS = "INVOICE_ALREADY_EXISTS",
INVALID_CUSTOMER_DATA = "INVALID_CUSTOMER_DATA",
INVALID_ORDER_ITEMS = "INVALID_ORDER_ITEMS",
PDF_GENERATION_FAILED = "PDF_GENERATION_FAILED",
SHIPPING_COST_NEGATIVE = "SHIPPING_COST_NEGATIVE",
TRANSITION_VIOLATION = "TRANSITION_VIOLATION",
INVOICE_STATE_VIOLATION = "INVOICE_STATE_VIOLATION",
INVOICE_DRAFT_NOT_FOUND = "INVOICE_DRAFT_NOT_FOUND"
}
/**
* Shipping cost extends Money and enforces special business rules,
* namely custom shipping policies and dynamic shipping rates.
*
* It has to extend Money so that it can be used in combination
* with the Money arithmetics, e.g. for grand total calculation.
*/
declare class ShippingCost extends Money {
#private;
readonly cost: Money;
/**
* Determines the shipping cost based on an order's grand total value.
* Due to business rules, shipping is applied (or not applied) based on the grand total.
* @param grandTotal The order's grand total value
* @param shippingRates Shipping rates provider that provides the costs
* @param shippingPolicy Policy that influences the way costs are calculated
* @returns Shipping costs based on business rules, policies and rates.
*/
static determine(grandTotal: Money, shippingRates: ShippingRateProvider, shippingPolicy: ShippingCostPolicy): ShippingCost;
constructor(cost: Money);
}
/**
* Represents the financial breakdown of an invoice, including subtotal, taxes, shipping cost, discount, and grand total.
*
* @remarks
* The grand total is validated to ensure it equals the sum of the subtotal, taxes, and shipping cost, minus the discount.
* If the calculated grand total does not match the provided value, an {@link InvoiceTotalMismatchError} is thrown.
*
* @param subtotal - The total amount before taxes, shipping, and discounts.
* @param taxes - The total tax amount applied to the invoice.
* @param shippingCost - The cost associated with shipping.
* @param discount - The total discount applied to the invoice.
* @param grandTotal - The final total amount after applying taxes, shipping, and discounts.
*
* @throws InvoiceTotalMismatchError - Thrown if the calculated grand total does not match the provided grand total.
*/
declare class InvoiceFinancials {
readonly subtotal: Money;
readonly taxes: Money;
readonly shippingCost: ShippingCost;
readonly discount: Money;
readonly grandTotal: Money;
constructor(subtotal: Money, taxes: Money, shippingCost: ShippingCost, discount: Money, grandTotal: Money);
}
/**
* German legal information required on invoices
*/
interface GermanLegalInfo {
readonly companyName: string;
readonly companyAddress: {
readonly street: string;
readonly zip: string;
readonly city: string;
readonly country: string;
};
readonly contactInfo: {
readonly phone: string;
readonly email: string;
readonly website?: string;
};
readonly legalInfo: {
readonly vatNumber: string;
readonly taxNumber: string;
readonly commercialRegister?: string;
readonly managingDirector?: string;
};
readonly bankInfo: {
readonly bankName: string;
readonly iban: string;
readonly bic: string;
};
}
/**
* Complete invoice data for PDF generation
*/
interface InvoiceData {
readonly invoiceId: string;
readonly invoiceNumber: string;
readonly orderId: string;
readonly customer: CustomerInfo;
readonly items: readonly OrderItem[];
readonly financials: InvoiceFinancials;
readonly createdOn: Date;
readonly createdBy: string;
readonly legalInfo: GermanLegalInfo;
}
/**
* PDF generation options
*/
interface PDFOptions {
readonly format: "A4" | "Letter";
readonly language: "de" | "en";
readonly includeZUGfERD: boolean;
readonly watermark?: string;
readonly logoUrl?: string;
}
/**
* Default PDF options for German invoices
*/
declare const DEFAULT_PDF_OPTIONS: PDFOptions;
/**
* Domain service interface for PDF generation
* Handles creation of legally compliant German invoice PDFs
*/
interface PDFRenderer {
/**
* Renders an invoice as PDF bytes
* @param invoiceData Complete invoice data
* @param options PDF generation options
* @returns Promise resolving to PDF bytes
* @throws PDFGenerationError if rendering fails
*/
render(invoiceData: InvoiceData, options?: PDFOptions): Promise<Uint8Array>;
/**
* Validates that all required legal information is present
* @param legalInfo German legal information
* @throws InvalidLegalInfoError if required information is missing
*/
validateLegalInfo(legalInfo: GermanLegalInfo): void;
/**
* Generates ZUGfERD XML data for PDF/A-3 compliance
* @param invoiceData Invoice data
* @returns XML string for embedding
*/
generateZUGfERDXML(invoiceData: InvoiceData): string;
}
/**
* Error thrown when PDF generation fails
*/
declare class PDFGenerationError extends Error {
readonly cause?: Error | undefined;
constructor(message: string, cause?: Error | undefined);
}
/**
* Error thrown when legal information is invalid or incomplete
*/
declare class InvalidLegalInfoError extends Error {
readonly missingFields: string[];
constructor(message: string, missingFields: string[]);
}
/**
* Base implementation of PDFRenderer with validation logic
* Concrete implementations should extend this class
*/
declare abstract class BasePDFRenderer implements PDFRenderer {
abstract render(invoiceData: InvoiceData, options?: PDFOptions): Promise<Uint8Array>;
validateLegalInfo(legalInfo: GermanLegalInfo): void;
generateZUGfERDXML(invoiceData: InvoiceData): string;
/**
* Robust IBAN validation (ISO 13616 + MOD 97-10)
* - Strips spaces
* - Checks basic structure
* - Validates country-specific length
* - Performs modulo 97 check
*/
private isValidIBAN;
/**
* Basic German VAT number validation
*/
private isValidGermanVATNumber;
/**
* Format date for XML (YYYYMMDD)
*/
private formatDateForXML;
}
export { AggregateRoot, BasePDFRenderer, type CreateInvoiceData, CustomerInfo, DEFAULT_GENERATOR_CONFIG, DEFAULT_INVOICE_NUMBER_CONFIG, DEFAULT_PDF_OPTIONS, DomainError, type DomainEvent, type DomainEventPublisher, DuplicateInvoiceNumberError, ErrorCode, GENERATOR_PRESETS, type GermanLegalInfo, InvalidInvoiceNumberError, InvalidLegalInfoError, Invoice, InvoiceAlreadyExistsError, InvoiceCreatedEvent, type InvoiceData, InvoiceDraft, InvoiceDraftNotFoundError, InvoiceFinancials, type InvoiceIdGenerator, InvoiceIntegrityCheckError, InvoiceIssuedEvent, type InvoiceNumberConfig, type InvoiceNumberGenerator, type InvoiceNumberGeneratorConfig, InvoiceNumberGeneratorImpl, type InvoiceNumberRepository, type InvoiceNumberValidator, InvoiceNumberValidatorImpl, InvoicePdfGeneratedEvent, InvoiceRequested, InvoiceStateViolationError, InvoiceStatus, InvoiceTotalMismatchError, InvoiceTransitionFailedError, InvoiceValidationFailedEvent, type IssueInvoiceData, Money, type MoneyPlainObject, type Optional, type Order, OrderItem, OrderNotFoundError, OrderStatus, PDFGenerationError, type PDFOptions, type PDFRenderer, type PaginatedResult, type PlainEvent, type Required, type SerializableEvent, ShippingCost, type ShippingCostPolicy, type ShippingRateProvider, type UUID, createInvoiceNumberGenerator, generateEventId, validateGeneratedNumber };