From 255b81866fe03c9393c82c4ac3b3f3895ac4a093 Mon Sep 17 00:00:00 2001 From: Philippe Date: Fri, 3 Apr 2026 16:00:36 +0200 Subject: [PATCH] feat: add pain.001.001.09, pain.001.001.13, pain.008.001.08, pain.008.001.12 support - Add support for ISO 20022 newer schema versions (.09/.13 for credit transfers, .08/.12 for direct debits) using official ISO XSDs - Use BICFI element instead of BIC for newer schemas (ISO renaming) - Use nested ReqdExctnDt/Dt structure for pain.001.001.09+ - Fix PR #117 bug: use group[:account].bic (not account.bic) in direct_debit.rb CdtrAgt block to preserve per-group creditor BIC - Add 24 new XSD validation tests (228 total, 0 failures, 100% coverage) - Include AUDIT.md with comprehensive gem audit findings --- AUDIT.md | 361 +++++ README.md | 6 +- lib/schema/pain.001.001.09.xsd | 1114 +++++++++++++++ lib/schema/pain.001.001.13.xsd | 1251 +++++++++++++++++ lib/schema/pain.008.001.08.xsd | 1106 +++++++++++++++ lib/schema/pain.008.001.12.xsd | 1135 +++++++++++++++ lib/sepa_king/message.rb | 6 +- lib/sepa_king/message/credit_transfer.rb | 26 +- lib/sepa_king/message/direct_debit.rb | 18 +- .../credit_transfer_transaction.rb | 2 +- .../transaction/direct_debit_transaction.rb | 2 +- spec/credit_transfer_spec.rb | 40 + spec/credit_transfer_transaction_spec.rb | 16 + spec/direct_debit_spec.rb | 40 + spec/direct_debit_transaction_spec.rb | 14 + 15 files changed, 5120 insertions(+), 17 deletions(-) create mode 100644 AUDIT.md create mode 100644 lib/schema/pain.001.001.09.xsd create mode 100644 lib/schema/pain.001.001.13.xsd create mode 100644 lib/schema/pain.008.001.08.xsd create mode 100644 lib/schema/pain.008.001.12.xsd diff --git a/AUDIT.md b/AUDIT.md new file mode 100644 index 0000000..b5f8e2a --- /dev/null +++ b/AUDIT.md @@ -0,0 +1,361 @@ +# Global audit — sepa_king (AdVitam fork) + +**Date**: 2026-04-03 +**Audited version**: 0.14.0 (last release: October 2022) +**Context**: Fork to add pain.001.001.09 support (PR #117) and modernize the gem for api-advitam (Ruby 3.4, Rails 8.1) + +--- + +## Executive summary + +The gem is functional and secure for basic use, but has had no maintenance for four years: outdated schemas, overly broad dependencies, and gaps in business validation. PR #117 (pain.001.001.09 / pain.008.001.08) adds the required support but includes a critical bug and German XSDs instead of pure ISO ones. + +**Deadline**: older pain.001.001.03 / pain.008.001.02 versions become obsolete in **November 2026**. + +--- + +## CRITICAL + +### C1 — PR #117 bug: `account.bic` regression in direct_debit.rb + +- **File**: `lib/sepa_king/message/direct_debit.rb`, `CdtrAgt` block +- **Issue**: The PR replaces `group[:account].bic` with `account.bic` in both branches (BICFI and BIC). When a transaction uses a `creditor_account` different from the main account, the wrong BIC is emitted. +- **Impact**: Regression for **all** existing DD schemas, not only the new .08 +- **Fix**: Restore `group[:account].bic` in both places + +```ruby +# BEFORE (buggy) +builder.BICFI(account.bic) +builder.BIC(account.bic) + +# AFTER (fixed) +builder.BICFI(group[:account].bic) +builder.BIC(group[:account].bic) +``` + +- [x] Fixed + +### C2 — Outdated schemas (2009) + +- **Issue**: Current schemas (pain.001.001.03, pain.008.001.02) date from 2009. The EPC has recommended .09/.08 since 2023; CFONB (France) mandates them for new implementations. +- **Deadline**: November 2026 for migration +- **Fix**: Merge PR #117 (with corrections) + +- [x] Fixed (+ added pain.001.001.13 and pain.008.001.12) + +### C3 — PR #117 XSDs = German DK subsets + +- **Issue**: The `pain.001.001.09.xsd` and `pain.008.001.08.xsd` files in the PR are “Technical Validation Subsets” from Deutsche Kreditwirtschaft (DK), not official ISO 20022 XSDs. They impose Germany-specific market restrictions. +- **Impact**: Risk of rejection by French banks (different structured address rules, etc.) +- **Fix**: Replace with official ISO 20022 or EPC XSDs + +- [x] Fixed (using official ISO 20022 XSDs from iso20022.org) + +--- + +## HIGH + +### H1 — Overly broad version constraints in the gemspec + +- **File**: `sepa_king.gemspec` +- **Issue**: `activemodel >= 4.2` allows versions with known CVEs (Rails 4.x, 5.x). Unconstrained `nokogiri` allows old vulnerable versions. +- **Fix**: `activemodel >= 6.1, < 9` and `nokogiri >= 1.13` + +- [ ] Fixed + +### H2 — No XML validation tests for the new schemas (PR #117) + +- **Issue**: The PR adds `schema_compatible?` tests but no `validate_against('pain.001.001.09.xsd')` or `validate_against('pain.008.001.08.xsd')`. Generated XML is never validated against the new XSDs. +- **Fix**: Add mirror tests like those for the other schemas + +- [x] Fixed (24 new XSD validation tests added) + +### H3 — Full duplication CreditorAddress / DebtorAddress + +- **Files**: `lib/sepa_king/account/creditor_address.rb`, `lib/sepa_king/account/debtor_address.rb` +- **Issue**: 38 strictly identical lines × 2. Address XML construction in `credit_transfer.rb` and `direct_debit.rb` is also duplicated. +- **Fix**: Extract `SEPA::Address` as a base class + `build_postal_address(builder, address)` on `Message` + +- [ ] Fixed + +### H4 — Flat error hierarchy + +- **File**: `lib/sepa_king/error.rb` +- **Issue**: Single class `SEPA::Error < RuntimeError`. Inconsistent mix with raw `ArgumentError`. No way to `rescue` selectively. +- **Fix**: Typed hierarchy: `SEPA::Error` → `SEPA::ValidationError`, `SEPA::SchemaValidationError` + +- [ ] Fixed + +### H5 — Potentially empty `PmtTpInf` + +- **File**: `lib/sepa_king/message/credit_transfer.rb` +- **Issue**: The `PmtTpInf` block is generated even when both `service_level` and `category_purpose` are nil, producing an empty XML element that XSD may reject. +- **Fix**: Only generate the block when at least one child is present + +- [ ] Fixed + +### H6 — No maximum amount + +- **Issue**: SEPA XSD enforces `maxInclusive 999999999.99`. Ruby validation only has `greater_than: 0`. An amount > 1 billion passes Ruby validation but fails XSD. +- **Fix**: Add `less_than_or_equal_to: 999_999_999.99` to `amount` validation + +- [ ] Fixed + +--- + +## MEDIUM + +### M1 — Validation messages exposing IBAN/BIC + +- **Files**: `lib/sepa_king/account.rb:11`, `lib/sepa_king/transaction.rb:33` +- **Issue**: `message: "%{value} is invalid"` exposes the actual IBAN/BIC in error messages. If logged to Sentry, financial data may leak. +- **Fix**: Use `message: "is invalid"` instead + +- [ ] Fixed + +### M2 — Ruby 3.4 and ActiveModel 8.x not tested in CI + +- **Files**: `.github/workflows/main.yml`, `gemfiles/` +- **Issue**: CI tests Ruby 3.0–3.3, ActiveModel 6.1–7.1. api-advitam uses Ruby 3.4.7 and Rails 8.1. +- **Fix**: Add Ruby 3.4 to the matrix, add `gemfiles/Gemfile-activemodel-8.1.x` + +- [ ] Fixed + +### M3 — Missing `frozen_string_literal: true` + +- **Issue**: No file has the pragma. All have obsolete `# encoding: utf-8` (unnecessary since Ruby 2.0). +- **Fix**: Replace `# encoding: utf-8` with `# frozen_string_literal: true` in all files + +- [ ] Fixed + +### M4 — Inconsistent `send` vs `public_send` + +- **File**: `lib/sepa_king/transaction.rb:37` +- **Issue**: `Transaction#initialize` uses `send("#{name}=", value)` while `Account#initialize` uses `public_send`. `send` can invoke private methods. +- **Fix**: Use `public_send` + +- [ ] Fixed + +### M5 — `creditor_address` declared twice + +- **Files**: `lib/sepa_king/transaction.rb`, `lib/sepa_king/transaction/credit_transfer_transaction.rb` +- **Issue**: `attr_accessor :creditor_address` exists on both parent and child. The child silently overrides the parent accessor. +- **Fix**: Remove the duplicate declaration from `CreditTransferTransaction` + +- [ ] Fixed + +### M6 — XSD schema read/parsed on every `to_xml` + +- **File**: `lib/sepa_king/message.rb:163-166` +- **Issue**: `validate_final_document!` reads and reparses the XSD file on every call. For batch generation this is a significant waste. +- **Fix**: Cache in a class constant + +```ruby +SCHEMA_CACHE = {} +def validate_final_document!(document, schema_name) + xsd = SCHEMA_CACHE[schema_name] ||= Nokogiri::XML::Schema(File.read(...)) + # ... +end +``` + +- [ ] Fixed + +### M7 — `transactions` recomputed on every call + +- **File**: `lib/sepa_king/message.rb` +- **Issue**: `transactions` does `grouped_transactions.values.flatten` on every call (4+ times during `to_xml`). No memoization. +- **Fix**: Memoize with invalidation in `add_transaction` + +- [ ] Fixed + +### M8 — Double `transaction_group()` call in `add_transaction` + +- **File**: `lib/sepa_king/message.rb` +- **Issue**: `transaction_group(transaction)` is called twice, creating two identical temporary hashes. +- **Fix**: `group = transaction_group(transaction)` then reuse + +- [ ] Fixed + +### M9 — `convert_decimal` fails silently + +- **File**: `lib/sepa_king/converter.rb` +- **Issue**: `BigDecimal(value.to_s)` inside `rescue ArgumentError` returns `nil` silently. The follow-up validation error (“is not a number”) hides the root cause. +- **Fix**: Log or raise an explicit error + +- [ ] Fixed + +### M10 — No address validation on Transaction + +- **File**: `lib/sepa_king/transaction.rb` +- **Issue**: `debtor_address` and `creditor_address` are never validated. An invalid address (e.g. 5-char `country_code`) reaches final XSD validation with a cryptic error. +- **Fix**: Validate addresses in `Transaction#valid?` + +- [ ] Fixed + +### M11 — Non-SEPA characters in `convert_text` + +- **File**: `lib/sepa_king/converter.rb` +- **Issue**: The whitelist includes `&*$%`, which are not in the basic SEPA character set (`a-z A-Z 0-9 / - ? : ( ) . , ' + space`). Banks may reject them. +- **Fix**: Remove `&*$%` from the whitelist or replace them + +- [ ] Fixed + +### M12 — No mod-97 validation on creditor identifier + +- **File**: `lib/sepa_king/validator.rb` +- **Issue**: Creditor identifier includes an ISO 7064 mod-97 check digit that is not verified. Only the regex is applied. +- **Fix**: Add mod-97 check (similar to IBAN) + +- [ ] Fixed + +### M13 — No structured remittance information (Strd) + +- **Issue**: Only `RmtInf/Ustrd` (free text) is supported. No `RmtInf/Strd/CdtrRefInf` (ISO 11649 creditor reference). Required by some institutional creditors. +- **Fix**: Add optional `structured_remittance_information` support + +- [ ] Fixed + +### M14 — `PmtInfId` can exceed 35 characters + +- **File**: `lib/sepa_king/message.rb` +- **Issue**: `PmtInfId` = `"#{message_identification}/#{index+1}"`. If MsgId is 30 chars and index > 9, length exceeds XSD’s 35 chars. +- **Fix**: Truncate or validate length + +- [ ] Fixed + +--- + +## LOW + +### B1 — COR1 deprecated + +- **File**: `lib/sepa_king/transaction/direct_debit_transaction.rb` +- `COR1` (Accelerated Direct Debit) deprecated by the EPC in November 2017. Keep for backward compatibility but add a warning. + +- [ ] Fixed + +### B2 — `required_ruby_version >= 2.7` too low + +- **File**: `sepa_king.gemspec` +- Ruby 2.7 has been EOL since 2023. Move to `>= 3.1`. + +- [ ] Fixed + +### B3 — Outdated `actions/checkout@v3` in CI + +- **File**: `.github/workflows/main.yml` +- v3 uses Node.js 16 (EOL). Upgrade to v4. + +- [ ] Fixed + +### B4 — Deprecated `add_development_dependency` + +- **File**: `sepa_king.gemspec` +- Deprecated in favor of `Gemfile` since Bundler 2.x. + +- [ ] Fixed + +### B5 — No XML injection tests + +- No tests verify behavior against XML injection attempts in text fields. Nokogiri Builder escapes automatically but tests never assert it. + +- [ ] Fixed + +### B6 — Fragile tests with `Date.today` and `Time.now` + +- Several tests compare against `Date.today` or `Time.now.iso8601`. Risk of flaky tests around midnight. + +- [ ] Fixed + +### B7 — EPC character set skewed toward German + +- **File**: `lib/sepa_king/converter.rb` +- The whitelist includes German umlauts (ÄÖÜäöüß) but not French/Spanish accents. Extended EPC set (EPC217-08) allows them. + +- [ ] Fixed + +### B8 — Undocumented `DEFAULT_REQUESTED_DATE` + +- **File**: `lib/sepa_king/transaction.rb` +- `Date.new(1999, 1, 1)` is an undocumented “as soon as possible” convention. + +- [ ] Fixed + +### B9 — No TARGET2 business-day validation + +- Execution dates on weekends/holidays are accepted but may be rejected by the bank. + +- [ ] Fixed + +### B10 — Assignment in condition in validator + +- **File**: `lib/sepa_king/validator.rb:51` +- `if ok = creditor_identifier.to_s.match?(REGEX)` — code smell; RuboCop would flag it. + +- [ ] Fixed + +--- + +## Strengths + +- **Solid XML security**: Nokogiri Builder escapes automatically; no input XML parsing (no XXE) +- **Systematic XSD validation** in `to_xml` — effective safety net +- **100% line coverage** on existing tests (204 specs, 0 failures) +- **No dangerous patterns** (eval, system, exec, Marshal, YAML.load) +- **IBAN/BIC validations** aligned with ISO 13616 / ISO 9362 +- **Clear architecture** with Template Method used well +- **Converter `convert_text`** sanitizes text fields correctly (double protection with Nokogiri) + +--- + +## Suggested action plan + +### Phase 1 — Corrected PR #117 integration + +- [ ] C1: Fix `group[:account].bic` bug +- [ ] C2: Apply PR #117 (pain.001.001.09 + pain.008.001.08) +- [ ] C3: Replace DK XSDs with ISO/EPC XSDs +- [ ] H2: Add XML validation tests + +### Phase 2 — Security and dependencies + +- [ ] H1: Tighten version constraints +- [ ] M1: Hide IBAN/BIC in validation messages +- [ ] M2: CI Ruby 3.4 + ActiveModel 8.1 +- [ ] M4: `send` → `public_send` +- [ ] B2: `required_ruby_version >= 3.1` +- [ ] B3: `actions/checkout@v4` + +### Phase 3 — Ruby modernization + +- [ ] M3: `frozen_string_literal: true` everywhere +- [ ] M5: Remove duplicate `creditor_address` +- [ ] B4: Migrate `add_development_dependency` to Gemfile +- [ ] B10: Fix assignment in condition + +### Phase 4 — Code quality and performance + +- [ ] H3: Extract `SEPA::Address` + `build_postal_address` +- [ ] H4: Typed error hierarchy +- [ ] M6: XSD schema cache +- [ ] M7: Memoize `transactions` +- [ ] M8: Double `transaction_group` call +- [ ] M9: Explicit error handling in `convert_decimal` + +### Phase 5 — SEPA/France compliance + +- [ ] H5: Do not emit empty `PmtTpInf` +- [ ] H6: Max amount validation 999 999 999.99 +- [ ] M10: Address validation on Transaction +- [ ] M11: Fix `convert_text` whitelist +- [ ] M12: Mod-97 creditor identifier validation +- [ ] M14: Truncate `PmtInfId` to 35 chars + +### Phase 6 — Desirable improvements + +- [ ] M13: Structured remittance information support +- [ ] B1: COR1 deprecation warning +- [ ] B5: XML injection tests +- [ ] B6: Freeze time in tests +- [ ] B7: Widen EPC character set +- [ ] B8: Document `DEFAULT_REQUESTED_DATE` diff --git a/README.md b/README.md index 3ee893a..75b5a01 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ We love building payment applications! So after developing the [DTAUS library fo This gem implements the following two messages out of the ISO 20022 standard: -* Credit Transfer Initiation (`pain.001.003.03`, `pain.001.002.03` and `pain.001.001.03`) -* Direct Debit Initiation (`pain.008.003.02`, `pain.008.002.02` and `pain.008.001.02`) +* Credit Transfer Initiation (`pain.001.001.13`, `pain.001.001.09`, `pain.001.003.03`, `pain.001.002.03` and `pain.001.001.03`) +* Direct Debit Initiation (`pain.008.001.12`, `pain.008.001.08`, `pain.008.003.02`, `pain.008.002.02` and `pain.008.001.02`) -It handles the _Specification of Data Formats_ v3.3 (2019-11-17). +It handles the _Specification of Data Formats_ up to the latest ISO 20022 versions. BTW: **pain** is a shortcut for **Pa**yment **In**itiation. diff --git a/lib/schema/pain.001.001.09.xsd b/lib/schema/pain.001.001.09.xsd new file mode 100644 index 0000000..d967513 --- /dev/null +++ b/lib/schema/pain.001.001.09.xsd @@ -0,0 +1,1114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/schema/pain.001.001.13.xsd b/lib/schema/pain.001.001.13.xsd new file mode 100644 index 0000000..07fd27b --- /dev/null +++ b/lib/schema/pain.001.001.13.xsd @@ -0,0 +1,1251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/schema/pain.008.001.08.xsd b/lib/schema/pain.008.001.08.xsd new file mode 100644 index 0000000..802e29d --- /dev/null +++ b/lib/schema/pain.008.001.08.xsd @@ -0,0 +1,1106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/schema/pain.008.001.12.xsd b/lib/schema/pain.008.001.12.xsd new file mode 100644 index 0000000..edc2681 --- /dev/null +++ b/lib/schema/pain.008.001.12.xsd @@ -0,0 +1,1135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/sepa_king/message.rb b/lib/sepa_king/message.rb index 8e2b8cd..f3bcdb0 100644 --- a/lib/sepa_king/message.rb +++ b/lib/sepa_king/message.rb @@ -2,9 +2,13 @@ module SEPA PAIN_008_001_02 = 'pain.008.001.02' + PAIN_008_001_08 = 'pain.008.001.08' + PAIN_008_001_12 = 'pain.008.001.12' PAIN_008_002_02 = 'pain.008.002.02' PAIN_008_003_02 = 'pain.008.003.02' PAIN_001_001_03 = 'pain.001.001.03' + PAIN_001_001_09 = 'pain.001.001.09' + PAIN_001_001_13 = 'pain.001.001.13' PAIN_001_002_03 = 'pain.001.002.03' PAIN_001_003_03 = 'pain.001.003.03' PAIN_001_001_03_CH_02 = 'pain.001.001.03.ch.02' @@ -65,7 +69,7 @@ def schema_compatible?(schema_name) case schema_name when PAIN_001_002_03, PAIN_008_002_02 account.bic.present? && transactions.all? { |t| t.schema_compatible?(schema_name) } - when PAIN_001_001_03, PAIN_001_001_03_CH_02, PAIN_001_003_03, PAIN_008_003_02, PAIN_008_001_02 + when PAIN_001_001_03, PAIN_001_001_09, PAIN_001_001_13, PAIN_001_001_03_CH_02, PAIN_001_003_03, PAIN_008_003_02, PAIN_008_001_02, PAIN_008_001_08, PAIN_008_001_12 transactions.all? { |t| t.schema_compatible?(schema_name) } end end diff --git a/lib/sepa_king/message/credit_transfer.rb b/lib/sepa_king/message/credit_transfer.rb index 1153bfb..b477376 100644 --- a/lib/sepa_king/message/credit_transfer.rb +++ b/lib/sepa_king/message/credit_transfer.rb @@ -5,7 +5,7 @@ class CreditTransfer < Message self.account_class = DebtorAccount self.transaction_class = CreditTransferTransaction self.xml_main_tag = 'CstmrCdtTrfInitn' - self.known_schemas = [ PAIN_001_001_03, PAIN_001_001_03_CH_02, PAIN_001_003_03, PAIN_001_002_03 ] + self.known_schemas = [ PAIN_001_001_03, PAIN_001_001_03_CH_02, PAIN_001_001_09, PAIN_001_001_13, PAIN_001_003_03, PAIN_001_002_03 ] private # Find groups of transactions which share the same values of some attributes @@ -39,7 +39,13 @@ def build_payment_informations(builder, schema_name) end end end - builder.ReqdExctnDt(group[:requested_date].iso8601) + if [PAIN_001_001_09, PAIN_001_001_13].include?(schema_name) + builder.ReqdExctnDt do + builder.Dt(group[:requested_date].iso8601) + end + else + builder.ReqdExctnDt(group[:requested_date].iso8601) + end builder.Dbtr do builder.Nm(account.name) end @@ -51,7 +57,11 @@ def build_payment_informations(builder, schema_name) builder.DbtrAgt do builder.FinInstnId do if account.bic - builder.BIC(account.bic) + if [PAIN_001_001_09, PAIN_001_001_13].include?(schema_name) + builder.BICFI(account.bic) + else + builder.BIC(account.bic) + end elsif schema_name != PAIN_001_001_03_CH_02 builder.Othr do builder.Id('NOTPROVIDED') @@ -64,13 +74,13 @@ def build_payment_informations(builder, schema_name) end transactions.each do |transaction| - build_transaction(builder, transaction) + build_transaction(builder, transaction, schema_name) end end end end - def build_transaction(builder, transaction) + def build_transaction(builder, transaction, schema_name) builder.CdtTrfTxInf do builder.PmtId do if transaction.instruction.present? @@ -84,7 +94,11 @@ def build_transaction(builder, transaction) if transaction.bic builder.CdtrAgt do builder.FinInstnId do - builder.BIC(transaction.bic) + if [PAIN_001_001_09, PAIN_001_001_13].include?(schema_name) + builder.BICFI(transaction.bic) + else + builder.BIC(transaction.bic) + end end end end diff --git a/lib/sepa_king/message/direct_debit.rb b/lib/sepa_king/message/direct_debit.rb index b643e13..d6e0179 100644 --- a/lib/sepa_king/message/direct_debit.rb +++ b/lib/sepa_king/message/direct_debit.rb @@ -5,7 +5,7 @@ class DirectDebit < Message self.account_class = CreditorAccount self.transaction_class = DirectDebitTransaction self.xml_main_tag = 'CstmrDrctDbtInitn' - self.known_schemas = [ PAIN_008_001_02, PAIN_008_003_02, PAIN_008_002_02 ] + self.known_schemas = [ PAIN_008_001_02, PAIN_008_001_08, PAIN_008_001_12, PAIN_008_003_02, PAIN_008_002_02 ] validate do |record| if record.transactions.map(&:local_instrument).uniq.size > 1 @@ -54,7 +54,11 @@ def build_payment_informations(builder, schema_name) builder.CdtrAgt do builder.FinInstnId do if group[:account].bic - builder.BIC(group[:account].bic) + if [PAIN_008_001_08, PAIN_008_001_12].include?(schema_name) + builder.BICFI(group[:account].bic) + else + builder.BIC(group[:account].bic) + end else builder.Othr do builder.Id('NOTPROVIDED') @@ -77,7 +81,7 @@ def build_payment_informations(builder, schema_name) end transactions.each do |transaction| - build_transaction(builder, transaction) + build_transaction(builder, transaction, schema_name) end end end @@ -123,7 +127,7 @@ def build_amendment_informations(builder, transaction) end end - def build_transaction(builder, transaction) + def build_transaction(builder, transaction, schema_name) builder.DrctDbtTxInf do builder.PmtId do if transaction.instruction.present? @@ -142,7 +146,11 @@ def build_transaction(builder, transaction) builder.DbtrAgt do builder.FinInstnId do if transaction.bic - builder.BIC(transaction.bic) + if [PAIN_008_001_08, PAIN_008_001_12].include?(schema_name) + builder.BICFI(transaction.bic) + else + builder.BIC(transaction.bic) + end else builder.Othr do builder.Id('NOTPROVIDED') diff --git a/lib/sepa_king/transaction/credit_transfer_transaction.rb b/lib/sepa_king/transaction/credit_transfer_transaction.rb index 8c031c2..360bdf6 100644 --- a/lib/sepa_king/transaction/credit_transfer_transaction.rb +++ b/lib/sepa_king/transaction/credit_transfer_transaction.rb @@ -17,7 +17,7 @@ def initialize(attributes = {}) def schema_compatible?(schema_name) case schema_name - when PAIN_001_001_03 + when PAIN_001_001_03, PAIN_001_001_09, PAIN_001_001_13 !self.service_level || (self.service_level == 'SEPA' && self.currency == 'EUR') when PAIN_001_002_03 self.bic.present? && self.service_level == 'SEPA' && self.currency == 'EUR' diff --git a/lib/sepa_king/transaction/direct_debit_transaction.rb b/lib/sepa_king/transaction/direct_debit_transaction.rb index 65b39ef..e7b56f3 100644 --- a/lib/sepa_king/transaction/direct_debit_transaction.rb +++ b/lib/sepa_king/transaction/direct_debit_transaction.rb @@ -48,7 +48,7 @@ def schema_compatible?(schema_name) self.bic.present? && %w(CORE B2B).include?(self.local_instrument) && self.currency == 'EUR' when PAIN_008_003_02 self.currency == 'EUR' - when PAIN_008_001_02 + when PAIN_008_001_02, PAIN_008_001_08, PAIN_008_001_12 true end end diff --git a/spec/credit_transfer_spec.rb b/spec/credit_transfer_spec.rb index 547546d..d3146f0 100644 --- a/spec/credit_transfer_spec.rb +++ b/spec/credit_transfer_spec.rb @@ -65,6 +65,14 @@ it 'should validate against pain.001.003.03' do expect(subject.to_xml(SEPA::PAIN_001_003_03)).to validate_against('pain.001.003.03.xsd') end + + it 'should validate against pain.001.001.09' do + expect(subject.to_xml(SEPA::PAIN_001_001_09)).to validate_against('pain.001.001.09.xsd') + end + + it 'should validate against pain.001.001.13' do + expect(subject.to_xml(SEPA::PAIN_001_001_13)).to validate_against('pain.001.001.13.xsd') + end end context 'setting creditor address with structured fields' do @@ -93,6 +101,14 @@ it 'should validate against pain.001.001.03' do expect(subject.to_xml(SEPA::PAIN_001_001_03)).to validate_against('pain.001.001.03.xsd') end + + it 'should validate against pain.001.001.09' do + expect(subject.to_xml(SEPA::PAIN_001_001_09)).to validate_against('pain.001.001.09.xsd') + end + + it 'should validate against pain.001.001.13' do + expect(subject.to_xml(SEPA::PAIN_001_001_13)).to validate_against('pain.001.001.13.xsd') + end end context 'for valid debtor' do @@ -122,6 +138,14 @@ expect(subject.to_xml(SEPA::PAIN_001_001_03)).to validate_against('pain.001.001.03.xsd') end + it 'should validate against pain.001.001.09' do + expect(subject.to_xml(SEPA::PAIN_001_001_09)).to validate_against('pain.001.001.09.xsd') + end + + it 'should validate against pain.001.001.13' do + expect(subject.to_xml(SEPA::PAIN_001_001_13)).to validate_against('pain.001.001.13.xsd') + end + context 'with CHF as currency' do let(:currency) { 'CHF' } @@ -162,6 +186,14 @@ it 'should validate against pain.001.003.03' do expect(subject.to_xml('pain.001.003.03')).to validate_against('pain.001.003.03.xsd') end + + it 'should validate against pain.001.001.09' do + expect(subject.to_xml(SEPA::PAIN_001_001_09)).to validate_against('pain.001.001.09.xsd') + end + + it 'should validate against pain.001.001.13' do + expect(subject.to_xml(SEPA::PAIN_001_001_13)).to validate_against('pain.001.001.13.xsd') + end end context 'without requested_date given' do @@ -415,6 +447,14 @@ expect(subject.to_xml('pain.001.001.03')).to validate_against('pain.001.001.03.xsd') end + it 'should validate against pain.001.001.09' do + expect(subject.to_xml(SEPA::PAIN_001_001_09)).to validate_against('pain.001.001.09.xsd') + end + + it 'should validate against pain.001.001.13' do + expect(subject.to_xml(SEPA::PAIN_001_001_13)).to validate_against('pain.001.001.13.xsd') + end + it 'should fail for pain.001.002.03' do expect { subject.to_xml(SEPA::PAIN_001_002_03) diff --git a/spec/credit_transfer_transaction_spec.rb b/spec/credit_transfer_transaction_spec.rb index 3fdd2e7..f8d1269 100644 --- a/spec/credit_transfer_transaction_spec.rb +++ b/spec/credit_transfer_transaction_spec.rb @@ -50,6 +50,22 @@ expect(SEPA::CreditTransferTransaction.new(:bic => 'SPUEDE2UXXX', :currency => 'CHF')).to be_schema_compatible('pain.001.001.03.ch.02') end end + + context 'for pain.001.001.09' do + it 'should succeed for valid attributes' do + expect(SEPA::CreditTransferTransaction.new(:bic => 'SPUEDE2UXXX')).to be_schema_compatible('pain.001.001.09') + expect(SEPA::CreditTransferTransaction.new(:bic => nil)).to be_schema_compatible('pain.001.001.09') + expect(SEPA::CreditTransferTransaction.new(:bic => 'SPUEDE2UXXX', :currency => 'CHF')).to be_schema_compatible('pain.001.001.09') + end + end + + context 'for pain.001.001.13' do + it 'should succeed for valid attributes' do + expect(SEPA::CreditTransferTransaction.new(:bic => 'SPUEDE2UXXX')).to be_schema_compatible('pain.001.001.13') + expect(SEPA::CreditTransferTransaction.new(:bic => nil)).to be_schema_compatible('pain.001.001.13') + expect(SEPA::CreditTransferTransaction.new(:bic => 'SPUEDE2UXXX', :currency => 'CHF')).to be_schema_compatible('pain.001.001.13') + end + end end context 'Requested date' do diff --git a/spec/direct_debit_spec.rb b/spec/direct_debit_spec.rb index 1d7bd13..89c0f0d 100644 --- a/spec/direct_debit_spec.rb +++ b/spec/direct_debit_spec.rb @@ -100,6 +100,14 @@ it 'should validate against pain.008.003.02' do expect(subject.to_xml(SEPA::PAIN_008_003_02)).to validate_against('pain.008.003.02.xsd') end + + it 'should validate against pain.008.001.08' do + expect(subject.to_xml(SEPA::PAIN_008_001_08)).to validate_against('pain.008.001.08.xsd') + end + + it 'should validate against pain.008.001.12' do + expect(subject.to_xml(SEPA::PAIN_008_001_12)).to validate_against('pain.008.001.12.xsd') + end end context 'setting debtor address with structured fields' do @@ -130,6 +138,14 @@ it 'should validate against pain.008.001.02' do expect(subject.to_xml(SEPA::PAIN_008_001_02)).to validate_against('pain.008.001.02.xsd') end + + it 'should validate against pain.008.001.08' do + expect(subject.to_xml(SEPA::PAIN_008_001_08)).to validate_against('pain.008.001.08.xsd') + end + + it 'should validate against pain.008.001.12' do + expect(subject.to_xml(SEPA::PAIN_008_001_12)).to validate_against('pain.008.001.12.xsd') + end end context 'for valid creditor' do @@ -164,6 +180,14 @@ it 'should validate against pain.008.001.02' do expect(subject.to_xml(SEPA::PAIN_008_001_02)).to validate_against('pain.008.001.02.xsd') end + + it 'should validate against pain.008.001.08' do + expect(subject.to_xml(SEPA::PAIN_008_001_08)).to validate_against('pain.008.001.08.xsd') + end + + it 'should validate against pain.008.001.12' do + expect(subject.to_xml(SEPA::PAIN_008_001_12)).to validate_against('pain.008.001.12.xsd') + end end context 'with BIC' do @@ -193,6 +217,14 @@ it 'should validate against pain.008.003.02' do expect(subject.to_xml(SEPA::PAIN_008_003_02)).to validate_against('pain.008.003.02.xsd') end + + it 'should validate against pain.008.001.08' do + expect(subject.to_xml(SEPA::PAIN_008_001_08)).to validate_against('pain.008.001.08.xsd') + end + + it 'should validate against pain.008.001.12' do + expect(subject.to_xml(SEPA::PAIN_008_001_12)).to validate_against('pain.008.001.12.xsd') + end end context 'with BIC and debtor address ' do @@ -229,6 +261,14 @@ it 'should validate against pain.008.003.02' do expect(subject.to_xml(SEPA::PAIN_008_003_02)).to validate_against('pain.008.003.02.xsd') end + + it 'should validate against pain.008.001.08' do + expect(subject.to_xml(SEPA::PAIN_008_001_08)).to validate_against('pain.008.001.08.xsd') + end + + it 'should validate against pain.008.001.12' do + expect(subject.to_xml(SEPA::PAIN_008_001_12)).to validate_against('pain.008.001.12.xsd') + end end context 'without requested_date given' do diff --git a/spec/direct_debit_transaction_spec.rb b/spec/direct_debit_transaction_spec.rb index dc4d005..2c36481 100644 --- a/spec/direct_debit_transaction_spec.rb +++ b/spec/direct_debit_transaction_spec.rb @@ -45,6 +45,20 @@ expect(SEPA::DirectDebitTransaction.new(:bic => 'SPUEDE2UXXX', :currency => 'CHF')).to be_schema_compatible('pain.008.001.02') end end + + context 'for pain.008.001.08' do + it 'should succeed for valid attributes' do + expect(SEPA::DirectDebitTransaction.new(:bic => 'SPUEDE2UXXX', :currency => 'CHF')).to be_schema_compatible('pain.008.001.08') + expect(SEPA::DirectDebitTransaction.new(:bic => nil)).to be_schema_compatible('pain.008.001.08') + end + end + + context 'for pain.008.001.12' do + it 'should succeed for valid attributes' do + expect(SEPA::DirectDebitTransaction.new(:bic => 'SPUEDE2UXXX', :currency => 'CHF')).to be_schema_compatible('pain.008.001.12') + expect(SEPA::DirectDebitTransaction.new(:bic => nil)).to be_schema_compatible('pain.008.001.12') + end + end end context 'Mandate Date of Signature' do