efaturaefatura
Beta

Beta documentation. APIs may change before the stable release.

Currency Conversion

@akira-io/efatura prepares a foreign-currency application invoice as one fiscal CVE projection. The package obtains one normalized quote, converts first-class monetary fields, records the original payable amount, and returns the invoice and conversion evidence together.

The official basis is the e-Fatura technical manual, the technical manual v11.0, the BCV exchange statistics page, and the XSD files embedded under resources/xsd/efatura/2024-05-27.

1. Fiscal Model: DFE In CVE And Alternative Payable Amount

A DFE is always issued in Cape Verde escudo. Conversion does not turn the DFE into a EUR, USD, or other foreign-currency document. All fiscal line amounts, taxes, totals, XML values, and primary DFA values in the prepared invoice are CVE.

For a foreign source invoice, prepareInvoiceToCve() adds one PayableAlternativeAmount to the converted totals. It records:

  • the original payable value;
  • the original ISO 4217 alphabetic currency code;
  • the multiplier applied from the original currency to CVE.

For 200 EUR at 110.265 CVE per EUR, the fiscal payable amount is 22053 CVE and the alternative amount is 200 EUR:

<PayableAmount>22053</PayableAmount>
<PayableAlternativeAmount CurrencyCode="EUR" ExchangeRate="110.265">200</PayableAlternativeAmount>

The DFE XML has no official fields for the provider name, retrieval time, or source URL. Keep those fields in application audit storage and pass them to DFA rendering through trusted facade code.

When sourceCurrency is CVE, preparation uses an identity quote with rate 1, provider identity, and rate type reference. It does not call the configured provider or add a CVE alternative amount.

Preparation requires invoice totals and a payable amount for both foreign and CVE identity paths. A document without totals cannot produce truthful original and converted payable metadata, so preparation fails with exchange_rate.invoice_invalid instead of recording zero.

2. Installation And Prerequisites

Install the package with Node.js 20 or newer:

npm install @akira-io/efatura

Currency preparation needs:

  • a valid createEfatura() configuration;
  • an InvoiceData value or raw invoice record that passes package validation;
  • a source currency in the active embedded e-Fatura XSD currency list;
  • a configured provider, or network access to BCV when the default provider is used.

The BCV and World Bank providers use the global fetch implementation available in Node.js 20. They require HTTPS and apply request timeouts and response-size limits. A fixed provider needs no network access. A callback provider uses the application service supplied by the consumer.

The later fiscal steps keep their existing prerequisites. XSD validation through XmllintXsdValidator requires xmllint. XAdES-BES signing requires valid certificate and private-key material. Middleware submission requires transmitterKey; direct platform submission requires an access token.

3. Default BCV Example

createEfatura(config) creates a BcvExchangeRateProvider when exchangeRateProvider is omitted. The facade and the provider share the configured clock.

import { createEfatura } from '@akira-io/efatura';

const efatura = createEfatura(config);
const prepared = await efatura.prepareInvoiceToCve(invoiceInEur, {
  sourceCurrency: 'EUR',
});

The facade derives effectiveAt from the invoice issue date and explicit issue time in Cape Verde time. If the invoice has no issue time, it combines the issue date with the configured clock’s current Cape Verde time-of-day at fixed UTC-01. An explicit effectiveAt option takes priority over both paths. Host-local timezone settings do not affect this value. The default BCV request asks for a buy quote targeting CVE and requires the publication date to match the requested date.

The default path performs a network request. Use the fixed-provider example for a deterministic local run: EUR To CVE With A Fixed Rate.

4. Complete Preparation, IUD, XML, XSD Validation, DFA, Signing, And Submission Flow

Resolve one quote before generating any fiscal representation. Use the returned prepared.invoice for XML and DFA, and pass prepared.conversion to DFA rendering.

import { EmissionMode, createEfatura } from '@akira-io/efatura';

const efatura = createEfatura(config);
const prepared = await efatura.prepareInvoiceToCve(invoiceInEur, {
  sourceCurrency: 'EUR',
});

const iud = await efatura.buildSequentialIud({
  issueDate: prepared.invoice.issueDate,
  documentType: prepared.invoice.type,
  randomCode: '1234567890',
});

const xml = efatura.buildDfeXml(prepared.invoice, {
  iud,
  emissionMode: EmissionMode.Online,
});

const validation = await efatura.validateDfeXml(xml, prepared.invoice.type);
if (!validation.valid) {
  throw new Error(validation.errors.map((error) => error.message).join('\n'));
}

