Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 34 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,23 +211,27 @@ if (!result.ok) {
input.

```ts
import { parse } from "sepa-xml-ts";
import { parse, MessageType } from "sepa-xml-ts";

const parsed = parse(xml);
if (!parsed.ok) {
console.error(parsed.error);
} else if (parsed.type === "pain.001") {
} else if (parsed.type === MessageType.CreditTransfer) {
// parsed.data is a CreditTransferDocument
const total = parsed.data.batches
.flatMap((b) => b.transfers)
.reduce((sum, t) => sum + t.amount.minorUnits, 0n);
console.log("credit transfer total:", total);
} else {
// parsed.type === "pain.008" -> parsed.data is a DirectDebitDocument
// parsed.type === MessageType.DirectDebit -> parsed.data is a DirectDebitDocument
console.log("collections:", parsed.data.batches.flatMap((b) => b.collections).length);
}
```

`MessageType.CreditTransfer` is `"pain.001"` and `MessageType.DirectDebit` is `"pain.008"`. Raw
string comparisons still type-check: `parsed.type === "pain.001"` is identical in behaviour and
the types are fully compatible.

The round-trip is anchored on the model: for any valid model,
`parse(write(model))` deep-equals the original. This is verified as a property test over thousands
of generated inputs, for both message types.
Expand Down Expand Up @@ -568,13 +572,14 @@ variant is a different XML schema with its own element ordering and element name

The legacy ISO credit transfer schema `pain.001.001.03` is supported as a write target for
systems that have not yet migrated to the modern `pain.001.001.09`. Pass `variant:
'pain.001.001.03'` to emit the older namespace. The model input is the same
`CreditTransferDocument`; only the serialization differs.
'pain.001.001.03'` (or the `CreditTransferVariant.SCT_Legacy` constant) to emit the older
namespace. The model input is the same `CreditTransferDocument`; only the serialization differs.

```ts
import { euros, writeCreditTransfer, type CreditTransferDocument } from "sepa-xml-ts";
import { euros, writeCreditTransfer, CreditTransferVariant, type CreditTransferDocument } from "sepa-xml-ts";

const xml = writeCreditTransfer(doc, { variant: "pain.001.001.03" });
const xml = writeCreditTransfer(doc, { variant: CreditTransferVariant.SCT_Legacy });
// equivalent to: writeCreditTransfer(doc, { variant: "pain.001.001.03" })
// <Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.03">...
```

Expand All @@ -589,15 +594,21 @@ Structural deltas from `pain.001.001.09`:
### German DK variant: pain.001.003.03

The German DK (DFU agreement Anlage 3) uses the namespace
`urn:iso:std:iso:20022:tech:xsd:pain.001.003.03`. Pass `variant: 'pain.001.003.03'` to emit and
validate against this schema. The model input is the same `CreditTransferDocument` for both
variants; only the serialization differs.
`urn:iso:std:iso:20022:tech:xsd:pain.001.003.03`. Pass `variant: 'pain.001.003.03'` (or the
`CreditTransferVariant.SCT_DK` constant) to emit and validate against this schema. The model
input is the same `CreditTransferDocument` for both variants; only the serialization differs.

```ts
import { euros, writeCreditTransfer, type CreditTransferDocument } from "sepa-xml-ts";
import {
euros,
writeCreditTransfer,
CreditTransferVariant,
type CreditTransferDocument,
} from "sepa-xml-ts";
import { validateXsd } from "sepa-xml-ts/xsd";

const xml = writeCreditTransfer(doc, { variant: "pain.001.003.03" });
const xml = writeCreditTransfer(doc, { variant: CreditTransferVariant.SCT_DK });
// equivalent to: writeCreditTransfer(doc, { variant: "pain.001.003.03" })
// <Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.003.03">...

const xsdResult = await validateXsd(xml); // validates against the DK XSD
Expand Down Expand Up @@ -627,15 +638,21 @@ const xml = writeCreditTransfer(doc, {
### German DK variant: pain.008.003.02

The German DK direct debit variant uses the namespace
`urn:iso:std:iso:20022:tech:xsd:pain.008.003.02`. Pass `variant: 'pain.008.003.02'` to emit and
validate against this schema. The model input is the same `DirectDebitDocument` for both
variants; only the serialization differs.
`urn:iso:std:iso:20022:tech:xsd:pain.008.003.02`. Pass `variant: 'pain.008.003.02'` (or the
`DirectDebitVariant.SDD_DK` constant) to emit and validate against this schema. The model input
is the same `DirectDebitDocument` for both variants; only the serialization differs.

```ts
import { euros, writeDirectDebit, type DirectDebitDocument } from "sepa-xml-ts";
import {
euros,
writeDirectDebit,
DirectDebitVariant,
type DirectDebitDocument,
} from "sepa-xml-ts";
import { validateXsd } from "sepa-xml-ts/xsd";

const xml = writeDirectDebit(doc, { variant: "pain.008.003.02" });
const xml = writeDirectDebit(doc, { variant: DirectDebitVariant.SDD_DK });
// equivalent to: writeDirectDebit(doc, { variant: "pain.008.003.02" })
// <Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.008.003.02">...

const xsdResult = await validateXsd(xml); // validates against the DK SDD XSD
Expand Down
13 changes: 11 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export type {
} from './model/schema.js'

export { writeCreditTransfer } from './writer/writer.js'
export type { WriteCreditTransferOptions, CreditTransferVariant } from './writer/writer.js'
export type { WriteCreditTransferOptions } from './writer/writer.js'

// ---------------------------------------------------------------------------
// pain.008 exports
Expand All @@ -97,7 +97,7 @@ export type {
} from './model/pain008.js'

export { writeDirectDebit } from './writer/direct-debit.js'
export type { WriteDirectDebitOptions, DirectDebitVariant } from './writer/direct-debit.js'
export type { WriteDirectDebitOptions } from './writer/direct-debit.js'

export { checkDirectDebitRules } from './model/dd-rules.js'

Expand Down Expand Up @@ -130,3 +130,12 @@ export type {

export type { BankProfile, ProfileIssue } from './profile/profile.js'
export { requireBic, ibanBicCountryMatch } from './profile/profiles.js'

// ---------------------------------------------------------------------------
// Typed message-type and variant constants
// Single source of truth for autocomplete-friendly constants and their derived types.
// Raw string literals remain valid everywhere (no breaking change).
// ---------------------------------------------------------------------------

// Exporting the const values also exposes the type aliases (same name, declaration merging).
export { MessageType, CreditTransferVariant, DirectDebitVariant } from './message-types.js'
113 changes: 113 additions & 0 deletions src/message-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Typed, documented constants for SEPA message types and write variants.
*
* Use these constants instead of bare strings for autocomplete and inline documentation.
* Raw string literals remain valid everywhere (no breaking change): the derived types
* keep the same resolved string-literal union.
*
* Example - parse narrowing:
* import { MessageType } from 'sepa-xml-ts'
* if (result.type === MessageType.CreditTransfer) { ... } // same as === 'pain.001'
*
* Example - write variant:
* import { CreditTransferVariant } from 'sepa-xml-ts'
* writeCreditTransfer(doc, { variant: CreditTransferVariant.SCT_DK })
*/

// ---------------------------------------------------------------------------
// MessageType
// ---------------------------------------------------------------------------

/**
* Discriminator constants returned by `parse()`.
*
* These match the `type` field on a successful `ParseResult` so you can
* narrow the discriminated union with a readable name instead of a raw string.
*/
export const MessageType = {
/**
* SEPA Credit Transfer (pain.001). You push payments to many creditors.
* Returned as `result.type` when `parse()` reads a pain.001 document.
*/
CreditTransfer: 'pain.001',
/**
* SEPA Direct Debit (pain.008). You pull payments from many debtors.
* Returned as `result.type` when `parse()` reads a pain.008 document.
*/
DirectDebit: 'pain.008',
} as const

/** Literal union of all recognised message-type discriminators. */
export type MessageType = (typeof MessageType)[keyof typeof MessageType]

// ---------------------------------------------------------------------------
// CreditTransferVariant
// ---------------------------------------------------------------------------

/**
* Output schema variant constants for `writeCreditTransfer`.
*
* Pass one of these (or the equivalent raw string) as `{ variant }` in
* `WriteCreditTransferOptions`. Defaults to `SCT_V09` when omitted.
*/
export const CreditTransferVariant = {
/**
* Modern ISO SEPA Credit Transfer: pain.001.001.09.
* V09 refers to ISO message version .09 (the current schema, published 2019).
* The default. Use this unless your bank specifically requires an older format.
*/
SCT_V09: 'pain.001.001.09',
/**
* Legacy ISO Credit Transfer: pain.001.001.03.
* Some banks still require this older format on the wire. It uses a plain
* ReqdExctnDt (no Dt wrapper), BIC element instead of BICFI, and an empty
* FinInstnId fallback for DbtrAgt when no BIC is set.
*/
SCT_Legacy: 'pain.001.001.03',
/**
* German DK national Credit Transfer: pain.001.003.03.
* Defined by the Deutsche Kreditwirtschaft (DK). Uses a different namespace,
* plain ReqdExctnDt, BIC element, and NOTPROVIDED fallback for DbtrAgt.
* Restricted to Ctry + AdrLine for postal addresses.
*/
SCT_DK: 'pain.001.003.03',
} as const

/**
* Literal union of all supported credit-transfer write variants.
* Identical to the string union accepted by `WriteCreditTransferOptions.variant`.
*/
export type CreditTransferVariant =
(typeof CreditTransferVariant)[keyof typeof CreditTransferVariant]

// ---------------------------------------------------------------------------
// DirectDebitVariant
// ---------------------------------------------------------------------------

/**
* Output schema variant constants for `writeDirectDebit`.
*
* Pass one of these (or the equivalent raw string) as `{ variant }` in
* `WriteDirectDebitOptions`. Defaults to `SDD_V08` when omitted.
*/
export const DirectDebitVariant = {
/**
* Modern ISO SEPA Direct Debit: pain.008.001.08.
* V08 refers to ISO message version .08 (the current schema, published 2019).
* The default. Use this unless your bank specifically requires the DK variant.
*/
SDD_V08: 'pain.008.001.08',
/**
* German DK national Direct Debit: pain.008.003.02.
* Defined by the Deutsche Kreditwirtschaft (DK). Uses a different namespace,
* BIC element instead of BICFI, NOTPROVIDED fallback, and omits CtrlSum from
* GrpHdr. Restricted to Ctry + AdrLine for postal addresses.
*/
SDD_DK: 'pain.008.003.02',
} as const

/**
* Literal union of all supported direct-debit write variants.
* Identical to the string union accepted by `WriteDirectDebitOptions.variant`.
*/
export type DirectDebitVariant = (typeof DirectDebitVariant)[keyof typeof DirectDebitVariant]
7 changes: 5 additions & 2 deletions src/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import { XMLParser } from 'fast-xml-parser'
import { detectSepaNamespace } from '../xmlns-detect.js'
import type { MessageType } from '../message-types.js'
import {
CreditTransferDocumentSchema,
type CreditTransferDocument,
Expand Down Expand Up @@ -56,14 +57,16 @@ import {

export type ParseSuccess001 = {
ok: true
type: 'pain.001'
/** Discriminator matching `MessageType.CreditTransfer` ("pain.001"). */
type: typeof MessageType.CreditTransfer
/** Detected schema version, e.g. "pain.001.001.09" or "pain.001.001.03". */
version?: string
data: CreditTransferDocument
}
export type ParseSuccess008 = {
ok: true
type: 'pain.008'
/** Discriminator matching `MessageType.DirectDebit` ("pain.008"). */
type: typeof MessageType.DirectDebit
/** Detected schema version, e.g. "pain.008.001.08" or "pain.008.001.02". */
version?: string
data: DirectDebitDocument
Expand Down
8 changes: 2 additions & 6 deletions src/writer/direct-debit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,9 @@ import {
} from './xml-emit.js'
import type { BankProfile } from '../profile/profile.js'
import { checkDirectDebitRules } from '../model/dd-rules.js'
import type { DirectDebitVariant } from '../message-types.js'

/**
* The output schema variant for writeDirectDebit.
* - 'pain.008.001.08': the modern SEPA SDD schema (default, unchanged behavior)
* - 'pain.008.003.02': the German DK national variant (different namespace and structure)
*/
export type DirectDebitVariant = 'pain.008.001.08' | 'pain.008.003.02'
export type { DirectDebitVariant }

/** Options accepted by writeDirectDebit. */
export interface WriteDirectDebitOptions {
Expand Down
9 changes: 2 additions & 7 deletions src/writer/writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,9 @@ import {
emitCdtTrfTxInfHeader,
} from './xml-emit.js'
import type { BankProfile } from '../profile/profile.js'
import type { CreditTransferVariant } from '../message-types.js'

/**
* The output schema variant for writeCreditTransfer.
* - 'pain.001.001.09': the modern SEPA SCT schema (default, unchanged behavior)
* - 'pain.001.001.03': the legacy ISO credit transfer format (for systems still requiring .03 on the wire)
* - 'pain.001.003.03': the German DK national variant (different namespace and structure)
*/
export type CreditTransferVariant = 'pain.001.001.09' | 'pain.001.001.03' | 'pain.001.003.03'
export type { CreditTransferVariant }

/** Options accepted by writeCreditTransfer. */
export interface WriteCreditTransferOptions {
Expand Down
Loading
Loading