factur-x-builder 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac6a0f9f68bea474096e87b73b676318343d3aff63d83dbc452f742a67ebb8f8
4
- data.tar.gz: 7c07d34573f36f5f16ce094ab8f84df948d288ab052e3a3c69ea1370176b1470
3
+ metadata.gz: 2f380d59a7b981bd4999314d3321a99dc762ac9c67709f6e543cc5632eade2c4
4
+ data.tar.gz: 553e1dda58b31b9804ede1acc9c75ff4ee515b1bcf152a508fb56bca7ebf2357
5
5
  SHA512:
6
- metadata.gz: 3cff2a09eae0ec30028c99e5453a5149154b9b45869dfa4bd88950e4803aeb58af4455f49b154f2e1b715ffe5877e38cdff482b0e190a57bab9fc4cc9af1afdd
7
- data.tar.gz: 7376dda081fe40784a55c71554215951cf7ec4d0f2a2f67e76e0370bbf0b8b927da565e1754722e2890b8591becb670ce3472d523f0040569f05a9d65e56e72e
6
+ metadata.gz: 54363eb24098df15598c699e48956247464bc739435af190736762e0707b77513f98e12b836db743e832b98672ef989fc225833c08ab06402b391bd10474f83d
7
+ data.tar.gz: 71355fc896353819a7d66da2b3d1fb078f251184858add396e3d25bc6475535c6eb7593ffeb6ec23b48fb4900e9fe8a76440b8576e10dc20173c41e556461223
data/CHANGELOG.md CHANGED
@@ -1,4 +1,18 @@
1
- ## [Unreleased]
1
+ ## [0.1.2] - 2026-08-15
2
+
3
+ ### Added
4
+
5
+ - `Codes::PaymentMeansType::ALL` lists the 84 UNTDID 4461 codes EN 16931 permits for BT-81, and a
6
+ payment means outside that list is now rejected instead of producing XML the schematron refuses.
7
+ More codes are named (`PAYMENT_TO_BANK_ACCOUNT`, `CREDIT_CARD`, `ONLINE_PAYMENT_SERVICE`,
8
+ `MUTUALLY_DEFINED`, …); the constants are a convenience, not the permitted set.
9
+ - BT-87 is checked against BR-51, which caps a card number at 10 characters — the first 6 and last
10
+ 4 digits PCI DSS allows to be displayed. Masking the middle with asterisks overflows that limit
11
+ and was previously emitted, producing XML the schematron refuses.
12
+ - Unit prices are checked against BR-27 and BR-28: a negative BT-146 or BT-148 is rejected with a
13
+ message pointing at credit notes, which use type code 381 with positive amounts.
14
+
15
+ ## [0.1.1] - 2026-08-10
2
16
 
3
17
  ### Fixed
4
18
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- factur-x-builder (0.1.0)
4
+ factur-x-builder (0.1.1)
5
5
  nokogiri (~> 1.15, < 1.16)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -112,6 +112,47 @@ FacturX.violations(invoice) # => ["...", "..."]
112
112
  FacturX.valid?(invoice) # => false