const dfa = await efatura.renderDfa({
  iud,
  invoice: prepared.invoice,
  conversion: prepared.conversion,
  emissionMode: EmissionMode.Online,
});

const signed = await efatura.signDfeXml(xml, {
  certificate,
  privateKey,
});
const zip = efatura.buildDfeZip([{ iud, xml: signed.xml }]);
const submission = await efatura.submitDfeZip(zip);

Persist the prepared invoice, conversion metadata, IUD, unsigned XML, signed XML, DFA, ZIP, and submission response according to the application’s audit policy. Do not request a second quote for DFA creation or a later reprint. buildDfeXml() remains synchronous and never performs conversion or provider access.

5. Provider Contract And Rate Direction

Providers implement one framework-agnostic contract:

export type ExchangeRateType = 'buy' | 'sell' | 'reference' | 'custom';

export interface ExchangeRateRequest {
  sourceCurrency: string;
  targetCurrency: string;
  effectiveAt: Date;
  rateType?: ExchangeRateType;
}

export interface ExchangeRateEvidenceLeg {
  role: 'source' | 'target';
  currency: string;
  economy: string | null;
  value: string;
  sourceUrl?: string;
}

export interface ExchangeRateEvidence {
  source: string;
  indicator: string;
  observationPeriod: string;
  legs: readonly ExchangeRateEvidenceLeg[];
}

export interface ExchangeRateQuote {
  sourceCurrency: string;
  targetCurrency: string;
  rate: number;
  rateType: ExchangeRateType;
  effectiveAt: Date;
  retrievedAt: Date;
  provider: string;
  sourceUrl?: string;
  evidence?: ExchangeRateEvidence;
}

export interface ExchangeRateProvider {
  getQuote(request: ExchangeRateRequest): Promise<ExchangeRateQuote>;
}

The rate direction is fixed:

amountInTarget = amountInSource * rate

110.265 for EUR to CVE means 1 EUR = 110.265 CVE. Providers normalize upstream units, inverse rates, and cross rates before returning a quote.

The facade method accepts:

export interface PrepareInvoiceToCveOptions {
  sourceCurrency: string;
  effectiveAt?: Date;
  rateType?: ExchangeRateType;
}

sourceCurrency is required, trimmed, uppercased, and checked against the 178 canonical uppercase codes in the active embedded ISO_ISO3AlphaCurrencyCode_2012-08-31.xsd enumeration. The XSD contains 179 entries, but its IdR value is noncanonical. The package rejects canonical IDR before provider access and does not emit mixed-case IdR. The checked-in runtime set requires no filesystem access. Other unsupported codes also fail before provider access. The target is fixed to CVE. effectiveAt uses the invoice issue date and explicit issue time when present. Without an issue time, it uses that date and the configured clock’s current Cape Verde time-of-day at fixed UTC-01. An explicit option overrides either derived value. rateType is passed to the provider; the BCV provider defaults it to buy, while the World Bank provider defaults it to reference when called directly. A provider rejects unsupported types instead of reinterpreting them.

The result is:

export interface CurrencyConversionMetadata extends ExchangeRateQuote {
  originalPayableAmount: number;
  convertedPayableAmount: number;
}

export interface PreparedCurrencyInvoice {
  invoice: InvoiceData;
  conversion: CurrencyConversionMetadata;
}

The result contains a new normalized invoice. The source object is not mutated. normalizeCurrencyCode() trims and uppercases a code, then enforces membership in the active e-Fatura XSD’s canonical uppercase currency entries. Schema-listed special codes such as XAU, XTS, and XXX are accepted. Canonical IDR and other codes absent from the usable set are rejected even when the host runtime recognizes them. The low-level payableAlternativeAmountSchema reuses this canonical set, so IDR, IdR, and unknown three-letter values fail before XML generation. validateExchangeRateQuote() applies the same currency check plus the package pair, date, provenance, HTTPS source URL, evidence URL, and rate validations before returning a normalized quote. HTTPS URLs with user information are rejected.

6. BCV Rate Type, Units, Publication Date, Weekend, Staleness, Timeout, And Current-Page Limitation

BcvExchangeRateProvider is the fiscal default. It reads the official BCV dated print view and requires exactly one identified rate table. The official date comes from one anchored spanning th[colspan="5"] publication row inside that table. One immediately adjacent semantic heading remains a compatibility shape when the spanning row is absent. Multiple date candidates, rate tables, header rows, or requested-currency rows fail closed. Unrelated tables and headings are ignored. Despite the _expType=PDF query parameter, the current response is textual HTML.

