Class: SEPA::Message

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::Validations, SchemaValidation
Defined in:
lib/sepa_rator/message.rb

Direct Known Subclasses

CreditTransfer, DirectDebit

Constant Summary collapse

FAMILY =

Overridden by subclasses; used to resolve profiles via country/version hints.

nil
XML_MAIN_TAG =

Root element inside <Document>. Overridden by subclasses.

nil

Constants included from SchemaValidation

SchemaValidation::SCHEMA_CACHE, SchemaValidation::SCHEMA_CACHE_MUTEX, SchemaValidation::SCHEMA_DIR

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(country: nil, version: :latest, profile: nil, **account_options) ⇒ Message

Returns a new instance of Message.

Parameters:

  • profile (SEPA::Profile, nil) (defaults to: nil)

    explicit profile object. Mutually exclusive with ‘country:` and `version:`. Power-user path.

  • country (Symbol, nil) (defaults to: nil)

    country code of **the bank that will receive and process this XML file** (not the beneficiary’s bank). Resolves to the country-specific profile via ProfileRegistry.recommended. Falls back to the generic SEPA/EPC profile when the country has no dedicated variant registered.

  • version (Symbol) (defaults to: :latest)

    semantic version hint (‘:latest`, `:v09`, `:v13`, …). Defaults to `:latest`.

  • account_options (Hash)

    attributes for the debtor/creditor account (‘:name`, `:iban`, `:bic`, …).

Raises:

  • (ArgumentError)

    if ‘profile:` is mixed with `country:` / `version:`

  • (SEPA::UnsupportedVersionError)

    if the requested version is not registered for the resolved country



48
49
50
51
52
53
54
# File 'lib/sepa_rator/message.rb', line 48

def initialize(country: nil, version: :latest, profile: nil, **)
  @profile = resolve_profile(country: country, version: version, profile: profile)
  @grouped_transactions = {}
  @account = .new()
  validate_message_against_profile!
  (@account)
end

Instance Attribute Details

#accountObject (readonly)

Returns the value of attribute account.



13
14
15
# File 'lib/sepa_rator/message.rb', line 13

def 
  @account
end

#grouped_transactionsObject (readonly)

Returns the value of attribute grouped_transactions.



13
14
15
# File 'lib/sepa_rator/message.rb', line 13

def grouped_transactions
  @grouped_transactions
end

#initiation_source_nameObject

Returns the value of attribute initiation_source_name.



13
14
15
# File 'lib/sepa_rator/message.rb', line 13

def initiation_source_name
  @initiation_source_name
end

#initiation_source_providerObject

Returns the value of attribute initiation_source_provider.



14
15
16
# File 'lib/sepa_rator/message.rb', line 14

def initiation_source_provider
  @initiation_source_provider
end

#profileObject (readonly)

Returns the value of attribute profile.



13
14
15
# File 'lib/sepa_rator/message.rb', line 13

def profile
  @profile
end

Instance Method Details

#add_transaction(options) ⇒ Object

Add a transaction to the message. The transaction is validated both against its own ActiveModel rules and against the profile’s ‘accept_transaction` lambda + extra validators.

Parameters:

  • options (Hash)

    transaction attributes

Raises:



62
63
64
65
66
67
68
69
70
71
72
# File 'lib/sepa_rator/message.rb', line 62

def add_transaction(options)
  transaction = transaction_class.new(options)
  raise SEPA::ValidationError, transaction.errors.full_messages.join("\n") unless transaction.valid?

  validate_transaction_against_profile!(transaction)

  group = transaction_group(transaction)
  @grouped_transactions[group] ||= []
  @grouped_transactions[group] << transaction
  @transactions = nil
end

#amount_total(selected_transactions = transactions) ⇒ BigDecimal

Returns total amount.

Parameters:

  • selected_transactions (Array<Transaction>) (defaults to: transactions)

    subset to sum (defaults to all)

Returns:

  • (BigDecimal)

    total amount



108
109
110
# File 'lib/sepa_rator/message.rb', line 108

def amount_total(selected_transactions = transactions)
  selected_transactions.sum(&:amount)
end

#batch_id(transaction_reference) ⇒ Object

Find the PmtInf ID for the batch containing a transaction with the given reference.



139
140
141
142
143
144
# File 'lib/sepa_rator/message.rb', line 139

def batch_id(transaction_reference)
  grouped_transactions.each do |group, transactions|
    return payment_information_identification(group) if transactions.any? { |t| t.reference == transaction_reference }
  end
  nil
end

#batchesObject



146
147
148
# File 'lib/sepa_rator/message.rb', line 146

def batches
  grouped_transactions.keys.map { |group| payment_information_identification(group) }
end

#creation_date_timeObject



134
135
136
# File 'lib/sepa_rator/message.rb', line 134

def creation_date_time
  @creation_date_time ||= Time.now.iso8601
end

#creation_date_time=(value) ⇒ Object

Raises:

  • (ArgumentError)


125
126
127
128
129
130
131
132
# File 'lib/sepa_rator/message.rb', line 125

def creation_date_time=(value)
  raise ArgumentError, 'creation_date_time must be a string!' unless value.is_a?(String)

  regex = /[0-9]{4}-[0-9]{2,2}-[0-9]{2,2}(?:\s|T)[0-9]{2,2}:[0-9]{2,2}:[0-9]{2,2}/
  raise ArgumentError, "creation_date_time does not match #{regex}!" unless value.match?(regex)

  @creation_date_time = value
end

#message_identificationObject



121
122
123
# File 'lib/sepa_rator/message.rb', line 121

def message_identification
  @message_identification ||= "MSG/#{SecureRandom.hex(14)}"
end

#message_identification=(value) ⇒ Object

Raises:

  • (ArgumentError)


112
113
114
115
116
117
118
119
# File 'lib/sepa_rator/message.rb', line 112

def message_identification=(value)
  raise ArgumentError, 'message_identification must be a string!' unless value.is_a?(String)

  regex = %r{\A([A-Za-z0-9]|[+|?/\-:().,'\ ]){1,35}\z}
  raise ArgumentError, "message_identification does not match #{regex}!" unless value.match?(regex)

  @message_identification = value
end

#payment_information_identification(group) ⇒ Object

Unique and consecutive identifier for a <PmtInf> block, derived from ‘message_identification` + the group’s position. Invoked by builder stages.



152
153
154
155
156
# File 'lib/sepa_rator/message.rb', line 152

def payment_information_identification(group)
  suffix = "/#{grouped_transactions.keys.index(group) + 1}"
  max_prefix_length = 35 - suffix.length
  "#{message_identification[0, max_prefix_length]}#{suffix}"
end

#to_xmlString

Generate the SEPA XML document using the profile provided at construction.

Returns:

  • (String)

    UTF-8 encoded XML

Raises:



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/sepa_rator/message.rb', line 84

def to_xml
  raise SEPA::ValidationError, errors.full_messages.join("\n") unless valid?

  # Fail-safe against post-insertion mutations of the account or
  # transactions (e.g. a caller flipping `currency` after adding).
  validate_message_against_profile!
  (@account)
  transactions.each { |transaction| validate_transaction_against_profile!(transaction) }

  doc = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |builder|
    builder.Document(xml_namespace_attributes) do
      builder.__send__(self.class::XML_MAIN_TAG) do
        run_stages(profile.group_header_stages, builder)
        run_stages(profile.payment_info_stages, builder)
      end
    end
  end

  validate_final_document!(doc.doc, profile)
  doc.to_xml
end

#transactionsArray<Transaction>

Returns all transactions across all groups.

Returns:

  • (Array<Transaction>)

    all transactions across all groups



75
76
77
# File 'lib/sepa_rator/message.rb', line 75

def transactions
  @transactions ||= grouped_transactions.values.flatten
end