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
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,7 @@ table 1226 "Payment Export Data"
local procedure FillSwissFieldsFromVendorBankAccount(VendorBankAccount: Record "Vendor Bank Account")
var
DtaMgt: Codeunit DtaMgt;
CHMgt: Codeunit CHMgt;
begin
if not SwissExport then
exit;
Expand All @@ -853,7 +854,12 @@ table 1226 "Payment Export Data"
end;
"Swiss Payment Type"::"2.2":
begin
"Recipient Bank BIC" := CopyStr("Recipient Bank Acc. No.", 1, MaxStrLen("Recipient Bank BIC"));
// The clearing member id (MmbId) is the bank clearing number. Use the Clearing No. when set,
Comment thread
dcenic marked this conversation as resolved.
Comment thread
dcenic marked this conversation as resolved.
// otherwise derive it from the domestic IBAN so it is not left as the full IBAN.
if VendorBankAccount."Clearing No." <> '' then
"Recipient Bank BIC" := VendorBankAccount."Clearing No."
else
"Recipient Bank BIC" := CHMgt.GetClearingNoFromIBAN(VendorBankAccount.IBAN);
"Recipient Bank Acc. No." := DtaMgt.IBANDELCHR(VendorBankAccount.IBAN);
end;
"Swiss Payment Type"::"6":
Expand Down
23 changes: 23 additions & 0 deletions src/Layers/CH/BaseApp/Local/Bank/Payment/CHMgt.Codeunit.al
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,29 @@ codeunit 11503 CHMgt
exit(CopyStr(DtaMgt.IBANDELCHR(IBAN), 1, 2) in ['CH', 'LI']);
end;

/// <summary>
/// Extracts the Swiss bank clearing number (IID) from a domestic (CH/LI) IBAN.
/// </summary>
/// <param name="IBAN">The IBAN to read the clearing number from.</param>
/// <returns>The clearing number from IBAN positions 5-9 with leading zeros stripped, or an empty code if the IBAN is not a domestic IBAN or is too short.</returns>
internal procedure GetClearingNoFromIBAN(IBAN: Code[50]) ClearingNo: Code[5]
var
DtaMgt: Codeunit DtaMgt;
PureIBAN: Text;
begin
// Swiss/Liechtenstein IBANs carry the 5-digit bank clearing number (IID) in positions 5-9,
// so it can be derived from the IBAN without a separately maintained Clearing No.
if not IsDomesticIBAN(IBAN) then
exit('');

// IBANDELCHR expects Code[37]; IBANs are at most 34 characters, so this never truncates real data.
PureIBAN := DtaMgt.IBANDELCHR(CopyStr(IBAN, 1, 37));
if StrLen(PureIBAN) < 9 then
exit('');

Comment thread
dcenic marked this conversation as resolved.
ClearingNo := CopyStr(DelChr(CopyStr(PureIBAN, 5, 5), '<', '0'), 1, 5);
end;

procedure IsDomesticCurrency(CurrencyCode: Code[10]): Boolean
var
DtaMgt: Codeunit DtaMgt;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -536,8 +536,11 @@ table 288 "Vendor Bank Account"
DomesticIBAN := CHMgt.IsDomesticIBAN(IBAN);

// Payment Type 2.2
// When the Clearing No. field is left blank, the bank clearing number is derived from the domestic IBAN,
Comment thread
dcenic marked this conversation as resolved.
// but only when no SWIFT Code is provided so SWIFT-routed accounts still classify as Payment Type 3/4.
Comment thread
dcenic marked this conversation as resolved.
if ("Payment Form" = "Payment Form"::"Bank Payment Domestic") and
("Clearing No." <> '') and DomesticCurrency and DomesticIBAN and
Comment thread
dcenic marked this conversation as resolved.
(("Clearing No." <> '') or (("SWIFT Code" = '') and (CHMgt.GetClearingNoFromIBAN(IBAN) <> ''))) and
DomesticCurrency and DomesticIBAN and
(IBAN <> '')
then begin
PaymentType := DummyPaymentExportData."Swiss Payment Type"::"2.2";
Expand Down
95 changes: 95 additions & 0 deletions src/Layers/CH/Tests/Local/SwissSEPACT09Export.Codeunit.al
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,27 @@ codeunit 144354 "Swiss SEPA CT 09 Export"
Assert.IsFalse(CHMgt.IsDomesticIBAN('DE12345'), '');
end;

[Test]
[Scope('OnPrem')]
procedure CHMgt_GetClearingNoFromIBAN()
var
CHMgt: Codeunit CHMgt;
begin
// [FEATURE] [AI test 0.3] [UT]
// [SCENARIO] COD 11503 "CHMgt".GetClearingNoFromIBAN() extracts the bank clearing number (IID) from positions 5-9 of a domestic IBAN, strips leading zeros, and returns blank when it cannot be derived
// [THEN] Domestic CH IBAN: clearing number from positions 5-9 with leading zeros stripped
Assert.AreEqual('8888', CHMgt.GetClearingNoFromIBAN('CH3808888123456789012'), 'CH IBAN with a leading zero in the clearing slice');
Assert.AreEqual('12345', CHMgt.GetClearingNoFromIBAN('CH9312345678901234567'), 'CH IBAN with a full 5-digit clearing number');
// [THEN] Domestic LI IBAN is also supported
Assert.AreEqual('8800', CHMgt.GetClearingNoFromIBAN('LI2108800123456789012'), 'LI IBAN clearing slice');
// [THEN] Non-domestic IBAN returns blank
Assert.AreEqual('', CHMgt.GetClearingNoFromIBAN('DE62007620110623852957'), 'Non-domestic IBAN');
// [THEN] Domestic IBAN too short to contain positions 5-9 returns blank
Assert.AreEqual('', CHMgt.GetClearingNoFromIBAN('CH1234'), 'Too-short domestic IBAN');
// [THEN] Domestic IBAN with all-zero clearing digits returns blank
Assert.AreEqual('', CHMgt.GetClearingNoFromIBAN('CH9300000123456789012'), 'All-zero clearing slice');
end;

[Test]
[Scope('OnPrem')]
procedure CHMgt_IsSwissSEPACTExport_Negative()
Expand Down Expand Up @@ -872,6 +893,48 @@ codeunit 144354 "Swiss SEPA CT 09 Export"
VerifyXMLFile(GenJournalLine, FileName, MessageID, PaymentTypeGbl::"2.2");
end;

[Test]
[Scope('OnPrem')]
procedure XMLExport_PaymentType22_CHF_BlankClearingNo()
var
GenJournalLine: Record "Gen. Journal Line";
Comment thread
dcenic marked this conversation as resolved.
LibraryXPathXMLReader: Codeunit "Library - XPath XML Reader";
ExportFile: File;
XMLInStream: InStream;
FileName: Text;
VendorNo: Code[20];
ExpectedMmbId: Code[5];
begin
// [FEATURE] [AI test 0.3] [XML] [Export]
// [SCENARIO] Swiss SEPA CT (pain.001.001.09) export for "Payment Type" = "2.2" derives the clearing member id (MmbId) from the domestic IBAN when "Clearing No." is blank
Initialize();

// [GIVEN] Vendor with bank account having "Payment Form" = "Bank Payment Domestic", blank "Clearing No." and "SWIFT Code", and a domestic IBAN
VendorNo := CreateVendorWithBankAccount(PaymentFormGbl::"Bank Payment Domestic", '', '', '', GetIBAN(true));
// [GIVEN] Positions 5-9 of the domestic IBAN 'CH3808888123456789012' yield clearing member id '8888'
ExpectedMmbId := '8888';

// [GIVEN] Vendor payment journal line with "Currency Code" = ""
CreatePaymentJournalLine(GenJournalLine, VendorNo, '', '',
GenJournalLine."Account Type"::Vendor, GenJournalLine."Document Type"::Payment);

// [WHEN] Export payments to file
FileName := GenJournalLine_XMLExport(GenJournalLine);

// [THEN] The payment is classified as Swiss Payment Type "2.2" (LclInstrm/Prtry = "CH03")
ExportFile.Open(FileName);
ExportFile.CreateInStream(XMLInStream);
LibraryXPathXMLReader.InitializeXml(XMLInStream, 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.09');
ExportFile.Close();
LibraryXPathXMLReader.VerifyXmlNodeValue('//ns:PmtTpInf//ns:LclInstrm/ns:Prtry', 'CH03');
// [THEN] The creditor agent uses the CHBCC clearing system with MmbId = the IID derived from the IBAN, and no BICFI
LibraryXPathXMLReader.VerifyXmlNodeAbsence('//ns:CdtrAgt//ns:BICFI');
LibraryXPathXMLReader.VerifyXmlNodeValue('//ns:CdtrAgt//ns:Cd', 'CHBCC');
LibraryXPathXMLReader.VerifyXmlNodeValue('//ns:CdtrAgt//ns:MmbId', ExpectedMmbId);
// [THEN] The creditor account holds the IBAN
LibraryXPathXMLReader.VerifyXmlNodeValue('//ns:CdtrAcct//ns:IBAN', GetIBAN(true));
end;

[Test]
[Scope('OnPrem')]
procedure XMLExport_PaymentType3_Negative_BlankedSWIFT()
Expand Down Expand Up @@ -2008,6 +2071,38 @@ codeunit 144354 "Swiss SEPA CT 09 Export"
Assert.AreEqual(PaymentTypeGbl::"6", PaymentTypeGbl, GetPaymentTypeErr);
end;

[Test]
[Scope('OnPrem')]
procedure VendorBankAccountGetPaymentTypeType22WhenClearingNoDerivedFromIBAN()
var
VendorBankAccount: Record "Vendor Bank Account";
begin
// [FEATURE] [AI test 0.3] [UT]
// [SCENARIO] "Vendor Bank Account".GetPaymentType returns Swiss Payment Type 2.2 when "Clearing No." and "SWIFT Code" are blank but the clearing number can be derived from a domestic IBAN
VendorBankAccount.Init();
VendorBankAccount."Payment Form" := VendorBankAccount."Payment Form"::"Bank Payment Domestic";
VendorBankAccount."SWIFT Code" := '';
VendorBankAccount.IBAN := GetIBAN(true);
Assert.IsTrue(VendorBankAccount.GetPaymentType(PaymentTypeGbl, GetCurrencyCode('')), GetPaymentTypeErr);
Assert.AreEqual(PaymentTypeGbl::"2.2", PaymentTypeGbl, GetPaymentTypeErr);
end;

[Test]
[Scope('OnPrem')]
procedure VendorBankAccountGetPaymentTypeType3WhenBlankClearingNoAndSWIFTSet()
var
VendorBankAccount: Record "Vendor Bank Account";
begin
// [FEATURE] [AI test 0.3] [UT]
// [SCENARIO] "Vendor Bank Account".GetPaymentType keeps Swiss Payment Type 3 (not 2.2) when "Clearing No." is blank but a "SWIFT Code" is provided, so the IBAN-derived clearing does not reclassify SWIFT-routed accounts
VendorBankAccount.Init();
VendorBankAccount."Payment Form" := VendorBankAccount."Payment Form"::"Bank Payment Domestic";
VendorBankAccount."SWIFT Code" := GetSWIFT(true);
VendorBankAccount.IBAN := GetIBAN(true);
Assert.IsTrue(VendorBankAccount.GetPaymentType(PaymentTypeGbl, GetCurrencyCode('')), GetPaymentTypeErr);
Assert.AreEqual(PaymentTypeGbl::"3", PaymentTypeGbl, GetPaymentTypeErr);
end;

[Test]
[Scope('OnPrem')]
procedure XMLExport_PaymentType6_Negative_BlankedSWIFTCode()
Expand Down
95 changes: 95 additions & 0 deletions src/Layers/CH/Tests/Local/SwissSEPACTExport.Codeunit.al
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@ codeunit 144352 "Swiss SEPA CT Export"
Assert.IsFalse(CHMgt.IsDomesticIBAN('DE12345'), '');
end;

[Test]
[Scope('OnPrem')]
procedure CHMgt_GetClearingNoFromIBAN()
var
CHMgt: Codeunit CHMgt;
begin
// [FEATURE] [AI test 0.3] [UT]
// [SCENARIO] COD 11503 "CHMgt".GetClearingNoFromIBAN() extracts the bank clearing number (IID) from positions 5-9 of a domestic IBAN, strips leading zeros, and returns blank when it cannot be derived
// [THEN] Domestic CH IBAN: clearing number from positions 5-9 with leading zeros stripped
Assert.AreEqual('8888', CHMgt.GetClearingNoFromIBAN('CH3808888123456789012'), 'CH IBAN with a leading zero in the clearing slice');
Assert.AreEqual('12345', CHMgt.GetClearingNoFromIBAN('CH9312345678901234567'), 'CH IBAN with a full 5-digit clearing number');
// [THEN] Domestic LI IBAN is also supported
Assert.AreEqual('8800', CHMgt.GetClearingNoFromIBAN('LI2108800123456789012'), 'LI IBAN clearing slice');
// [THEN] Non-domestic IBAN returns blank
Assert.AreEqual('', CHMgt.GetClearingNoFromIBAN('DE62007620110623852957'), 'Non-domestic IBAN');
// [THEN] Domestic IBAN too short to contain positions 5-9 returns blank
Assert.AreEqual('', CHMgt.GetClearingNoFromIBAN('CH1234'), 'Too-short domestic IBAN');
// [THEN] Domestic IBAN with all-zero clearing digits returns blank
Assert.AreEqual('', CHMgt.GetClearingNoFromIBAN('CH9300000123456789012'), 'All-zero clearing slice');
end;

[Test]
[Scope('OnPrem')]
procedure CHMgt_IsSwissSEPACTExport_Negative()
Expand Down Expand Up @@ -875,6 +896,48 @@ codeunit 144352 "Swiss SEPA CT Export"
VerifyXMLFile(GenJournalLine, FileName, MessageID, PaymentTypeGbl::"2.2");
end;

[Test]
[Scope('OnPrem')]
procedure XMLExport_PaymentType22_CHF_BlankClearingNo()
var
GenJournalLine: Record "Gen. Journal Line";
LibraryXPathXMLReader: Codeunit "Library - XPath XML Reader";
ExportFile: File;
XMLInStream: InStream;
FileName: Text;
VendorNo: Code[20];
ExpectedMmbId: Code[5];
begin
// [FEATURE] [AI test 0.3] [XML] [Export]
// [SCENARIO] Swiss SEPA CT export for "Payment Type" = "2.2" derives the clearing member id (MmbId) from the domestic IBAN when "Clearing No." is blank
Initialize();

// [GIVEN] Vendor with bank account having "Payment Form" = "Bank Payment Domestic", blank "Clearing No." and "SWIFT Code", and a domestic IBAN
VendorNo := CreateVendorWithBankAccount(PaymentFormGbl::"Bank Payment Domestic", '', '', '', GetIBAN(true));
// [GIVEN] Positions 5-9 of the domestic IBAN 'CH3808888123456789012' yield clearing member id '8888'
ExpectedMmbId := '8888';

// [GIVEN] Vendor payment journal line with "Currency Code" = ""
CreatePaymentJournalLine(GenJournalLine, VendorNo, '', '',
GenJournalLine."Account Type"::Vendor, GenJournalLine."Document Type"::Payment);

// [WHEN] Export payments to file
FileName := GenJournalLine_XMLExport(GenJournalLine);

// [THEN] The payment is classified as Swiss Payment Type "2.2" (LclInstrm/Prtry = "CH03")
ExportFile.Open(FileName);
ExportFile.CreateInStream(XMLInStream);
LibraryXPathXMLReader.InitializeXml(XMLInStream, 'http://www.six-interbank-clearing.com/de/pain.001.001.03.ch.02.xsd');
ExportFile.Close();
LibraryXPathXMLReader.VerifyXmlNodeValue('//ns:PmtTpInf//ns:LclInstrm/ns:Prtry', 'CH03');
// [THEN] The creditor agent uses the CHBCC clearing system with MmbId = the IID derived from the IBAN, and no BIC
LibraryXPathXMLReader.VerifyXmlNodeAbsence('//ns:CdtrAgt//ns:BIC');
LibraryXPathXMLReader.VerifyXmlNodeValue('//ns:CdtrAgt//ns:Cd', 'CHBCC');
LibraryXPathXMLReader.VerifyXmlNodeValue('//ns:CdtrAgt//ns:MmbId', ExpectedMmbId);
// [THEN] The creditor account holds the IBAN
LibraryXPathXMLReader.VerifyXmlNodeValue('//ns:CdtrAcct//ns:IBAN', GetIBAN(true));
end;

[Test]
[Scope('OnPrem')]
procedure XMLExport_PaymentType3_Negative_BlankedSWIFT()
Expand Down Expand Up @@ -2011,6 +2074,38 @@ codeunit 144352 "Swiss SEPA CT Export"
Assert.AreEqual(PaymentTypeGbl::"6", PaymentTypeGbl, GetPaymentTypeErr);
end;

[Test]
[Scope('OnPrem')]
procedure VendorBankAccountGetPaymentTypeType22WhenClearingNoDerivedFromIBAN()
var
VendorBankAccount: Record "Vendor Bank Account";
begin
// [FEATURE] [AI test 0.3] [UT]
// [SCENARIO] "Vendor Bank Account".GetPaymentType returns Swiss Payment Type 2.2 when "Clearing No." and "SWIFT Code" are blank but the clearing number can be derived from a domestic IBAN
VendorBankAccount.Init();
VendorBankAccount."Payment Form" := VendorBankAccount."Payment Form"::"Bank Payment Domestic";
VendorBankAccount."SWIFT Code" := '';
VendorBankAccount.IBAN := GetIBAN(true);
Assert.IsTrue(VendorBankAccount.GetPaymentType(PaymentTypeGbl, GetCurrencyCode('')), GetPaymentTypeErr);
Assert.AreEqual(PaymentTypeGbl::"2.2", PaymentTypeGbl, GetPaymentTypeErr);
end;

[Test]
[Scope('OnPrem')]
procedure VendorBankAccountGetPaymentTypeType3WhenBlankClearingNoAndSWIFTSet()
var
VendorBankAccount: Record "Vendor Bank Account";
begin
// [FEATURE] [AI test 0.3] [UT]
// [SCENARIO] "Vendor Bank Account".GetPaymentType keeps Swiss Payment Type 3 (not 2.2) when "Clearing No." is blank but a "SWIFT Code" is provided, so the IBAN-derived clearing does not reclassify SWIFT-routed accounts
VendorBankAccount.Init();
VendorBankAccount."Payment Form" := VendorBankAccount."Payment Form"::"Bank Payment Domestic";
VendorBankAccount."SWIFT Code" := GetSWIFT(true);
VendorBankAccount.IBAN := GetIBAN(true);
Assert.IsTrue(VendorBankAccount.GetPaymentType(PaymentTypeGbl, GetCurrencyCode('')), GetPaymentTypeErr);
Assert.AreEqual(PaymentTypeGbl::"3", PaymentTypeGbl, GetPaymentTypeErr);
end;

[Test]
[Scope('OnPrem')]
procedure XMLExport_PaymentType6_Negative_BlankedSWIFTCode()
Expand Down
Loading