import { BcvExchangeRateProvider, createEfatura } from '@akira-io/efatura';

const provider = new BcvExchangeRateProvider({
  timeoutMs: 10_000,
  maxResponseBytes: 1024 * 1024,
  allowPreviousPublication: true,
  maxPublicationAgeDays: 3,
});

const efatura = createEfatura(config, { exchangeRateProvider: provider });
OptionDefaultBehaviour
fetcherglobal fetchHTTP implementation used for the official page
clockSystemClockSupplies retrievedAt
sourceUrlofficial BCV print URLOrigin must be exactly https://www.bcv.cv, without credentials or a non-default port
timeoutMs10000Positive safe integer that aborts a slow request
maxResponseBytes1048576Positive safe integer that rejects a response above 1 MiB
allowPreviousPublicationfalsePermits an earlier publication only when enabled
maxPublicationAgeDays0Nonnegative safe integer for the earlier-publication age

The provider defaults to the BCV buy column. Set rateType: 'sell' in preparation options when the sell column is required and available. reference and custom are rejected for BCV.

BCV can publish a row for more than one source-currency unit. The parser divides the published CVE value by the unit count. A row of 11,026.50 CVE for 100 EUR becomes 110.265 CVE for one EUR. Missing currencies, malformed tables, non-positive units, non-positive rates, absent publication dates, HTTP failures, oversized responses, and timeouts fail closed.

The configured sourceUrl origin must be exactly https://www.bcv.cv. Automatic redirects are disabled. Redirect responses are rejected without reading or exposing their Location value. Use FixedExchangeRateProvider for an approved static quote or CallbackExchangeRateProvider for another trusted integration; neither receives BCV attribution automatically.

timeoutMs and maxResponseBytes must be positive safe integers. maxPublicationAgeDays must be a finite nonnegative safe integer. Fractions, infinities, unsafe integers, and invalid signs fail at construction.

By default, the publication date must equal the requested instant’s fixed UTC-01 Cape Verde calendar date. The comparison changes date at 01:00Z, including month and year boundaries; it does not use the UTC date directly. For a weekend or public holiday, enable allowPreviousPublication and set a positive maxPublicationAgeDays. The provider records the actual earlier publication date and rejects future or older publications. Setting allowPreviousPublication: true while leaving the maximum age at 0 does not permit an earlier date.

The BCV print page is current and dynamic. It is not a documented historical-rate API. Changing effectiveAt does not make that page return an archived publication. The BCV provider does not accept a separately hosted historical page. For audited historical rates, use FixedExchangeRateProvider or a trusted CallbackExchangeRateProvider with provenance that identifies the source actually used.

The package never switches to World Bank, a cached value, a prior day, or another provider after a BCV failure. Any fallback policy must be explicit in application code and must preserve the provenance of the rate that was applied.

7. World Bank Annual-Reference Limitation And Economy Mapping

WorldBankExchangeRateProvider is optional and must be injected explicitly. Its default indicator, PA.NUS.FCRF, is an annual official exchange-rate observation. It can support reporting, analytics, imports, and application-defined reference policies. It is not a BCV daily fiscal quote and is never the fiscal default.

import { WorldBankExchangeRateProvider, createEfatura } from '@akira-io/efatura';

const provider = new WorldBankExchangeRateProvider({
  economyByCurrency: {
    EUR: 'EMU',
  },
});

const efatura = createEfatura(config, { exchangeRateProvider: provider });
const prepared = await efatura.prepareInvoiceToCve(invoiceInEur, {
  sourceCurrency: 'EUR',
  rateType: 'reference',
});

The API is organized by economy, not currency. The CVE to CPV mapping is locked and cannot be replaced by consumer configuration. USD is treated as one USD per USD without a lookup. Every other currency needs an explicit unambiguous economyByCurrency entry.

OptionDefaultBehaviour
fetcherglobal fetchHTTP implementation used for World Bank requests
clockSystemClockSupplies retrievedAt
economyByCurrency{ CVE: 'CPV' }Adds mappings without replacing CVE to CPV
indicatorPA.NUS.FCRFThe only supported indicator is PA.NUS.FCRF
baseUrlhttps://api.worldbank.orgMust remain the official HTTPS World Bank API origin
timeoutMs10000Aborts slow requests
maxResponseBytes1048576Rejects a response above 1 MiB

The provider reads source and target observations for the same requested UTC year, calculates the normalized multiplier, rounds it to five fractional digits, and returns rateType: 'reference'. effectiveAt is January 1 of the observation year, retrievedAt is the fetch time, and sourceUrl identifies the official indicator page. Custom hosts and indicators are rejected so a custom service cannot receive World Bank attribution.

World Bank quotes include optional ExchangeRateEvidence metadata. It records source: 'World Bank', the indicator, observation period, and both cross-rate legs. Each leg preserves its role, currency, exact economy mapping, decimal observation value, and exact API endpoint. USD identity legs record value 1 without an observation endpoint. Persist this metadata with CurrencyConversionMetadata for audit replay. Missing mappings, missing observations, mismatched periods, invalid responses, and non-reference rate requests fail.

8. Fixed User Rate

Use FixedExchangeRateProvider when an audited rate is approved outside the package or when deterministic execution is required.

import { FixedExchangeRateProvider, createEfatura } from '@akira-io/efatura';

const provider = new FixedExchangeRateProvider({
  sourceCurrency: 'EUR',
  targetCurrency: 'CVE',
  rate: 110.265,
  effectiveAt: new Date('2026-07-21T00:00:00.000Z'),
  retrievedAt: new Date('2026-07-21T10:00:00.000Z'),
  rateType: 'custom',
  provider: 'Rate approved by the accounting team',
  sourceUrl: 'https://internal.example/rates/2026-07-21',
});

const efatura = createEfatura(config, { exchangeRateProvider: provider });

Constructor fields:

FieldRequiredDefault
sourceCurrencyyesnone
targetCurrencyyesnone
rateyesnone; must normalize to a positive value
effectiveAtyesnone
retrievedAtnoeffectiveAt
rateTypenocustom
provideryesnone; must contain non-whitespace text
sourceUrlnoomitted; when present, must use HTTPS

The provider accepts only the configured pair, rate type when requested, and an effective date on or after the quote date. It does not invent provenance. The application is responsible for approving and storing the source evidence.

The complete tested example is EUR To CVE With A Fixed Rate.

9. Callback Provider

CallbackExchangeRateProvider adapts an application-owned service to ExchangeRateProvider.

import { CallbackExchangeRateProvider, createEfatura } from '@akira-io/efatura';

const provider = new CallbackExchangeRateProvider(async (request) => {
  const rate = await applicationRates.getApprovedQuote(request);

  return {
    sourceCurrency: request.sourceCurrency,
    targetCurrency: request.targetCurrency,
    rate: rate.value,
    rateType: rate.type,
    effectiveAt: rate.effectiveAt,
    retrievedAt: new Date(),
    provider: 'Application treasury service',
    sourceUrl: rate.auditUrl,
  };
});

const efatura = createEfatura(config, { exchangeRateProvider: provider });

The callback type is:

export type ExchangeRateCallback = (
  request: ExchangeRateRequest,
) => Promise<ExchangeRateQuote>;

Callback results pass through the same quote validator as built-in providers. A callback cannot bypass pair, rate type, date, positive-rate, provider, or HTTPS source URL requirements. An ExchangeRateError from the callback keeps its original code. Another thrown value becomes exchange_rate.provider_unavailable with the original value as its cause.

An application may implement an explicit provider sequence inside its callback. The returned provider, effectiveAt, retrievedAt, and sourceUrl must describe the quote that was used, not the provider that failed first.

10. Monetary-Field Conversion Matrix

Line, reference, payment, and non-reconciled total amounts are multiplied by the same normalized quote and rounded independently. Null values remain null. Fiscal aggregate totals are then recomputed from the rounded converted lines so the prepared invoice remains internally consistent.

LocationConversion or reconciliation behavior
lines[]price, priceExtension, netTotal
lines[].discountvalue when valueType is A or absent
lines[].taxes[]taxAmount, taxTotal
totalschargeTotalAmount, discountTotalAmount, and payableAmount are multiplied and rounded independently
totals.priceExtensionTotalAmountsigned sum of rounded line priceExtension values, excluding informational lines
totals.netTotalAmountsigned sum of rounded line netTotal values, excluding informational lines
totals.taxTotalAmountsigned sum of rounded non-withholding line tax totals under the package fiscal rules
totals.withholdingTaxTotalAmountsigned sum of rounded income-tax line totals; remains null only when no converted withholding evidence exists
totals.payableRoundingAmountderived as payableAmount - netTotalAmount - taxTotalAmount + withholdingTaxTotalAmount
totals.discountvalue when valueType is A or absent
references[]paymentAmount
references[].taxtaxAmount, taxTotal
payments.payments[]paymentAmount

The converter does not change:

  • quantities or package quantities;
  • tax percentages;
  • percentage discounts with valueType: 'P';
  • dates, times, identifiers, descriptions, or codes;
  • numeric or non-numeric extraFields;
  • arbitrary numbers whose fiscal meaning is unknown.

The original source object remains unchanged. The converted invoice is validated again against the package fiscal rules before it is returned.

11. Precision And Rounding

Quote validation uses decimal arithmetic to round the provider rate to at most five fractional digits with half-up rounding. The active embedded XSD defines ExchangeRate as stDecimal5MinExc0, so the serialized rate must be positive and have no more than five fractional digits.

Every directly converted fiscal monetary amount uses decimal arithmetic and two fractional digits with half-up rounding. Signed line and tax accumulation also uses decimal arithmetic. The converter does not derive a line from an already rounded total and does not change an arbitrary line to force reconciliation.

original payable amount: 200.00 EUR
normalized quote:         110.265 CVE per EUR
exact multiplication:    22053.00000 CVE
fiscal payable amount:    22053.00 CVE
alternative amount:      200.00 EUR
XML exchange rate:        110.265

Independent line rounding can create a residual between converted lines, taxes, and the directly converted payable amount. The converter recomputes the fiscal aggregates from rounded line evidence and records that residual in payableRoundingAmount. Preparation then validates the reconciled invoice. It throws exchange_rate.invoice_invalid only when the converted projection still violates a fiscal rule, such as missing line evidence required for totals reconciliation.

The embedded Read Me.txt records an earlier three-decimal revision. The active XSD type in common/CV_EFatura_Types_v1.0.xsd permits five fractional digits, and generated XML is validated against that active schema.

12. Existing Alternative Amounts

Automatic foreign-currency preparation rejects an invoice whose totals already contain any payableAlternativeAmounts. It cannot establish the provenance, direction, date, or compatibility of those existing values. The error code is exchange_rate.alternatives_conflict.

The low-level InvoiceData and XML APIs continue to accept caller-supplied alternative amounts for applications that manage those values independently. Their currency codes use the same canonical schema set as provider quotes. XAU, XTS, and XXX remain valid; IDR, mixed-case IdR, and unknown ZZZ are rejected before XML generation with validation.payable_alternative_currency_unsupported. Replace the invalid value with a canonical uppercase code from the active e-Fatura schema. Do not pass such an invoice to prepareInvoiceToCve().

For a CVE identity preparation, the provider is not called and no CVE alternative amount is created. Existing low-level alternative amounts are preserved by normal invoice validation because no foreign conversion runs.

13. Error Codes And Recovery Decisions

ExchangeRateError extends EfaturaError, exposes a stable code, and accepts an optional cause through its constructor options.

CodeConditionRecovery decision
exchange_rate.provider_unavailableNetwork, timeout, HTTP, or callback failureRetry under an application policy or stop issuance. Do not substitute another source silently.
exchange_rate.response_invalidUpstream content or quote shape cannot be parsed safelyStop and investigate provider or parser changes.
exchange_rate.currency_unsupportedInput is absent from the active e-Fatura XSD currency list, or the provider lacks a required economy mappingUse a schema-listed code or configure the selected provider’s mapping.
exchange_rate.pair_mismatchReturned pair or rate type differs from the requestCorrect provider configuration or callback normalization.
exchange_rate.rate_invalidRate is non-finite, non-positive, or rounds to zeroReject the quote and obtain approved evidence.
exchange_rate.date_unavailableNo quote satisfies the requested dateConfigure an explicit prior-publication policy or audited historical source.
exchange_rate.date_invalidRequested or returned date is invalid or a BCV publication is in the futureCorrect the date source before issuance.
exchange_rate.staleBCV publication exceeds maxPublicationAgeDaysObtain a newer publication or change policy through an approved configuration change.
exchange_rate.source_requiredA configured source URL is empty, invalid, or not allowed by the providerBCV requires the exact official https://www.bcv.cv origin. For another trusted source, use FixedExchangeRateProvider or CallbackExchangeRateProvider.
exchange_rate.invoice_invalidTotals are absent or the converted invoice fails fiscal validationSupply totals or inspect the validation cause and correct the source invoice.
exchange_rate.alternatives_conflictExisting alternative amounts cannot be merged safelyRemove them before automatic preparation or use the low-level API.
import { ExchangeRateError } from '@akira-io/efatura';