113
113
  ```
114
114
 
115
+ ### Payment states
116
+
117
+ An issued invoice is immutable. Payments received **after** issuance are not written back into it —
118
+ they are reported to the platform as lifecycle statuses (`Encaissée`), a separate message flow that
119
+ this gem does not produce and is not meant to.
120
+
121
+ What the invoice records is the state **at issuance**, through BT-113 and BT-115:
122
+
123
+ | Situation | `prepaid` (BT-113) | `due_payable` (BT-115) | `type_code` |
124
+ |---|---|---|---|
125
+ | Nothing paid | omitted | grand total | `380` |
126
+ | Paid before issuance | grand total | `0.00` | `380` |
127
+ | Deposit collected before issuance | the deposit | the balance | `380` |
128
+ | Cancellation | omitted | amount to refund | `381` + `preceding_invoices` |
129
+
130
+ `BR-CO-16` ties them together — `BT-115 = BT-112 − BT-113 + BT-114` — and is checked before
131
+ anything is generated.
132
+
133
+ A credit note carries **positive** amounts; the type code conveys the direction. Negative unit
134
+ prices are rejected (`BR-27`, `BR-28`).
135
+
136
+ ### Payment instructions are optional
137
+
138
+ `payment_means` maps to BG-16 *Payment instructions*, which both schematrons accept as absent. It
139
+ states how the seller **expects** to be paid — it is not a record of the buyer's choice, since the
140
+ invoice precedes the payment. Several means may be declared at once.
141
+
142
+ Once a means is declared, the rules apply: a credit transfer (`30`, `58`) requires an account
143
+ identifier (BR-61, BR-CO-27), and the code must belong to UNTDID 4461 —
144
+ `Codes::PaymentMeansType::ALL` holds the 84 permitted values, checked against the code database the
145
+ official schematron uses. The named constants (`CREDIT_TRANSFER`, `BANK_CARD`, …) cover the common
146
+ cases; any other code can be passed as a plain string.
147
+
148
+ A platform may narrow the list further. Chorus Pro accepts only `30`, `42`, `48`, `49`, `58`, `59`
149
+ and `97` for the public sector — a restriction no schematron encodes, so the gem cannot catch it.
150
+
151
+ Nothing downstream contradicts it. The `Encaissée` lifecycle status reports only the invoice number,
152
+ the payment date and the amount collected per VAT rate — the means actually used is not among the
153
+ data transmitted, so a buyer paying by cheque against an invoice quoting a transfer raises no
154
+ compliance issue.
155
+
115
156
  ### One address per party
116
157
 
117
158
  EN 16931 gives each party exactly one `PostalTradeAddress`, and it must be the address of the
@@ -37,15 +37,34 @@ module FacturX
37
37
  REQUIRING_EXEMPTION_REASON = [EXEMPT, REVERSE_CHARGE, INTRA_COMMUNITY, EXPORT].freeze
38
38
  end
39
39
 
40
+ # NOTE: Chorus Pro only accepts 30, 42, 48, 49, 58, 59 and 97
40
41
  module PaymentMeansType
42
+ NOT_DEFINED = "1"
41
43
  CASH = "10"
42
44
  CHEQUE = "20"
43
45
  CREDIT_TRANSFER = "30"
46
+ PAYMENT_TO_BANK_ACCOUNT = "42"
47
+ # The instrument, whatever the channel: a card read on a physical
48
+ # terminal and a card entered on a web page are both 48.
44
49
  BANK_CARD = "48"
45
50
  DIRECT_DEBIT = "49"
51
+ CREDIT_CARD = "54"
52
+ DEBIT_CARD = "55"
53
+ STANDING_AGREEMENT = "57"
46
54
  SEPA_CREDIT_TRANSFER = "58"
47
55
  SEPA_DIRECT_DEBIT = "59"
48
- NOT_DEFINED = "1"
56
+ # The service is itself the instrument - a wallet or a provider that
57
+ # collects in its own name. A card payment through a payment page is
58
+ # still BANK_CARD, not this.
59
+ ONLINE_PAYMENT_SERVICE = "68"
60
+ CLEARING_BETWEEN_PARTNERS = "97"
61
+ MUTUALLY_DEFINED = "ZZZ"
62
+
63
+ # The complete UNTDID 4461 subset of the EN 16931 code list, as published
64
+ # in the Factur-X code database used by the official schematron.
65
+ ALL = (
66
+ (1..70).to_a + (74..78).to_a + (91..98).to_a
67
+ ).map(&:to_s).push("ZZZ").freeze
49
68
 
50
69
  REQUIRING_CREDITOR_ACCOUNT = [CREDIT_TRANSFER, SEPA_CREDIT_TRANSFER].freeze
51
70
  REQUIRING_MANDATE = [DIRECT_DEBIT, SEPA_DIRECT_DEBIT].freeze
data/lib/factur_x/line.rb CHANGED
@@ -7,7 +7,7 @@ module FacturX
7
7
  class Line
8
8
  attr_reader :number, :name, :description, :seller_item_id, :buyer_item_id,
9
9
  :global_id, :global_id_scheme, :unit_price, :gross_unit_price,
10
- :unit_discount, :quantity, :unit_code, :vat_category, :vat_rate,
10
+ :unit_discount, :quantity, :unit_code, :vat_category, :vat_rate, :vat_amount,
11
11
  :net_amount, :note, :origin_country, :period_start, :period_end,
12
12
  :order_line_id, :accounting_reference
13
13
 
@@ -15,7 +15,7 @@ module FacturX
15
15
  number: nil, description: nil, seller_item_id: nil, buyer_item_id: nil,
16
16
  global_id: nil, global_id_scheme: nil, gross_unit_price: nil,
17
17
  unit_discount: nil, unit_code: Codes::PIECE_UNIT,
18
- vat_category: Codes::VatCategory::STANDARD, vat_rate: nil,
18
+ vat_category: Codes::VatCategory::STANDARD, vat_rate: nil, vat_amount: nil,
19
19
  note: nil, origin_country: nil, period_start: nil, period_end: nil,
20
20
  order_line_id: nil, accounting_reference: nil)
21
21
  @number = number
@@ -32,6 +32,7 @@ module FacturX
32
32
  @unit_code = unit_code
33
33
  @vat_category = vat_category
34
34
  @vat_rate = vat_rate
35
+ @vat_amount = vat_amount
35
36
  @net_amount = net_amount
36
37
  @note = note
37
38
  @origin_country = origin_country
@@ -72,8 +73,8 @@ module FacturX
72
73
  global_id: global_id, global_id_scheme: global_id_scheme,
73
74
  unit_price: unit_price, gross_unit_price: gross_unit_price,
74
75
  unit_discount: unit_discount, quantity: quantity, unit_code: unit_code,
75
- vat_category: vat_category, vat_rate: vat_rate, net_amount: net_amount,
76
- note: note, origin_country: origin_country,
76
+ vat_category: vat_category, vat_rate: vat_rate, vat_amount: vat_amount,
77
+ net_amount: net_amount, note: note, origin_country: origin_country,
77
78
  period_start: period_start, period_end: period_end,
78
79
  order_line_id: order_line_id, accounting_reference: accounting_reference
79
80
  }
@@ -22,6 +22,8 @@ module FacturX
22
22
 
23
23
  ROUNDING_TOLERANCE = BigDecimal("0.01")
24
24
 
25
+ CARD_NUMBER_MAX_LENGTH = 10
26
+
25
27
  PROFILES = %i[en16931 fr_ctc].freeze
26
28
 
27
29
  # BR-FR-05: every invoice must carry these three statements.
@@ -174,7 +176,25 @@ module FacturX
174
176
  add("#{label}: name is required (BT-153)") if blank?(line.name)
175
177
  add("#{label}: unit code is required (BT-130)") if blank?(line.unit_code)
176
178
  check_vat_category(line.vat_category, line.vat_rate, label)
179
+ check_unit_prices(line, label)
180
+ end
181
+ end
182
+
183
+ # A credit note carries positive amounts: the document type code (BT-3)
184
+ # conveys the direction, not the sign of the prices.
185
+ def check_unit_prices(line, label)
186
+ if negative?(line.unit_price)
187
+ add("#{label}: unit price must not be negative (BT-146, BR-27) - a credit note uses " \
188
+ "type code #{Codes::CREDIT_NOTE} with positive amounts")
177
189
  end
190
+
191
+ return unless negative?(line.gross_unit_price)
192
+
193
+ add("#{label}: gross unit price must not be negative (BT-148, BR-28)")
194
+ end
195
+
196
+ def negative?(value)
197
+ !value.nil? && Formatting.decimal(value).negative?
178
198
  end
179
199
 
180
200
  def check_vat_category(category, rate, label)
@@ -274,6 +294,9 @@ module FacturX
274
294
  [item.vat_category, Formatting.decimal(item.vat_rate || 0)]
275
295
  end
276
296
 
297
+ # Payment instructions (BG-16) are optional: an invoice with no payment
298
+ # means at all satisfies both schematrons. What is checked here applies
299
+ # only once a payment means has been declared.
277
300
  def check_payment
278
301
  invoice.payment_means.each do |means|
279
302
  if blank?(means.type_code)
@@ -281,18 +304,39 @@ module FacturX
281
304
  next
282
305
  end
283
306
 
307
+ unless Codes::PaymentMeansType::ALL.include?(means.type_code.to_s)
308
+ add("payment means #{means.type_code.inspect} is not a UNTDID 4461 code (BT-81)")
309
+ next
310
+ end
311
+
284
312
  if Codes::PaymentMeansType::REQUIRING_CREDITOR_ACCOUNT.include?(means.type_code) &&
285
313
  !means.creditor_account?
286
314
  add("payment means #{means.type_code} (credit transfer) requires a creditor " \
287
- "account identifier (BR-61)")
315
+ "account identifier - an IBAN or a proprietary account id (BR-61, BR-CO-27)")
288
316
  end
289
317
 
290
318
  if means.iban && !means.iban.to_s.delete(" ").match?(IBAN_PATTERN)
291
319
  add("payment IBAN #{means.iban.inspect} is not a well-formed IBAN")
292
320
  end
321
+
322
+ check_card_number(means)
293
323
  end
294
324
  end
295
325
 
326
+ # BR-51 caps BT-87 at 10 characters, which is what PCI DSS allows to be
327
+ # displayed: the first 6 and last 4 digits. Masking the middle with
328
+ # asterisks overflows that limit and is rejected.
329
+ def check_card_number(means)
330
+ return if means.card_id.nil?
331
+
332
+ length = means.card_id.to_s.strip.length
333
+ return if length <= CARD_NUMBER_MAX_LENGTH
334
+
335
+ add("payment card number is #{length} characters; an invoice must never carry a full " \
336
+ "card number (BT-87, BR-51). Keep at most #{CARD_NUMBER_MAX_LENGTH} characters - " \
337
+ "the first 6 and last 4 digits, without masking characters")
338
+ end
339
+
296
340
  def check_totals
297
341
  totals = invoice.totals
298
342
  if totals.nil?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FacturX
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factur-x-builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hotentic
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-10 00:00:00.000000000 Z
11
+ date: 2026-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri