diff --git a/README.rst b/README.rst
index 8189860..4601811 100644
--- a/README.rst
+++ b/README.rst
@@ -122,6 +122,7 @@ Example:
"BIC": "BANKNL2A",
"batch": True,
# For non-SEPA transfers, set "domestic" to True, necessary e.g. for CH/LI
+ # For SEPA Instant (SCT Inst / Echtzeitüberweisung), set "instant" to True
"currency": "EUR", # ISO 4217
"address": {
# The address and all of its fields are optional but in some countries they are required
diff --git a/sepaxml/transfer.py b/sepaxml/transfer.py
index 669632f..829c6b1 100644
--- a/sepaxml/transfer.py
+++ b/sepaxml/transfer.py
@@ -108,6 +108,8 @@ def add_payment(self, payment):
)
if not self._config.get('domestic', False):
PmtInf_nodes['Cd_SvcLvl_Node'].text = "SEPA"
+ if self._config.get('instant', False):
+ PmtInf_nodes['Cd_LclInstrm_Node'].text = "INST"
if 'execution_date' in payment:
if self.schema == "pain.001.001.03":
PmtInf_nodes['ReqdExctnDtNode'].text = payment['execution_date']
@@ -226,6 +228,9 @@ def _create_PmtInf_node(self):
if not self._config.get('domestic', False):
ED['SvcLvlNode'] = ET.Element("SvcLvl")
ED['Cd_SvcLvl_Node'] = ET.Element("Cd")
+ if self._config.get('instant', False):
+ ED['LclInstrmNode'] = ET.Element("LclInstrm")
+ ED['Cd_LclInstrm_Node'] = ET.Element("Cd")
ED['ReqdExctnDtNode'] = ET.Element("ReqdExctnDt")
ED['ReqdExctnDt_Dt_Node'] = ET.Element("Dt")
@@ -287,6 +292,9 @@ def _add_non_batch(self, TX_nodes, PmtInf_nodes):
if not self._config.get('domestic', False):
PmtInf_nodes['SvcLvlNode'].append(PmtInf_nodes['Cd_SvcLvl_Node'])
PmtInf_nodes['PmtTpInfNode'].append(PmtInf_nodes['SvcLvlNode'])
+ if self._config.get('instant', False):
+ PmtInf_nodes['LclInstrmNode'].append(PmtInf_nodes['Cd_LclInstrm_Node'])
+ PmtInf_nodes['PmtTpInfNode'].append(PmtInf_nodes['LclInstrmNode'])
PmtInf_nodes['PmtInfNode'].append(PmtInf_nodes['PmtTpInfNode'])
if 'ReqdExctnDtNode' in PmtInf_nodes:
PmtInf_nodes['PmtInfNode'].append(PmtInf_nodes['ReqdExctnDtNode'])
@@ -400,6 +408,8 @@ def _finalize_batch(self):
PmtInf_nodes['BtchBookgNode'].text = "true"
if not self._config.get('domestic', False):
PmtInf_nodes['Cd_SvcLvl_Node'].text = "SEPA"
+ if self._config.get('instant', False):
+ PmtInf_nodes['Cd_LclInstrm_Node'].text = "INST"
if batch_meta:
if self.schema == "pain.001.001.03":
@@ -438,6 +448,9 @@ def _finalize_batch(self):
if not self._config.get('domestic', False):
PmtInf_nodes['SvcLvlNode'].append(PmtInf_nodes['Cd_SvcLvl_Node'])
PmtInf_nodes['PmtTpInfNode'].append(PmtInf_nodes['SvcLvlNode'])
+ if self._config.get('instant', False):
+ PmtInf_nodes['LclInstrmNode'].append(PmtInf_nodes['Cd_LclInstrm_Node'])
+ PmtInf_nodes['PmtTpInfNode'].append(PmtInf_nodes['LclInstrmNode'])
PmtInf_nodes['PmtInfNode'].append(PmtInf_nodes['PmtTpInfNode'])
if 'ReqdExctnDtNode' in PmtInf_nodes:
PmtInf_nodes['PmtInfNode'].append(PmtInf_nodes['ReqdExctnDtNode'])
diff --git a/tests/transfer/test_instant.py b/tests/transfer/test_instant.py
new file mode 100644
index 0000000..302ab53
--- /dev/null
+++ b/tests/transfer/test_instant.py
@@ -0,0 +1,219 @@
+import datetime
+
+import pytest
+
+from sepaxml import SepaTransfer
+from tests.utils import clean_ids, validate_xml
+
+
+@pytest.fixture
+def strf_instant():
+ return SepaTransfer({
+ "name": "TestCreditor",
+ "IBAN": "NL50BANK1234567890",
+ "BIC": "BANKNL2A",
+ "batch": True,
+ "instant": True,
+ "currency": "EUR"
+ }, schema="pain.001.001.03")
+
+
+# Identical to tests/transfer/test_00100103.py's SAMPLE_RESULT, but with the
+# SEPA Instant local instrument (LclInstrm/Cd=INST) emitted directly after
+# SvcLvl inside PmtTpInf -- the ISO 20022 PaymentTypeInformation sequence is
+# InstrPrty?, SvcLvl?, LclInstrm?, CtgyPurp?.
+SAMPLE_RESULT = b"""
+
+
+
+ 20180724040432-d24ce3b3e284
+ 2018-07-24T16:04:32
+ 2
+ 60.12
+
+ TestCreditor
+
+
+
+ TestCreditor-90102652f82a
+ TRF
+ true
+ 2
+ 60.12
+
+
+ SEPA
+
+
+ INST
+
+
+ 2018-07-24
+
+ TestCreditor
+
+
+
+ NL50BANK1234567890
+
+
+
+
+ BANKNL2A
+
+
+ SLEV
+
+
+ NOTPROVIDED
+
+
+ 10.12
+
+
+
+ BANKNL2A
+
+
+
+ Test von Testenstein
+
+
+
+ NL50BANK1234567890
+
+
+
+ Test transaction1
+
+
+
+
+ NOTPROVIDED
+
+
+ 50.00
+
+
+
+ BANKNL2A
+
+
+
+ Test du Test
+
+
+
+ NL50BANK1234567890
+
+
+
+ Test transaction2
+
+
+
+
+
+"""
+
+
+def test_instant_batch_emits_inst_after_svclvl(strf_instant):
+ payment1 = {
+ "name": "Test von Testenstein",
+ "IBAN": "NL50BANK1234567890",
+ "BIC": "BANKNL2A",
+ "amount": 1012,
+ "execution_date": datetime.date.today(),
+ "description": "Test transaction1"
+ }
+ payment2 = {
+ "name": "Test du Test",
+ "IBAN": "NL50BANK1234567890",
+ "BIC": "BANKNL2A",
+ "amount": 5000,
+ "execution_date": datetime.date.today(),
+ "description": "Test transaction2"
+ }
+
+ strf_instant.add_payment(payment1)
+ strf_instant.add_payment(payment2)
+ xmlout = strf_instant.export()
+ xmlpretty = validate_xml(xmlout, "pain.001.001.03")
+ assert clean_ids(xmlpretty.strip()) == clean_ids(SAMPLE_RESULT.strip())
+
+
+def test_instant_non_batch_emits_inst_after_svclvl():
+ strf = SepaTransfer({
+ "name": "TestCreditor",
+ "IBAN": "NL50BANK1234567890",
+ "BIC": "BANKNL2A",
+ "batch": False,
+ "instant": True,
+ "currency": "EUR"
+ }, schema="pain.001.001.03")
+
+ strf.add_payment({
+ "name": "Test von Testenstein",
+ "IBAN": "NL50BANK1234567890",
+ "BIC": "BANKNL2A",
+ "amount": 1012,
+ "execution_date": datetime.date.today(),
+ "description": "Test transaction1"
+ })
+
+ xmlout = strf.export()
+ # export() output is not pretty-printed, so the elements are adjacent:
+ # asserting the compact substring pins both presence and SvcLvl->LclInstrm order.
+ assert b"SEPAINST" in xmlout
+ validate_xml(xmlout, "pain.001.001.03")
+
+
+def test_no_instant_flag_emits_no_lclinstrm():
+ """Without the flag, the transfer is a plain SEPA transfer -- no INST."""
+ strf = SepaTransfer({
+ "name": "TestCreditor",
+ "IBAN": "NL50BANK1234567890",
+ "BIC": "BANKNL2A",
+ "batch": True,
+ "currency": "EUR"
+ }, schema="pain.001.001.03")
+
+ strf.add_payment({
+ "name": "Test von Testenstein",
+ "IBAN": "NL50BANK1234567890",
+ "BIC": "BANKNL2A",
+ "amount": 1012,
+ "execution_date": datetime.date.today(),
+ "description": "Test transaction1"
+ })
+
+ xmlout = strf.export()
+ assert b"LclInstrm" not in xmlout
+ assert b"INST" not in xmlout
+ validate_xml(xmlout, "pain.001.001.03")
+
+
+def test_instant_false_emits_no_lclinstrm():
+ """instant=False is the verified plain-SEPA fallback -- no INST node."""
+ strf = SepaTransfer({
+ "name": "TestCreditor",
+ "IBAN": "NL50BANK1234567890",
+ "BIC": "BANKNL2A",
+ "batch": True,
+ "instant": False,
+ "currency": "EUR"
+ }, schema="pain.001.001.03")
+
+ strf.add_payment({
+ "name": "Test von Testenstein",
+ "IBAN": "NL50BANK1234567890",
+ "BIC": "BANKNL2A",
+ "amount": 1012,
+ "execution_date": datetime.date.today(),
+ "description": "Test transaction1"
+ })
+
+ xmlout = strf.export()
+ assert b"LclInstrm" not in xmlout
+ assert b"INST" not in xmlout
+ validate_xml(xmlout, "pain.001.001.03")