try {
  await efatura.prepareInvoiceToCve(invoiceInEur, { sourceCurrency: 'EUR' });
} catch (error) {
  if (error instanceof ExchangeRateError) {
    auditLogger.warn({ code: error.code }, error.message);
  }

  throw error;
}

Error messages and causes derived from built-in upstream response values omit credentials, invalid decimal text, and complete response bodies. Provider and evidence URLs reject user information. Logs may contain the code, provider, pair, and dates when the application has those fields.

14. Audit Persistence And DFA Reprints

Persist both members of PreparedCurrencyInvoice beside the invoice or submission record:

  • invoice, containing the normalized CVE values and canonical alternative payable amount;
  • conversion, containing the pair, normalized rate, rate type, effective date, retrieval date, provider, optional source URL, original payable amount, and converted payable amount.

Dates are Date objects at runtime. When JSON storage converts them to strings, reconstruct them before passing metadata to a renderer:

const conversion = {
  ...stored.conversion,
  effectiveAt: new Date(stored.conversion.effectiveAt),
  retrievedAt: new Date(stored.conversion.retrievedAt),
};

const dfa = await efatura.renderDfa({
  iud: stored.iud,
  invoice: stored.invoice,
  conversion,
});

A reprint must use the stored converted invoice and stored conversion metadata. It must not call prepareInvoiceToCve() again, fetch a new quote, replace retrievedAt, or recalculate CVE values. Provider caches are optional infrastructure concerns; a cache hit must retain the original retrieval time and remain within the configured date policy. Provider failures are not cached by the package.

The default DFA displays the fiscal payable amount in CVE plus the original amount, source currency, normalized direction, effective date, provider, and optional source URL. Custom renderers receive the same conversion value through DfaRenderInput. DFA conversion metadata is validated against the invoice before rendering: the target must be CVE, totals must exist, and the converted payable amount must match both the invoice and the original payable amount multiplied by the normalized rate, rounded to two fractional digits with half-up rounding. Foreign original value, currency, and rate must match the sole alternative payable amount. Invalid direct metadata throws dfa.conversion_invalid.

15. Security And Adapter Boundary

Built-in providers require HTTPS, limit response size, apply timeouts, and parse upstream content as untrusted input. Logs must not contain credentials or full provider responses. Callback authentication and secret handling remain application responsibilities.

Currency preparation is a trusted facade API in this release. The Express, Fastify, and Nest request schemas do not expose a generic conversion route and do not accept client-supplied provider names, source URLs, rates, or callback code as fiscal provenance. Server code selects and configures the provider.

If an application adds its own authenticated endpoint, accept only policy-approved inputs such as source currency, effective date, and allowed rate type. The server must choose the provider and return metadata produced by the trusted preparation result. Apply authentication, authorization, throttling, and timeout controls at the host boundary.

The package performs no silent fallback. Applications that implement an explicit fallback inside a callback must return evidence for the selected quote.

16. Migration From renderDfa({ currency })

RenderDfaOptions.currency is deprecated and will be removed in v1.0.0. It changed a display label without proving that invoice values had been converted, which could misrepresent foreign values as fiscal amounts.

Old code:

await efatura.renderDfa({
  iud,
  invoice: invoiceInEur,
  currency: 'EUR',
});

Migrated code:

const prepared = await efatura.prepareInvoiceToCve(invoiceInEur, {
  sourceCurrency: 'EUR',
});

await efatura.renderDfa({
  iud,
  invoice: prepared.invoice,
  conversion: prepared.conversion,
});

When currency is defined, the package emits a Node.js DeprecationWarning once per process with code EFATURA_RENDER_DFA_CURRENCY_DEPRECATED. The warning occurs before legacy normalization. When invoice is supplied, the renderer always receives currency: 'CVE'; remove the legacy option. For IUD-only rendering, currency: 'CVE' remains accepted during the compatibility period, while a foreign value emits the warning and then throws EfaturaValidationError with code dfa.currency_invalid. Omitting the option emits no warning.

Migration is complete when the application uses prepareInvoiceToCve() before XML or DFA creation, persists the result, removes foreign DFA labels, and reuses stored conversion evidence for every reprint. TypeScript consumers also receive the source-level deprecation annotation.