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 +4 -4
- data/CHANGELOG.md +15 -1
- data/Gemfile.lock +1 -1
- data/README.md +41 -0
- data/lib/factur_x/codes.rb +20 -1
- data/lib/factur_x/line.rb +5 -4
- data/lib/factur_x/validation/rules.rb +45 -1
- data/lib/factur_x/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f380d59a7b981bd4999314d3321a99dc762ac9c67709f6e543cc5632eade2c4
|
|
4
|
+
data.tar.gz: 553e1dda58b31b9804ede1acc9c75ff4ee515b1bcf152a508fb56bca7ebf2357
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54363eb24098df15598c699e48956247464bc739435af190736762e0707b77513f98e12b836db743e832b98672ef989fc225833c08ab06402b391bd10474f83d
|
|
7
|
+
data.tar.gz: 71355fc896353819a7d66da2b3d1fb078f251184858add396e3d25bc6475535c6eb7593ffeb6ec23b48fb4900e9fe8a76440b8576e10dc20173c41e556461223
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
## [
|
|
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
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
|
data/lib/factur_x/codes.rb
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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?
|
data/lib/factur_x/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-08-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|