feat(billing): issue credit/debit memos against registered invoices

POST billing/invoices/:id/memo files a MoR DEB/CRE memo by reusing
createInvoice unchanged. sourceId is the original invoice's own id, not its
source's — this structurally keeps memos out of findPayable/expirePayable/
billQuery's sourceId-keyed lookups regardless of status. Credit notes are
created settled; debit notes are created open/unpaid as a genuine new
receivable, not force-settled. memoIssue is granted to the chief position,
not the general finance role.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-08-15 06:44:41 +00:00
parent 87c51d7676
commit d66cfe2328
5 changed files with 396 additions and 17 deletions

View File

@@ -0,0 +1,71 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsArray,
IsIn,
IsNumber,
IsObject,
IsOptional,
IsString,
Length,
ValidateNested,
} from "class-validator";
/** One line on a memo; omit the whole `lines` array on the parent DTO to copy the original's. */
export class MemoLineDto {
@ApiProperty()
@IsString()
chargeType!: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
description?: string;
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
quantity?: number;
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
unitRate?: number;
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
amount?: number;
@ApiPropertyOptional()
@IsOptional()
@IsString()
currency?: string;
@ApiPropertyOptional()
@IsOptional()
@IsObject()
metadata?: Record<string, unknown>;
}
/** `POST billing/invoices/:id/memo` body — see `BillingService.issueMemo`. */
export class IssueMemoDto {
@ApiProperty({ enum: ["CRE", "DEB"], description: "MoR DocumentDetails.Type for the memo." })
@IsIn(["CRE", "DEB"])
type!: "CRE" | "DEB";
@ApiProperty({ description: "Why the memo was issued — MoR DocumentDetails.Reason." })
@IsString()
@Length(1, 500)
reason!: string;
@ApiPropertyOptional({
type: [MemoLineDto],
description: "Omit to copy every line of the original invoice verbatim.",
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => MemoLineDto)
lines?: MemoLineDto[];
}