From 5690fa96f4a722089a937599b00b0e4886daae35 Mon Sep 17 00:00:00 2001 From: David Neugebauer Date: Wed, 1 Oct 2025 17:01:52 +0200 Subject: [PATCH] Add support for trade party contact details --- lib/secretariat.rb | 1 + lib/secretariat/constants.rb | 5 ++ lib/secretariat/invoice.rb | 4 +- lib/secretariat/trade_contact.rb | 34 ++++++++++ lib/secretariat/trade_party.rb | 15 ++++- test/invoice_test.rb | 106 +++++++++++++++++++++++++++---- 6 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 lib/secretariat/trade_contact.rb diff --git a/lib/secretariat.rb b/lib/secretariat.rb index aec4498..52e184e 100644 --- a/lib/secretariat.rb +++ b/lib/secretariat.rb @@ -22,6 +22,7 @@ require_relative 'secretariat/validation_error' require_relative 'secretariat/invoice' require_relative 'secretariat/trade_party' +require_relative 'secretariat/trade_contact' require_relative 'secretariat/line_item' require_relative 'secretariat/validator' require_relative 'secretariat/tax' diff --git a/lib/secretariat/constants.rb b/lib/secretariat/constants.rb index 1445064..f5c23cd 100644 --- a/lib/secretariat/constants.rb +++ b/lib/secretariat/constants.rb @@ -68,6 +68,11 @@ module Secretariat :UNTAXEDSERVICE => 'Not subject to VAT' } + ELECTRONIC_ADDRESS_SCHEME_CODES = { + # For other possible values: https://ec.europa.eu/digital-building-blocks/sites/spaces/DIGITAL/pages/467108974/Registry+of+supporting+artefacts+to+implement+EN16931 + :EMAIL => "EM" + } + # For the background of vertical and horizontal tax calculation see https://hilfe.pacemo.de/de-form/articles/3489851-rundungsfehler-bei-rechnungen # The idea of introducing an unknown value is that this could be inferred from the given invoice total and line items by probing both variants and selecting the matching one - or reporting a taxation error if neither matches. TAX_CALCULATION_METHODS = %i[HORIZONTAL VERTICAL NONE UNKNOWN].freeze diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index 140627b..878685a 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -18,7 +18,7 @@ module Secretariat using ObjectExtensions - + Invoice = Struct.new("Invoice", :id, :issue_date, @@ -261,7 +261,7 @@ def to_xml(version: 1, validate: true) xml['ram'].send(delivery) do if version == 2 && ship_to_or_buyer xml['ram'].ShipToTradeParty do - ship_to_or_buyer.to_xml(xml, exclude_tax: true, version: version) + ship_to_or_buyer.to_xml(xml, exclude_tax: true, exclude_email: true, exclude_contact: true, version: version) end end xml['ram'].ActualDeliverySupplyChainEvent do diff --git a/lib/secretariat/trade_contact.rb b/lib/secretariat/trade_contact.rb new file mode 100644 index 0000000..ec803ff --- /dev/null +++ b/lib/secretariat/trade_contact.rb @@ -0,0 +1,34 @@ + +module Secretariat + + TradeContact = Struct.new('TradeContact', :person_name, :department, :telephone, :email, keyword_init: true) do + def to_xml(xml, version: 2) + xml['ram'].DefinedTradeContact do + if person_name + xml['ram'].PersonName do + xml.text(person_name) + end + end + if department + xml['ram'].DepartmentName do + xml.text(department) + end + end + if telephone + xml['ram'].TelephoneUniversalCommunication do + xml['ram'].CompleteNumber do + xml.text(telephone) + end + end + end + if email + xml['ram'].EmailURIUniversalCommunication do + xml['ram'].URIID do + xml.text(email) + end + end + end + end + end + end +end diff --git a/lib/secretariat/trade_party.rb b/lib/secretariat/trade_party.rb index 8ad0125..74785a5 100644 --- a/lib/secretariat/trade_party.rb +++ b/lib/secretariat/trade_party.rb @@ -20,10 +20,10 @@ module Secretariat TradeParty = Struct.new('TradeParty', :id, :name, :street1, :street2, :city, :postal_code, :country_id, :vat_id, :global_id, :global_id_scheme_id, :tax_id, - :person_name, :legal_organization, + :person_name, :legal_organization, :email, :trade_contact, keyword_init: true, ) do - def to_xml(xml, exclude_tax: false, version: 2) + def to_xml(xml, exclude_tax: false, exclude_email: false, exclude_contact: false, version: 2) if id && !exclude_tax xml['ram'].ID id # BT-46 end @@ -40,7 +40,9 @@ def to_xml(xml, exclude_tax: false, version: 2) end end end - if person_name + if !exclude_contact && trade_contact.present? + trade_contact.to_xml(xml, version: version) + elsif !exclude_contact && person_name xml['ram'].DefinedTradeContact do xml['ram'].PersonName person_name end @@ -54,6 +56,13 @@ def to_xml(xml, exclude_tax: false, version: 2) xml['ram'].CityName city xml['ram'].CountryID country_id end + if version == 2 && !exclude_email && email.present? + xml['ram'].URIUniversalCommunication do + xml['ram'].URIID(schemeID: ELECTRONIC_ADDRESS_SCHEME_CODES[:EMAIL]) do + xml.text(email) + end + end + end if !exclude_tax && vat_id.present? xml['ram'].SpecifiedTaxRegistration do xml['ram'].ID(schemeID: 'VA') do diff --git a/test/invoice_test.rb b/test/invoice_test.rb index 8af2c76..55f7781 100644 --- a/test/invoice_test.rb +++ b/test/invoice_test.rb @@ -12,7 +12,14 @@ def make_eu_invoice(tax_category: :REVERSECHARGE, ship_to: nil) city: 'Hamburg', postal_code: '20253', country_id: 'DE', - vat_id: 'DE304755032' + vat_id: 'DE304755032', + email: 'seller@example.com', + trade_contact: TradeContact.new( + person_name: 'Alex B.', + department: 'Invoicing', + telephone: '+49012345678', + email: 'invoicing@example.com' + ) ) buyer = TradeParty.new( id: 'Kunde 4711', @@ -21,7 +28,14 @@ def make_eu_invoice(tax_category: :REVERSECHARGE, ship_to: nil) city: 'Hamburg', postal_code: '20253', country_id: 'SE', - vat_id: 'SE304755032' + vat_id: 'SE304755032', + email: 'buyer@example.com', + trade_contact: TradeContact.new( + person_name: 'Bob C.', + department: 'Purchasing', + telephone: '+49012345678', + email: 'purchasing@example.com' + ) ) line_item = LineItem.new( name: 'Depfu Starter Plan', @@ -183,6 +197,13 @@ def make_foreign_invoice(tax_category: :TAXEXEMPT) city: 'Hamburg', postal_code: '20253', country_id: 'DE', + email: 'seller@example.com', + trade_contact: TradeContact.new( + person_name: 'Alex B.', + department: 'Invoicing', + telephone: '+49012345678', + email: 'invoicing@example.com' + ) ) buyer = TradeParty.new( name: 'Another Corp Inc.', @@ -190,6 +211,13 @@ def make_foreign_invoice(tax_category: :TAXEXEMPT) city: 'Hamburg', postal_code: 'NH-2003', country_id: 'US', + email: 'buyer@example.com', + trade_contact: TradeContact.new( + person_name: 'Bob C.', + department: 'Purchasing', + telephone: '+49012345678', + email: 'purchasing@example.com' + ) ) line_item = LineItem.new( name: 'Depfu Starter Plan', @@ -233,7 +261,14 @@ def make_eu_invoice_with_attachment city: 'Hamburg', postal_code: '20253', country_id: 'DE', - vat_id: 'DE304755032' + vat_id: 'DE304755032', + email: 'seller@example.com', + trade_contact: TradeContact.new( + person_name: 'Alex B.', + department: 'Invoicing', + telephone: '+49012345678', + email: 'invoicing@example.com' + ) ) buyer = TradeParty.new( name: 'Depfu inc', @@ -241,7 +276,14 @@ def make_eu_invoice_with_attachment city: 'Hamburg', postal_code: '20253', country_id: 'SE', - vat_id: 'SE304755032' + vat_id: 'SE304755032', + email: 'buyer@example.com', + trade_contact: TradeContact.new( + person_name: 'Bob C.', + department: 'Purchasing', + telephone: '+49012345678', + email: 'purchasing@example.com' + ) ) line_item = LineItem.new( name: 'Depfu Starter Plan', @@ -291,7 +333,14 @@ def make_de_invoice city: 'Hamburg', postal_code: '20253', country_id: 'DE', - vat_id: 'DE304755032' + vat_id: 'DE304755032', + email: 'seller@example.com', + trade_contact: TradeContact.new( + person_name: 'Alex B.', + department: 'Invoicing', + telephone: '+49012345678', + email: 'invoicing@example.com' + ) ) buyer = TradeParty.new( name: 'Depfu inc', @@ -300,7 +349,14 @@ def make_de_invoice city: 'Hamburg', postal_code: '20253', country_id: 'DE', - vat_id: 'DE304755032' + vat_id: 'DE304755032', + email: 'buyer@example.com', + trade_contact: TradeContact.new( + person_name: 'Bob C.', + department: 'Purchasing', + telephone: '+49012345678', + email: 'purchasing@example.com' + ) ) line_item = LineItem.new( name: 'Depfu Starter Plan', @@ -350,7 +406,14 @@ def make_de_invoice_with_multiple_tax_rates city: 'Hamburg', postal_code: '20253', country_id: 'DE', - vat_id: 'DE304755032' + vat_id: 'DE304755032', + email: 'seller@example.com', + trade_contact: TradeContact.new( + person_name: 'Alex B.', + department: 'Invoicing', + telephone: '+49012345678', + email: 'invoicing@example.com' + ) ) buyer = TradeParty.new( name: 'Depfu inc', @@ -358,7 +421,14 @@ def make_de_invoice_with_multiple_tax_rates city: 'Hamburg', postal_code: '20253', country_id: 'DE', - vat_id: 'DE304755032' + vat_id: 'DE304755032', + email: 'buyer@example.com', + trade_contact: TradeContact.new( + person_name: 'Bob C.', + department: 'Purchasing', + telephone: '+49012345678', + email: 'purchasing@example.com' + ) ) line_item = LineItem.new( name: 'Depfu Starter Plan', @@ -431,7 +501,14 @@ def make_negative_de_invoice city: 'Hamburg', postal_code: '20253', country_id: 'DE', - vat_id: 'DE304755032' + vat_id: 'DE304755032', + email: 'seller@example.com', + trade_contact: TradeContact.new( + person_name: 'Alex B.', + department: 'Invoicing', + telephone: '+49012345678', + email: 'invoicing@example.com' + ) ) buyer = TradeParty.new( name: 'Depfu inc', @@ -439,7 +516,14 @@ def make_negative_de_invoice city: 'Hamburg', postal_code: '20253', country_id: 'DE', - vat_id: 'DE304755032' + vat_id: 'DE304755032', + email: 'buyer@example.com', + trade_contact: TradeContact.new( + person_name: 'Bob C.', + department: 'Purchasing', + telephone: '+49012345678', + email: 'purchasing@example.com' + ) ) line_item = LineItem.new( name: 'Depfu Starter Plan', @@ -851,7 +935,6 @@ def test_invoice_object_extensions xml = invoice.to_xml(version: 2) assert_match(/#{invoice.payment_reference}<\/ram:PaymentReference>/, xml) - assert_match(%r{\s*Max Mustermann\s*}, xml) assert_match(//, xml) end @@ -859,6 +942,7 @@ def test_fr_invoice invoice = make_fr_invoice xml = invoice.to_xml(version: 2) assert_match(%r{\s*304755032\s*}, xml) + assert_match(%r{\s*Max Mustermann\s*}, xml) end def test_invoice_with_quantity_causing_sub_cent_amounts